Skip to content

[fix](fe) Fix deadlock risks in ConcurrentLong2*HashMap#62117

Open
dataroaring wants to merge 3 commits intoapache:masterfrom
dataroaring:fix/concurrent-map-deadlock-safety
Open

[fix](fe) Fix deadlock risks in ConcurrentLong2*HashMap#62117
dataroaring wants to merge 3 commits intoapache:masterfrom
dataroaring:fix/concurrent-map-deadlock-safety

Conversation

@dataroaring
Copy link
Copy Markdown
Contributor

Summary

  • forEach deadlock: Changed forEach in both ConcurrentLong2ObjectHashMap and ConcurrentLong2LongHashMap from inline iteration under read-lock to snapshot-based iteration. Entries are copied into arrays under the read lock, then callbacks are invoked outside the lock. This eliminates the read-to-write upgrade deadlock (e.g., forEach((k, v) -> map.put(k, v + 1))).
  • Reentrant callback guard: Added a ThreadLocal<Boolean> per map instance that is set during compute/merge callback execution. All write operations (put, remove, putIfAbsent, replace, clear, addTo, and all compute/merge variants) check this flag and throw IllegalStateException on reentrant access, turning a silent deadlock into a fail-fast error.
  • Documentation: Added Javadoc on both classes documenting the callback restriction (matching ConcurrentHashMap's contract).

Test plan

  • testForEachWithMutatingCallbackDoesNotDeadlock — verifies forEach + put no longer deadlocks (uses 5s timeout)
  • testReentrantComputeThrowsIllegalStateException — verifies compute callback calling put throws ISE
  • testReentrantComputeIfAbsentThrowsIllegalStateException — verifies computeIfAbsent callback calling put throws ISE
  • testReentrantMergeThrowsIllegalStateException / testReentrantMergeLongThrowsIllegalStateException — verifies merge callback calling remove throws ISE
  • All 64 existing tests pass (33 Long2Long + 31 Long2Object)

🤖 Generated with Claude Code

…urrentLong2LongHashMap

Fix two classes of deadlock in the segmented concurrent maps:

1. forEach read-to-write upgrade deadlock: forEach held a read lock while
   invoking user callbacks, which could attempt write operations on the same
   segment. ReentrantReadWriteLock does not support read-to-write upgrade,
   causing the thread to park forever. Fixed by snapshot-copying entries
   under the read lock and invoking callbacks outside the lock.

2. Reentrant callback deadlock: compute/merge methods invoked user-supplied
   functions while holding segment write locks, enabling both same-thread
   reentrant deadlock and cross-thread lock-order inversion. Added a
   ThreadLocal guard that detects reentrant map access from within callbacks
   and throws IllegalStateException (fail-fast instead of silent deadlock).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 4, 2026 01:45
@hello-stephen
Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to eliminate deadlock scenarios in the FE segmented concurrent primitive maps (ConcurrentLong2*HashMap) by changing iteration semantics and adding a fail-fast reentrancy guard around compute/merge callbacks, plus tests and Javadoc to codify the new constraints.

Changes:

  • Updated forEach to snapshot entries under read lock and invoke callbacks outside the lock (per segment) to avoid read→write upgrade deadlocks.
  • Added a per-map ThreadLocal callback guard and enforced it across write APIs (and compute/merge variants) to fail fast on reentrant mutation.
  • Added unit tests covering the previous deadlock pattern and the new reentrant-callback exceptions.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
fe/fe-foundation/src/main/java/org/apache/doris/foundation/util/ConcurrentLong2ObjectHashMap.java Snapshot-based forEach, callback restriction Javadoc, and reentrancy guard for write + compute/merge paths
fe/fe-foundation/src/main/java/org/apache/doris/foundation/util/ConcurrentLong2LongHashMap.java Same as above for primitive long→long map (including mergeLong)
fe/fe-foundation/src/test/java/org/apache/doris/foundation/util/ConcurrentLong2ObjectHashMapTest.java Added deadlock-safety and reentrant-callback tests
fe/fe-foundation/src/test/java/org/apache/doris/foundation/util/ConcurrentLong2LongHashMapTest.java Added deadlock-safety and reentrant-callback tests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +407 to +414
Future<?> future = executor.submit(() -> {
map.forEach((ConcurrentLong2ObjectHashMap.LongObjConsumer<String>) (k, v) -> {
map.put(k + 1000L, v + "-copy");
});
});
// Should complete within 5 seconds; if it deadlocks, get() will time out
future.get(5, java.util.concurrent.TimeUnit.SECONDS);
executor.shutdown();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executor is only shut down on the success path. If future.get(...) times out or throws, the single-thread executor can remain alive and keep the test JVM from exiting. Wrap the submit/get in try/finally, cancel the Future on failure, and use shutdownNow() (optionally awaitTermination) to ensure cleanup.

Suggested change
Future<?> future = executor.submit(() -> {
map.forEach((ConcurrentLong2ObjectHashMap.LongObjConsumer<String>) (k, v) -> {
map.put(k + 1000L, v + "-copy");
});
});
// Should complete within 5 seconds; if it deadlocks, get() will time out
future.get(5, java.util.concurrent.TimeUnit.SECONDS);
executor.shutdown();
Future<?> future = null;
try {
future = executor.submit(() -> {
map.forEach((ConcurrentLong2ObjectHashMap.LongObjConsumer<String>) (k, v) -> {
map.put(k + 1000L, v + "-copy");
});
});
// Should complete within 5 seconds; if it deadlocks, get() will time out
future.get(5, java.util.concurrent.TimeUnit.SECONDS);
} finally {
if (future != null && !future.isDone()) {
future.cancel(true);
}
executor.shutdownNow();
executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 7f590a5. The test now uses try/catch/finally: on timeout or exception the Future is cancelled, and executor.shutdownNow() is called in the finally block to prevent thread leaks.

— ThinkOps 🤖

Comment on lines +418 to +419
future.get(5, java.util.concurrent.TimeUnit.SECONDS);
executor.shutdown();
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executor is shut down only after future.get(...) completes successfully. If the get times out or throws, the executor thread may leak and interfere with the rest of the test suite. Use try/finally, cancel the future on failure, and call shutdownNow() (and optionally awaitTermination).

Suggested change
future.get(5, java.util.concurrent.TimeUnit.SECONDS);
executor.shutdown();
try {
future.get(5, java.util.concurrent.TimeUnit.SECONDS);
} catch (Exception e) {
future.cancel(true);
throw e;
} finally {
executor.shutdownNow();
executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 7f590a5. Same fix applied: try/catch/finally with future.cancel(true) on exception and executor.shutdownNow() in finally.

— ThinkOps 🤖

Comment on lines +327 to 336
inCallback.set(Boolean.TRUE);
try {
V newValue = mappingFunction.apply(key);
if (newValue != null) {
seg.map.put(key, newValue);
}
return newValue;
} finally {
inCallback.set(Boolean.FALSE);
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ThreadLocal and setting it back to Boolean.FALSE can still leave an entry in long-lived thread pools (and may delay cleanup after the map is GC'd). Consider using inCallback.remove() in the finally block instead of set(Boolean.FALSE) so the per-thread value is cleared entirely when leaving the callback.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 7f590a5. Changed from inCallback.set(Boolean.FALSE) to inCallback.remove() in all finally blocks to fully clear the ThreadLocal entry.

— ThinkOps 🤖

Comment on lines +336 to +342
inCallback.set(Boolean.TRUE);
long newValue;
try {
newValue = mappingFunction.applyAsLong(key);
} finally {
inCallback.set(Boolean.FALSE);
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThreadLocal values set back to Boolean.FALSE still leave an entry in the thread's ThreadLocalMap (which can be undesirable with long-lived thread pools). Consider calling inCallback.remove() in the finally block instead of set(Boolean.FALSE) to fully clear the thread-local state on exit.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 7f590a5. All set(Boolean.FALSE) calls replaced with inCallback.remove() in both map implementations.

— ThinkOps 🤖

Comment on lines +57 to +60
* {@code computeIfPresent}, {@code compute}, and {@code merge} <em>must not</em> attempt to update
* any other mappings of this map. This restriction is enforced at runtime: reentrant access from a
* callback throws {@link IllegalStateException}. Violation may also cause deadlock if callbacks
* attempt cross-segment updates from multiple threads.
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reentrancy guard only blocks writes from compute/merge callbacks. Callbacks can still call get/containsKey/... on other keys, which can deadlock across segments (e.g., T1 holds segA write-lock then reads segB; T2 holds segB write-lock then reads segA). If the intent is to fail-fast instead of risking deadlock, consider also checking inCallback in read methods (or explicitly documenting that callbacks must not access the map at all, even for reads).

Suggested change
* {@code computeIfPresent}, {@code compute}, and {@code merge} <em>must not</em> attempt to update
* any other mappings of this map. This restriction is enforced at runtime: reentrant access from a
* callback throws {@link IllegalStateException}. Violation may also cause deadlock if callbacks
* attempt cross-segment updates from multiple threads.
* {@code computeIfPresent}, {@code compute}, and {@code merge} <em>must not</em> access this map
* reentrantly in any way, including both updates and reads (for example {@code get},
* {@code containsKey}, or any other operation on another key). Runtime checks reject reentrant
* mutating access from a callback by throwing {@link IllegalStateException}; however, even
* read-only cross-segment access from callbacks can deadlock when multiple threads hold locks on
* different segments. Callbacks must therefore treat this map as off-limits for the duration of
* the callback.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 64db7d2. checkNotInCallback() is now called at the top of all read methods too: get, getOrDefault, containsKey, containsValue, size, and isEmpty. A dedicated test testReentrantReadFromCallbackThrowsIllegalStateException verifies that get() from within a compute callback throws ISE.

— ThinkOps 🤖

Comment on lines +58 to +60
* attempt to update any other mappings of this map. This restriction is enforced at runtime:
* reentrant access from a callback throws {@link IllegalStateException}. Violation may also cause
* deadlock if callbacks attempt cross-segment updates from multiple threads.
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callback guard only prevents reentrant writes. A compute/merge callback can still call read APIs (get, containsKey, etc.) for other keys, which can deadlock between segments when two threads each hold a segment write-lock and try to read the other segment. To truly turn these deadlocks into fail-fast errors, consider guarding read methods too (or strengthen the documented restriction to forbid any map access from callbacks, not just updates).

Suggested change
* attempt to update any other mappings of this map. This restriction is enforced at runtime:
* reentrant access from a callback throws {@link IllegalStateException}. Violation may also cause
* deadlock if callbacks attempt cross-segment updates from multiple threads.
* access this map in any way while executing, including reads such as {@code get},
* {@code containsKey}, or any attempt to update other mappings. Reentrant callback access is
* rejected with {@link IllegalStateException}; any map access from a callback is unsupported
* because cross-segment reads or writes from multiple threads may deadlock.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 64db7d2. Same fix in the Long2Long map: checkNotInCallback() added to all read methods (get, getOrDefault, containsKey, containsValue, size, isEmpty). Test testReentrantReadFromCallbackThrowsIllegalStateException covers this.

— ThinkOps 🤖

gavinchou
gavinchou previously approved these changes Apr 4, 2026
Copy link
Copy Markdown
Contributor

@gavinchou gavinchou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions github-actions bot added the approved Indicates a PR has been approved by one committer. label Apr 4, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

PR approved by at least one committer and no changes requested.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

PR approved by anyone and no changes requested.

- Use inCallback.remove() instead of set(FALSE) to avoid stale
  ThreadLocalMap entries in long-lived thread pools
- Wrap test executor in try/finally with shutdownNow() to prevent
  thread leak on timeout/failure
- Strengthen Javadoc: callbacks must not access the map at all (including
  reads), since cross-segment reads can also deadlock across threads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions github-actions bot removed the approved Indicates a PR has been approved by one committer. label Apr 4, 2026
@dataroaring
Copy link
Copy Markdown
Contributor Author

run buildall

@doris-robot
Copy link
Copy Markdown

TPC-H: Total hot run time: 29038 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 7f590a5392ac99471552f870f532e39c659d0c81, data reload: false

------ Round 1 ----------------------------------
orders	Doris	NULL	NULL	0	0	0	NULL	0	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	17641	3751	3694	3694
q2	q3	10706	872	610	610
q4	4666	466	353	353
q5	7441	1335	1120	1120
q6	182	173	138	138
q7	909	939	768	768
q8	9291	1405	1339	1339
q9	5458	5301	5234	5234
q10	6315	2025	1782	1782
q11	484	284	275	275
q12	637	406	296	296
q13	18086	2800	2166	2166
q14	286	286	257	257
q15	q16	896	841	791	791
q17	1025	1075	875	875
q18	6369	5677	5554	5554
q19	1177	1266	1063	1063
q20	577	424	289	289
q21	4932	2444	2088	2088
q22	509	426	346	346
Total cold run time: 97587 ms
Total hot run time: 29038 ms

----- Round 2, with runtime_filter_mode=off -----
orders	Doris	NULL	NULL	150000000	42	6422171781	NULL	22778155	NULL	NULL	2023-12-26 18:27:23	2023-12-26 18:42:55	NULL	utf-8	NULL	NULL	
============================================
q1	4574	4469	4421	4421
q2	q3	4577	4776	4179	4179
q4	2095	2156	1326	1326
q5	4959	5002	5193	5002
q6	205	167	131	131
q7	2029	1737	1643	1643
q8	3281	3030	3107	3030
q9	8281	8380	8202	8202
q10	4404	4443	4208	4208
q11	592	402	381	381
q12	671	744	511	511
q13	2780	3086	2303	2303
q14	321	324	277	277
q15	q16	754	780	684	684
q17	1302	1398	1234	1234
q18	7939	7042	6964	6964
q19	1196	1215	1166	1166
q20	2202	2187	1956	1956
q21	6114	5283	4807	4807
q22	556	506	435	435
Total cold run time: 58832 ms
Total hot run time: 52860 ms

@doris-robot
Copy link
Copy Markdown

TPC-DS: Total hot run time: 177967 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 7f590a5392ac99471552f870f532e39c659d0c81, data reload: false

query5	4327	659	523	523
query6	331	236	210	210
query7	4247	607	343	343
query8	338	256	238	238
query9	8713	3892	3949	3892
query10	455	362	311	311
query11	6657	5479	5134	5134
query12	184	129	127	127
query13	1311	662	440	440
query14	5632	5168	4738	4738
query14_1	4144	4125	4124	4124
query15	215	206	181	181
query16	1028	456	436	436
query17	1155	775	679	679
query18	2729	479	376	376
query19	235	211	176	176
query20	142	130	132	130
query21	235	145	120	120
query22	13839	14862	14309	14309
query23	18047	17149	16685	16685
query23_1	16959	16863	16684	16684
query24	8203	1831	1389	1389
query24_1	1374	1357	1385	1357
query25	656	495	452	452
query26	1283	339	188	188
query27	2651	615	404	404
query28	4443	1898	1914	1898
query29	974	677	552	552
query30	302	235	193	193
query31	1086	1035	926	926
query32	93	70	70	70
query33	531	346	286	286
query34	1176	1184	673	673
query35	741	762	659	659
query36	1220	1183	1029	1029
query37	158	98	84	84
query38	3070	3029	2977	2977
query39	908	889	848	848
query39_1	830	838	831	831
query40	232	148	133	133
query41	63	59	59	59
query42	108	106	108	106
query43	316	314	272	272
query44	
query45	199	194	191	191
query46	1143	1257	780	780
query47	2348	2317	2255	2255
query48	422	400	316	316
query49	687	523	419	419
query50	775	282	220	220
query51	4423	4349	4200	4200
query52	109	109	99	99
query53	250	270	196	196
query54	327	265	261	261
query55	100	92	88	88
query56	310	316	310	310
query57	1723	1676	1745	1676
query58	321	280	280	280
query59	2891	2949	2721	2721
query60	328	339	324	324
query61	156	153	157	153
query62	691	625	573	573
query63	247	197	194	194
query64	5239	1363	974	974
query65	
query66	1404	476	363	363
query67	24299	24205	24138	24138
query68	
query69	448	344	313	313
query70	998	1014	970	970
query71	323	277	274	274
query72	2948	2689	2393	2393
query73	826	756	477	477
query74	9846	9738	9526	9526
query75	2733	2619	2289	2289
query76	2304	1118	751	751
query77	396	403	346	346
query78	11202	11441	10728	10728
query79	1511	1058	783	783
query80	1253	565	508	508
query81	499	288	241	241
query82	1319	157	125	125
query83	337	295	269	269
query84	257	149	125	125
query85	966	491	439	439
query86	436	310	312	310
query87	3259	3180	3087	3087
query88	3618	2712	2698	2698
query89	439	384	358	358
query90	1931	179	176	176
query91	177	168	169	168
query92	80	76	73	73
query93	947	950	554	554
query94	632	319	301	301
query95	672	452	330	330
query96	1040	792	336	336
query97	2679	2650	2566	2566
query98	236	224	221	221
query99	1059	1055	937	937
Total cold run time: 258269 ms
Total hot run time: 177967 ms

@dataroaring
Copy link
Copy Markdown
Contributor Author

/review

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 1 issue.

  1. fe/fe-foundation/src/main/java/org/apache/doris/foundation/util/ConcurrentLong2LongHashMap.java and fe/fe-foundation/src/main/java/org/apache/doris/foundation/util/ConcurrentLong2ObjectHashMap.java: the new callback guard only blocks write APIs. Read APIs (get, containsKey, size, keySet, values, etc.) are still callable from inside compute/merge callbacks even though the new contract says callbacks must not access the map at all. This leaves an ABBA deadlock: thread T1 can enter compute(kA, ...), hold segment A's write lock, then call get(kB) and wait for segment B's read lock while thread T2 does the symmetric compute(kB, ...) -> get(kA) path. Both threads then wait forever.

Critical checkpoint conclusions

  • Goal of task / proof: Partially achieved. The forEach upgrade deadlock is fixed and tests cover that, but callback-triggered deadlocks are not eliminated because callback reads remain allowed. No test covers this scenario.
  • Minimality/focus: Yes. The patch is focused on the two map implementations and their tests.
  • Concurrency review: This code is concurrency-sensitive. Segment ReentrantReadWriteLocks and inCallback are the critical state. Running callbacks under segment write locks is still unsafe when callback read methods can acquire other segment locks; lock ordering across segments remains inconsistent.
  • Lifecycle/static init: No special lifecycle issue beyond the per-instance ThreadLocal; switching to remove() is correct.
  • Config changes: None.
  • Compatibility: None.
  • Parallel code paths: Yes. The same incomplete guard is present in both concurrent map classes.
  • Special conditional checks: checkNotInCallback() is reasonable, but its enforcement is narrower than the documented contract.
  • Test coverage: Positive tests were added for mutating forEach and write reentrancy. Missing negative coverage for callback read access across segments.
  • Observability: No extra observability appears necessary for this utility class.
  • Transaction/persistence: Not applicable.
  • Data write/atomicity: Segment-local atomicity is preserved, but deadlock remains possible for cross-segment callback reads.
  • FE/BE variable passing: Not applicable.
  • Performance: Snapshot-based forEach adds allocations, but that tradeoff looks acceptable for correctness.
  • Other issues: None beyond the deadlock hole above.

Not run locally in this review runner.

* <p><b>Callback restriction:</b> The mapping/remapping functions passed to {@code computeIfAbsent},
* {@code computeIfPresent}, {@code compute}, {@code merge}, and {@code mergeLong} <em>must not</em>
* access this map in any way while executing, including reads such as {@code get} or
* {@code containsKey}, as well as writes. Reentrant write access from a callback is rejected at
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still leaves the deadlock you describe in the new Javadoc. checkNotInCallback() is only wired into mutating methods, so a callback can still call get() / containsKey() / size() / snapshot iterators while the current thread already holds one segment's write lock. With two threads running compute on different segments and each callback reading the other key, you get an ABBA write(A) -> read(B) / write(B) -> read(A) deadlock. Since the contract now says callbacks must not access the map at all, the runtime guard and tests need to cover read paths too, not just writes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 64db7d2. checkNotInCallback() now guards all read methods (get, getOrDefault, containsKey, containsValue, size, isEmpty) in addition to write methods. Any map access from within a compute/merge callback — read or write — now throws IllegalStateException, fully closing the ABBA cross-segment deadlock window. Test coverage added via testReentrantReadFromCallbackThrowsIllegalStateException.

— ThinkOps 🤖

* <p><b>Callback restriction:</b> The mapping/remapping functions passed to {@code computeIfAbsent},
* {@code computeIfPresent}, {@code compute}, and {@code merge} <em>must not</em> access this map
* in any way while executing, including reads such as {@code get} or {@code containsKey}, as well
* as writes. Reentrant write access from a callback is rejected at runtime with
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here: the new ThreadLocal guard only protects write APIs. Read-only access from inside compute* / merge callbacks is still allowed, even though the class Javadoc now says callbacks must not access the map in any way. Two threads doing cross-segment callback reads can still deadlock while each holds a segment write lock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 64db7d2. Same fix applied here: the ThreadLocal guard now covers both read and write APIs. All six read methods (get, getOrDefault, containsKey, containsValue, size, isEmpty) call checkNotInCallback() at entry. The documented contract (callbacks must not access the map in any way) is now enforced at runtime for all code paths.

— ThinkOps 🤖

…dlock

Add checkNotInCallback() to all read methods (get, getOrDefault,
containsKey, containsValue, size, isEmpty) so that compute/merge
callbacks cannot acquire read-locks on other segments, preventing
ABBA deadlocks when two threads each hold a segment write-lock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dataroaring
Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen
Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants