Skip to content

Commit b399d14

Browse files
committed
Add comprehensive thread safety tests for data store improvements
Added extensive test coverage for the thread safety improvements made to the data store handling layer. DataStoreFactoryTest additions: - test_add_concurrentAccess: Verifies ConcurrentHashMap handles concurrent additions - test_getDataStore_concurrentWithAdd: Tests concurrent read/write operations - test_getDataStoreNames_concurrentAccess: Verifies synchronized method correctness - test_volatileFields_visibility: Ensures volatile fields are visible across threads - test_cacheRefresh_withConcurrentReads: Validates cache refresh with concurrent access - test_nullSafety_concurrentAccess: Tests null handling under concurrent load AbstractDataStoreTest additions: - test_aliveField_volatileVisibility: Verifies volatile alive field visibility - test_stop_volatileVisibility: Tests stop() method sets alive and is visible - test_stop_concurrentAccess: Validates concurrent calls to stop() - test_aliveField_concurrentReadWrite: Tests concurrent read/write of alive field - test_getName_concurrentAccess: Verifies getName() is thread-safe FileListIndexUpdateCallbackImplTest additions: - test_deleteUrlList_synchronizedAccess: Validates ArrayList with synchronized blocks - test_deleteUrlList_concurrentReads: Tests concurrent reads from deleteUrlList - test_deleteUrlList_clearOperation: Verifies clear() operation thread safety - test_deleteUrlList_iteration: Tests iteration with proper synchronization - test_deleteUrlList_isEmptyCheck: Validates isEmpty() check under concurrent access These tests verify: 1. ConcurrentHashMap correctly handles concurrent operations in DataStoreFactory 2. Volatile fields ensure proper visibility across threads 3. Synchronized methods prevent race conditions during cache refresh 4. ArrayList works correctly when all access is properly synchronized 5. No ConcurrentModificationException occurs in any concurrent scenario All tests use minimal or no sleep to maintain fast test execution time.
1 parent 97f8be4 commit b399d14

File tree

3 files changed

+877
-0
lines changed

3 files changed

+877
-0
lines changed

src/test/java/org/codelibs/fess/ds/AbstractDataStoreTest.java

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,235 @@ public void test_convertValue() {
108108
value = " ";
109109
assertNull(dataStore.convertValue(Constants.DEFAULT_SCRIPT, value, paramMap));
110110
}
111+
112+
// ========== Thread Safety Tests ==========
113+
114+
/**
115+
* Test that the volatile alive field is visible across threads.
116+
* One thread sets alive to false, other threads should see the change immediately.
117+
*/
118+
public void test_aliveField_volatileVisibility() throws Exception {
119+
// Ensure alive starts as true
120+
assertTrue(dataStore.alive);
121+
122+
final int readerThreadCount = 10;
123+
final Thread[] readerThreads = new Thread[readerThreadCount];
124+
final boolean[][] observations = new boolean[readerThreadCount][100];
125+
126+
// Start reader threads that continuously check alive field
127+
for (int i = 0; i < readerThreadCount; i++) {
128+
final int threadIndex = i;
129+
readerThreads[i] = new Thread(() -> {
130+
for (int j = 0; j < 100; j++) {
131+
observations[threadIndex][j] = dataStore.alive;
132+
// Small yield to allow context switching
133+
Thread.yield();
134+
}
135+
});
136+
readerThreads[i].start();
137+
}
138+
139+
// Writer thread sets alive to false
140+
Thread writerThread = new Thread(() -> {
141+
dataStore.alive = false;
142+
});
143+
writerThread.start();
144+
writerThread.join();
145+
146+
// Wait for all reader threads to complete
147+
for (Thread thread : readerThreads) {
148+
thread.join();
149+
}
150+
151+
// Verify that alive was changed to false
152+
assertFalse("alive should be false after writer thread", dataStore.alive);
153+
154+
// At least some observations from reader threads should have seen false
155+
// due to volatile ensuring visibility
156+
int falseCount = 0;
157+
for (int i = 0; i < readerThreadCount; i++) {
158+
for (int j = 0; j < 100; j++) {
159+
if (!observations[i][j]) {
160+
falseCount++;
161+
}
162+
}
163+
}
164+
assertTrue("Some threads should have observed alive=false", falseCount > 0);
165+
}
166+
167+
/**
168+
* Test stop() method sets alive to false and is visible to other threads.
169+
*/
170+
public void test_stop_volatileVisibility() throws Exception {
171+
assertTrue(dataStore.alive);
172+
173+
final boolean[] observedValues = new boolean[10];
174+
final Thread[] threads = new Thread[10];
175+
176+
// Call stop() in main thread
177+
dataStore.stop();
178+
179+
// Multiple threads read the alive field
180+
for (int i = 0; i < 10; i++) {
181+
final int index = i;
182+
threads[i] = new Thread(() -> {
183+
observedValues[index] = dataStore.alive;
184+
});
185+
}
186+
187+
for (Thread thread : threads) {
188+
thread.start();
189+
}
190+
191+
for (Thread thread : threads) {
192+
thread.join();
193+
}
194+
195+
// All threads should observe alive=false due to volatile
196+
for (int i = 0; i < 10; i++) {
197+
assertFalse("Thread " + i + " should see alive=false", observedValues[i]);
198+
}
199+
}
200+
201+
/**
202+
* Test concurrent access to stop() method.
203+
* Multiple threads call stop() simultaneously - should be safe.
204+
*/
205+
public void test_stop_concurrentAccess() throws Exception {
206+
final int threadCount = 10;
207+
final Thread[] threads = new Thread[threadCount];
208+
final Exception[] exceptions = new Exception[threadCount];
209+
210+
for (int i = 0; i < threadCount; i++) {
211+
final int index = i;
212+
threads[i] = new Thread(() -> {
213+
try {
214+
dataStore.stop();
215+
} catch (Exception e) {
216+
exceptions[index] = e;
217+
}
218+
});
219+
}
220+
221+
// Start all threads
222+
for (Thread thread : threads) {
223+
thread.start();
224+
}
225+
226+
// Wait for all threads to complete
227+
for (Thread thread : threads) {
228+
thread.join();
229+
}
230+
231+
// Verify no exceptions occurred
232+
for (int i = 0; i < threadCount; i++) {
233+
assertNull("Thread " + i + " threw exception", exceptions[i]);
234+
}
235+
236+
// Verify alive is false
237+
assertFalse("alive should be false after all stop() calls", dataStore.alive);
238+
}
239+
240+
/**
241+
* Test that multiple threads can safely read alive field while one writes.
242+
*/
243+
public void test_aliveField_concurrentReadWrite() throws Exception {
244+
dataStore.alive = true;
245+
246+
final int readerCount = 5;
247+
final int iterations = 1000;
248+
final Thread[] readers = new Thread[readerCount];
249+
final Exception[] exceptions = new Exception[readerCount + 1];
250+
final int[] trueCount = new int[readerCount];
251+
final int[] falseCount = new int[readerCount];
252+
253+
// Start reader threads
254+
for (int i = 0; i < readerCount; i++) {
255+
final int index = i;
256+
readers[i] = new Thread(() -> {
257+
try {
258+
for (int j = 0; j < iterations; j++) {
259+
if (dataStore.alive) {
260+
trueCount[index]++;
261+
} else {
262+
falseCount[index]++;
263+
}
264+
}
265+
} catch (Exception e) {
266+
exceptions[index] = e;
267+
}
268+
});
269+
}
270+
271+
// Start writer thread that toggles alive
272+
Thread writer = new Thread(() -> {
273+
try {
274+
for (int i = 0; i < 100; i++) {
275+
dataStore.alive = !dataStore.alive;
276+
Thread.yield();
277+
}
278+
} catch (Exception e) {
279+
exceptions[readerCount] = e;
280+
}
281+
});
282+
283+
// Start all threads
284+
for (Thread reader : readers) {
285+
reader.start();
286+
}
287+
writer.start();
288+
289+
// Wait for completion
290+
for (Thread reader : readers) {
291+
reader.join();
292+
}
293+
writer.join();
294+
295+
// Verify no exceptions
296+
for (int i = 0; i <= readerCount; i++) {
297+
assertNull("Thread " + i + " threw exception", exceptions[i]);
298+
}
299+
300+
// Verify all readers completed all iterations
301+
for (int i = 0; i < readerCount; i++) {
302+
assertEquals("Reader " + i + " should complete all iterations", iterations, trueCount[i] + falseCount[i]);
303+
}
304+
}
305+
306+
/**
307+
* Test getName() method can be called concurrently without issues.
308+
*/
309+
public void test_getName_concurrentAccess() throws Exception {
310+
final int threadCount = 10;
311+
final Thread[] threads = new Thread[threadCount];
312+
final String[] results = new String[threadCount];
313+
final Exception[] exceptions = new Exception[threadCount];
314+
315+
for (int i = 0; i < threadCount; i++) {
316+
final int index = i;
317+
threads[i] = new Thread(() -> {
318+
try {
319+
for (int j = 0; j < 100; j++) {
320+
results[index] = dataStore.getName();
321+
}
322+
} catch (Exception e) {
323+
exceptions[index] = e;
324+
}
325+
});
326+
}
327+
328+
for (Thread thread : threads) {
329+
thread.start();
330+
}
331+
332+
for (Thread thread : threads) {
333+
thread.join();
334+
}
335+
336+
// Verify no exceptions
337+
for (int i = 0; i < threadCount; i++) {
338+
assertNull("Thread " + i + " threw exception", exceptions[i]);
339+
assertEquals("Thread " + i + " got wrong name", "Test", results[i]);
340+
}
341+
}
111342
}

0 commit comments

Comments
 (0)