Skip to content

Commit 534b4f9

Browse files
authored
Merge pull request #4855 from volodya-lombrozo/4850-cache-thread-safety
feat(#4850): implement thread-safe caching with ConcurrentCache class
2 parents b284e08 + 561e5b9 commit 534b4f9

File tree

3 files changed

+109
-7
lines changed

3 files changed

+109
-7
lines changed

eo-maven-plugin/src/main/java/org/eolang/maven/Cache.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,8 @@
1515

1616
/**
1717
* Simple cache mechanism.
18+
* This class isn't thread-safe, use {@link ConcurrentCache} for concurrent scenarios.
1819
* @since 0.60
19-
* @todo #4846:30min Make Cache thread-safe.
20-
* The Cache class lacks thread-safety mechanisms. If multiple threads call the apply method
21-
* concurrently with the same source file, there could be race conditions where both threads
22-
* determine the cache is stale and attempt to write simultaneously, potentially leading to
23-
* corrupted files or inconsistent state. Consider adding synchronization or using atomic file
24-
* operations. See ChCachedTest.java:63-86 for an example of how concurrency is tested in similar
25-
* caching classes in this codebase.
2620
* @todo #4846:30min Replace {@link FpDefault} with {@link Cache}.
2721
* The FpDefault class currently implements caching logic that is similar to the Cache class.
2822
* Refactor the codebase to use Cache instead of FpDefault for caching functionality to
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package org.eolang.maven;
6+
7+
import java.nio.file.Path;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import java.util.concurrent.ConcurrentMap;
10+
11+
/**
12+
* Concurrent cache wrapper for Cache.
13+
* Wrap {@link Cache} to make it thread-safe.
14+
* @since 0.60
15+
*/
16+
final class ConcurrentCache {
17+
18+
/**
19+
* Original cache.
20+
*/
21+
private final Cache original;
22+
23+
/**
24+
* Locks for each cache entry.
25+
*/
26+
private final ConcurrentMap<? super Path, Object> locks;
27+
28+
/**
29+
* Ctor.
30+
* @param original Original cache
31+
*/
32+
ConcurrentCache(final Cache original) {
33+
this(original, new ConcurrentHashMap<>(0));
34+
}
35+
36+
/**
37+
* Ctor.
38+
* @param original Original cache
39+
* @param locks Locks map
40+
*/
41+
private ConcurrentCache(final Cache original, final ConcurrentMap<? super Path, Object> locks) {
42+
this.original = original;
43+
this.locks = locks;
44+
}
45+
46+
/**
47+
* Check cache and apply compilation if needed.
48+
* @param source From file
49+
* @param target To file
50+
* @param tail Tail path in cache
51+
*/
52+
void apply(final Path source, final Path target, final Path tail) {
53+
final Object loc = this.locks.computeIfAbsent(tail.normalize(), k -> new Object());
54+
synchronized (loc) {
55+
this.original.apply(source, target, tail.normalize());
56+
}
57+
}
58+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
package org.eolang.maven;
6+
7+
import com.yegor256.Mktmp;
8+
import com.yegor256.MktmpResolver;
9+
import java.io.IOException;
10+
import java.nio.file.Path;
11+
import java.util.concurrent.atomic.AtomicInteger;
12+
import java.util.stream.Collectors;
13+
import java.util.stream.IntStream;
14+
import org.hamcrest.MatcherAssert;
15+
import org.hamcrest.Matchers;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.ExtendWith;
18+
19+
/**
20+
* Test for {@link ConcurrentCache}.
21+
* @since 0.60
22+
*/
23+
@ExtendWith(MktmpResolver.class)
24+
final class ConcurrentCacheTest {
25+
26+
@Test
27+
void triesToCompileProgramConcurrently(@Mktmp final Path temp) throws IOException {
28+
final AtomicInteger counter = new AtomicInteger(0);
29+
final var cache = new ConcurrentCache(
30+
new Cache(
31+
temp.resolve("cache"),
32+
p -> String.format("only once %d", counter.incrementAndGet())
33+
)
34+
);
35+
final var source = temp.resolve("program.eo");
36+
new Saved("[] > main\n (stdout \"Hello, EO!\") > @\n", source).value();
37+
final var target = temp.resolve("program.xmir");
38+
new Threaded<>(
39+
IntStream.range(0, 100).boxed().collect(Collectors.toList()), ignored -> {
40+
cache.apply(source, target, source.getFileName());
41+
return ignored;
42+
}
43+
).total();
44+
MatcherAssert.assertThat(
45+
"Program must be compiled only once",
46+
counter.get(),
47+
Matchers.equalTo(1)
48+
);
49+
}
50+
}

0 commit comments

Comments
 (0)