Skip to content

Commit 468d705

Browse files
Merge pull request #12 from danieladams456/concurrency-fix
Merge Status Concurrency Fix
2 parents 4c95802 + 4d94f41 commit 468d705

4 files changed

Lines changed: 86 additions & 19 deletions

File tree

src/main/java/com/github/danieladams456/statusvoter/StatusVoter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ public StatusVoter() {
1414
/**
1515
* Merges the incoming classification with the existing status classification.
1616
* The status classification with the higher score is retained.
17+
* <p>
18+
* This method is marked as synchronized to avoid concurrency bugs.
19+
* It is not a huge penalty to mark the whole method as such since it
20+
* is fast in comparison to the work this status is tracking.
1721
*
1822
* @param newClassification the new, incoming classification to merge with the existing classification
1923
*/
20-
public void merge(StatusClassification newClassification) {
24+
public synchronized void merge(StatusClassification newClassification) {
2125
// null value is reset to a legitimate non-null value
2226
if (null == newClassification) {
2327
newClassification = StatusClassification.INTERNAL_STATUS_MERGE_ERROR;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.github.danieladams456.statusvoter;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.concurrent.CountDownLatch;
6+
import java.util.concurrent.ExecutorService;
7+
import java.util.concurrent.Executors;
8+
import java.util.concurrent.TimeUnit;
9+
10+
import static com.github.danieladams456.statusvoter.TestData.STATUSES_IN_ORDER;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
public class ConcurrencyTest {
14+
/**
15+
* This test checks for atomic compare-and-set merges.
16+
* Set each status on a separate thread, and always expect the final result to be the most severe.
17+
*/
18+
@Test
19+
void multipleConcurrentUpdates() throws InterruptedException {
20+
final int testIterations = 10_000;
21+
int failureCount = 0; // should not need AtomicInteger
22+
23+
for (int i = 0; i < testIterations; i++) {
24+
try (ExecutorService executor = Executors.newFixedThreadPool(STATUSES_IN_ORDER.size())) {
25+
CountDownLatch startLatch = new CountDownLatch(1);
26+
CountDownLatch doneLatch = new CountDownLatch(STATUSES_IN_ORDER.size());
27+
StatusVoter voter = new StatusVoter();
28+
29+
for (StatusClassification statusClassification : STATUSES_IN_ORDER) {
30+
executor.submit(() -> {
31+
try {
32+
startLatch.await();
33+
Thread.sleep(1);
34+
voter.merge(statusClassification);
35+
} catch (InterruptedException e) {
36+
Thread.currentThread().interrupt();
37+
} finally {
38+
doneLatch.countDown();
39+
}
40+
});
41+
}
42+
43+
// single countDown() will unblock
44+
startLatch.countDown();
45+
// wait until all threads have completed their operations, then shut down thread pool.
46+
doneLatch.await();
47+
executor.shutdown();
48+
assertThat(executor.awaitTermination(100, TimeUnit.MILLISECONDS)).isTrue();
49+
50+
// result should always be the most critical status
51+
if (voter.getClassification() != StatusClassification.INTERNAL_ERROR) {
52+
failureCount++;
53+
}
54+
}
55+
}
56+
assertThat(failureCount).isEqualTo(0);
57+
}
58+
}

src/test/java/com/github/danieladams456/statusvoter/StateTransitionTest.java

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,10 @@
55
import org.junit.jupiter.params.provider.NullAndEmptySource;
66
import org.junit.jupiter.params.provider.ValueSource;
77

8-
import java.util.List;
9-
8+
import static com.github.danieladams456.statusvoter.TestData.STATUSES_IN_ORDER;
109
import static org.assertj.core.api.Assertions.assertThat;
1110

1211
public class StateTransitionTest {
13-
private static final List<StatusClassification> statusesInOrder = List.of(
14-
StatusClassification.INITIAL,
15-
StatusClassification.SUCCESS,
16-
StatusClassification.UNCLEAR_ATTRIBUTION_ERROR,
17-
StatusClassification.CUSTOMER_DATA_ERROR,
18-
StatusClassification.CUSTOMER_DATA_VALIDATION_ERROR,
19-
StatusClassification.INTERNAL_STATUS_MERGE_ERROR,
20-
StatusClassification.INTERNAL_ERROR
21-
);
2212

2313
@Test
2414
void testStatusInit() {
@@ -35,16 +25,16 @@ void testStatusInit() {
3525
*/
3626
@Test
3727
void testStatusOnlyMovesUp() {
38-
for (int i = 0; i < statusesInOrder.size(); i++)
39-
for (int j = 0; j < statusesInOrder.size(); j++)
40-
for (int k = 0; k < statusesInOrder.size(); k++) {
28+
for (int i = 0; i < STATUSES_IN_ORDER.size(); i++)
29+
for (int j = 0; j < STATUSES_IN_ORDER.size(); j++)
30+
for (int k = 0; k < STATUSES_IN_ORDER.size(); k++) {
4131
StatusVoter voter = new StatusVoter();
42-
voter.merge(statusesInOrder.get(i));
43-
voter.merge(statusesInOrder.get(j));
44-
voter.merge(statusesInOrder.get(k));
32+
voter.merge(STATUSES_IN_ORDER.get(i));
33+
voter.merge(STATUSES_IN_ORDER.get(j));
34+
voter.merge(STATUSES_IN_ORDER.get(k));
4535

4636
var maxIndex = Math.max(Math.max(i, j), k);
47-
var expectedClassification = statusesInOrder.get(maxIndex);
37+
var expectedClassification = STATUSES_IN_ORDER.get(maxIndex);
4838
assertThat(voter.getClassification()).isEqualTo(expectedClassification);
4939
assertThat(voter).hasToString(expectedClassification.name());
5040
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.github.danieladams456.statusvoter;
2+
3+
import java.util.List;
4+
5+
public class TestData {
6+
static final List<StatusClassification> STATUSES_IN_ORDER = List.of(
7+
StatusClassification.INITIAL,
8+
StatusClassification.SUCCESS,
9+
StatusClassification.UNCLEAR_ATTRIBUTION_ERROR,
10+
StatusClassification.CUSTOMER_DATA_ERROR,
11+
StatusClassification.CUSTOMER_DATA_VALIDATION_ERROR,
12+
StatusClassification.INTERNAL_STATUS_MERGE_ERROR,
13+
StatusClassification.INTERNAL_ERROR
14+
);
15+
}

0 commit comments

Comments
 (0)