Skip to content

Double check concurrent logic into simplified one liner, more safe and more performant #59

Merged
NeatGuyCoding merged 8 commits intomainfrom
concurrent-performance-bug
Nov 30, 2025
Merged

Double check concurrent logic into simplified one liner, more safe and more performant #59
NeatGuyCoding merged 8 commits intomainfrom
concurrent-performance-bug

Conversation

@sanjomo
Copy link
Member

@sanjomo sanjomo commented Nov 26, 2025

Description

Double check concurrent into simplified one liner, more safe and more performant
added proper shutdown logic of subscribers

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Test improvements
  • Build/tooling changes

Related Issue

Closes #(issue number)

Changes Made

Testing

  • All existing tests pass
  • New tests added for new functionality
  • Tests pass locally with mvn test
  • Integration tests pass (if applicable)

Checklist

  • Code follows project coding standards
  • Self-review completed
  • Code is commented where necessary
  • Documentation updated (if needed)
  • Commit messages follow conventional format
  • No merge conflicts
  • All CI checks pass

Additional Notes

Any additional information, screenshots, or context that reviewers should know.

Summary by CodeRabbit

Release Notes

  • New Features

    • Expanded store factory APIs with additional constructor options for flexible initialization.
    • Added generic map creation capability to store factories.
  • Bug Fixes

    • Improved error handling and logging in pub/sub message operations.
    • Enhanced resource cleanup during shutdown with exception-safe handling.
    • Added null-safety validation for store initialization.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

This change introduces logging infrastructure, adds null-safety validations across storage implementations, expands constructor overloads in factory classes for improved flexibility, and refactors shutdown sequences with ordered cleanup and exception handling in Hazelcast and Redisson store components.

Changes

Cohort / File(s) Summary
Logging & Null-Safety Infrastructure
HazelcastPubSubStore.java, RedissonPubSubStore.java, HazelcastStoreFactory.java, RedissonStoreFactory.java
Added SLF4J Logger initialization and Objects.requireNonNull validation checks across all store and factory classes for defensive programming.
PubSub Store Listener Management
HazelcastPubSubStore.java, RedissonPubSubStore.java
Refactored subscribe/unsubscribe logic to use computeIfAbsent() for cleaner listener registration tracking; wrapped removeListener calls in try-catch with failure logging.
Hazelcast Store Factory Lifecycle
HazelcastStoreFactory.java
Added new constructor overload accepting separate Hazelcast instances and PubSubStore; introduced createMap() public method; restructured shutdown to use ordered cleanup via LinkedHashSet with per-component logging.
Redisson Store Factory Lifecycle
RedissonStoreFactory.java
Added two new constructor overloads accepting RedissonClient and RedissonPubSubStore parameters; reworked shutdown sequence with ordered client cleanup (sub, pub, client) and pubSubStore shutdown invocation.
Enhanced Shutdown Behavior
HazelcastPubSubStore.java, RedissonPubSubStore.java
Both PubSub stores now unsubscribe all PubSubType values, clear internal maps, and avoid shutting down underlying clients during shutdown.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Areas requiring attention:

  • Shutdown sequence correctness in both factory classes—verify order of operations doesn't introduce race conditions or resource leaks
  • Constructor overload compatibility and null-check behavior across Hazelcast and Redisson implementations
  • Listener registration tracking via computeIfAbsent() and its interaction with unsubscribe logic in both PubSub stores

Suggested labels

enhancement

Poem

🐰 With logging trails and safety bounds,
Our stores now guard their precious grounds,
New factories dance in ordered grace,
Null-checks embrace each listening place,
The cleanup code hops swift and sure—
Pure socketio magic, safe and pure!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive The PR description identifies the refactoring type and mentions shutdown logic improvements, but the 'Changes Made' section is empty and lacks detailed explanation of file-level modifications. Complete the 'Changes Made' section by listing key modifications (e.g., HazelcastPubSubStore shutdown logic, PubSubStore registration tracking via computeIfAbsent, new factory constructors) to clarify the scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main refactoring objective: simplifying concurrent logic and improving safety/performance, which aligns with the summary of changes across all modified store files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch concurrent-performance-bug

Comment @coderabbitai help to get the list of available commands and usage tips.

@sanjomo sanjomo changed the title simplify Double check concurrent logic into simplified one liner, more safe and more performing Nov 26, 2025
@sanjomo
Copy link
Member Author

sanjomo commented Nov 27, 2025

@NeatGuyCoding please review this

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonPubSubStore.java (1)

67-74: Guard against null registrations in unsubscribe to avoid NPE

If unsubscribe(type) is called when there are no registrations for that PubSubType (or called twice), map.remove(name) returns null and the for (Integer id : regIds) loop will throw a NullPointerException.

A small guard keeps this safe without changing normal behavior:

     @Override
     public void unsubscribe(PubSubType type) {
         String name = type.toString();
-        Queue<Integer> regIds = map.remove(name);
-        RTopic topic = redissonSub.getTopic(name);
-        for (Integer id : regIds) {
-            topic.removeListener(id);
-        }
+        Queue<Integer> regIds = map.remove(name);
+        if (regIds == null) {
+            return;
+        }
+        RTopic topic = redissonSub.getTopic(name);
+        for (Integer id : regIds) {
+            topic.removeListener(id);
+        }
     }

This also avoids touching Redisson when there is nothing to unsubscribe.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afc75ab and 20ba62f.

📒 Files selected for processing (1)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonPubSubStore.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build (17) / build
  • GitHub Check: build (25) / build
  • GitHub Check: build (21) / build
  • GitHub Check: Analyze (java-kotlin)
  • GitHub Check: Analyze (java-kotlin)

@sanjomo sanjomo changed the title Double check concurrent logic into simplified one liner, more safe and more performing Double check concurrent logic into simplified one liner, more safe and more performant Nov 28, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastPubSubStore.java (1)

70-80: Consider aligning Hazelcast unsubscribe error handling with Redisson

unsubscribe() currently assumes topic.removeMessageListener(id) will always succeed. In the unlikely case Hazelcast throws, a single failure would abort the loop and can leak remaining listeners.

You could mirror the defensive pattern used in RedissonPubSubStore.unsubscribe() by wrapping removeMessageListener in a try/catch and logging failures, so all IDs are attempted:

-        ITopic<Object> topic = hazelcastSub.getTopic(name);
-        for (UUID id : regIds) {
-            topic.removeMessageListener(id);
-        }
+        ITopic<Object> topic = hazelcastSub.getTopic(name);
+        for (UUID id : regIds) {
+            try {
+                topic.removeMessageListener(id);
+            } catch (Exception e) {
+                // keep going even if one removal fails
+                // log.warn("Failed to remove listener {} from topic {}", id, name, e);
+            }
+        }

(Adjust logging as desired.)

netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastStoreFactory.java (1)

95-115: Ordered, deduplicated Hazelcast shutdown is an improvement

Using a LinkedHashSet to add hazelcastSub, then hazelcastPub, then hazelcastClient, and looping with per‑instance try/catch:

  • Avoids double shutdown when the same HazelcastInstance is reused for multiple roles.
  • Ensures a stable, documented shutdown order.
  • Logs both successes and failures instead of failing hard on the first exception.

If you ever need HazelcastPubSubStore.shutdown() semantics as well (topic unsubscribe + internal map clear), you could optionally call it from here before shutting instances down, but the current behavior is already safe and consistent with the Redisson side.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15c2dc0 and 651142a.

📒 Files selected for processing (5)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOServer.java (1 hunks)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastPubSubStore.java (3 hunks)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastStoreFactory.java (3 hunks)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonPubSubStore.java (3 hunks)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonStoreFactory.java (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonStoreFactory.java (1)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/pubsub/BaseStoreFactory.java (1)
  • BaseStoreFactory (32-161)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastStoreFactory.java (1)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/pubsub/BaseStoreFactory.java (1)
  • BaseStoreFactory (32-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build (25) / build
  • GitHub Check: build (21) / build
  • GitHub Check: build (17) / build
  • GitHub Check: Analyze (java-kotlin)
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (9)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastPubSubStore.java (2)

55-67: Subscribe lambda + computeIfAbsent look correct and thread‑safe

Using ITopic<T> with a nodeId filter and map.computeIfAbsent(...).add(regId) removes the previous racy “get or create” pattern and safely tracks listener registrations per PubSubType. This is aligned with the PR’s goal (simpler and safer concurrent logic).


82-87: Shutdown behavior is clean and non‑destructive

Unsubscribing all PubSubType values and then clearing map ensures internal listener state is cleaned up without touching the underlying HazelcastInstance, which is appropriate given the factory now owns instance shutdown.

netty-socketio-core/src/main/java/com/socketio4j/socketio/SocketIOServer.java (1)

331-333: Good: server now tears down the configured store factory on stop

Calling configCopy.getStoreFactory().shutdown() here completes the lifecycle for Hazelcast/Redisson factories added in this PR and avoids leaking distributed resources when the server stops. The null‑check keeps the behavior safe when no store factory is configured.

netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastStoreFactory.java (1)

40-88: Constructors and logging improvements are sensible and safer

  • Adding a dedicated Logger and using Objects.requireNonNull(...) in all constructors gives clearer diagnostics for misconfigured Hazelcast instances.
  • The new 4‑arg constructor that accepts an explicit HazelcastPubSubStore provides a clean injection point for custom pub/sub wiring while still preserving the default wiring in the other constructors.

All fields remain final and are always initialized along every constructor path, so the class stays immutable after construction.

netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonStoreFactory.java (2)

37-97: Constructor overloads and null‑checks clean up configuration paths

  • Adding a static Logger and Objects.requireNonNull in each constructor makes misconfiguration much easier to spot.
  • The new overloads—RedissonStoreFactory(RedissonClient, RedissonPubSubStore) and the 3/4‑arg Redisson variants—give flexibility for custom client/pub/sub wiring and custom pub/sub stores without sacrificing the simple default Redisson.create() path.
  • pubSubStore remains final and non‑null for all constructor paths, preserving invariants.

This is a good balance between configurability and safety.


109-129: Ordered Redisson client shutdown is robust and idempotent

The new shutdown() implementation:

  • Uses a LinkedHashSet with redisSub, redisPub, then redisClient to define a clear shutdown order and avoid duplicate shutdown when instances are reused.
  • Guards each shutdown() with a null‑check and try/catch, logging both success and failure, so one bad client doesn’t prevent others from stopping.

This is a clear upgrade over direct, unguarded shutdown calls.

netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonPubSubStore.java (3)

45-64: Constructors now properly validate inputs and wire clients

The added logger and constructors with Objects.requireNonNull(...) for RedissonClient arguments ensure that misconfigured pub/sub clients fail fast. The convenience RedissonPubSubStore(RedissonClient redisson, Long nodeId) cleanly supports the common “single client for all roles” case while the 3‑arg variant supports split pub/sub clients.


73-107: Subscribe/unsubscribe logic is now type‑safe and resilient

  • subscribe() now passes the provided clazz into topic.addListener(...) and calls listener.onMessage(msg) directly, eliminating the previous unchecked cast and preserving generic type information.
  • Using map.computeIfAbsent(type.toString(), ...) with a ConcurrentLinkedQueue gives a concise, thread‑safe way to track registration IDs per PubSubType.
  • unsubscribe() guards against null/empty regId queues and wraps each removeListener(id) in a try/catch with logging, so a failure to remove one listener doesn’t prevent others from being cleaned up.

These changes address the earlier NPE and type‑safety issues and make the pub/sub lifecycle much more robust under concurrency.


110-120: Shutdown cleanly tears down listeners without touching Redis clients

Iterating over all PubSubType values to unsubscribe, then clearing the internal map, ensures the store’s own listener bookkeeping is reset. Leaving the actual RedissonClient lifecycle to the owning factory avoids double‑shutdown and matches the new RedissonStoreFactory.shutdown() behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastPubSubStore.java (2)

60-72: subscribe(): concurrent registration & self-filtering look good; consider future-proofing null-safety

The lambda filter and computeIfAbsent-based registration look correct and thread-safe for concurrent subscriptions. The !nodeId.equals(msg.getMessageObject().getNodeId()) check assumes both IDs are always non-null; if that invariant might ever be relaxed, consider guarding with a null-safe comparison (e.g., via Objects.equals) to avoid a potential NPE. Otherwise this refactor is a clear improvement over manual map handling.


92-96: shutdown(): explicit unsubscribe-all is good; be aware of subscribe vs shutdown race

Unsubscribing all PubSubType values and then clearing the internal registration map makes the shutdown behavior explicit and avoids touching the Hazelcast client itself (leaving lifecycle to the owning factory), which is appropriate. If there’s any chance subscribe() can run concurrently with or after shutdown(), the final map.clear() can drop newly added regIds and leave Hazelcast listeners untracked; if that scenario is possible, consider a simple lifecycle guard (e.g., an AtomicBoolean checked in both subscribe and shutdown). If shutdown is only called during full server teardown with no concurrent subscriptions, current logic is fine.

netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonStoreFactory.java (3)

49-71: Constructors: null-checks and pubSubStore wiring look sound; minor API-shape questions

The added Objects.requireNonNull guards and the new (RedissonClient, RedissonPubSubStore) overload nicely tighten invariants and make dependency injection easier while keeping pubSubStore final and non-null. Two optional API-shape thoughts:

  • The 2-arg constructor currently accepts RedissonPubSubStore while the field type is PubSubStore; if there’s no Redisson-specific behavior needed here, accepting PubSubStore would make the factory more flexible.
  • For consistency and easier mocking, you might consider using RedissonClient in all constructor signatures (including the multi-instance variants) unless there’s a concrete-type requirement.
    Neither is blocking; the current behavior is correct as written.

72-83: Multi-client constructors: invariants enforced; clarify lifecycle ownership expectations

The multi-client constructors enforce non-null redisClient, redisPub, redisSub, and pubSubStore, which is good for catching misconfigurations early. One behavioral nuance: the factory unconditionally owns and shuts down the passed-in clients and pubSubStore in shutdown(). If callers might sometimes pass externally managed Redisson clients or pub/sub stores, it may be worth documenting that RedissonStoreFactory assumes ownership of these resources (or introducing a variant that does not shut them down). From a correctness perspective the current setup is internally consistent.

Also applies to: 85-97


110-131: shutdown(): ordered, de-duplicated client shutdown is an improvement; consider guarding pubSubStore.shutdown()

Calling pubSubStore.shutdown() first and then shutting down redisSub, redisPub, and redisClient via an ordered LinkedHashSet is a nice improvement: it enforces sub→pub→client order and naturally deduplicates when all three references point to the same RedissonClient. Since client shutdown is already best-effort with logging, you might similarly wrap pubSubStore.shutdown() in a try/catch so a failure there doesn’t prevent client shutdown from running.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 651142a and 74d4570.

📒 Files selected for processing (3)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastPubSubStore.java (4 hunks)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastStoreFactory.java (3 hunks)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonStoreFactory.java (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastStoreFactory.java
🧰 Additional context used
🧬 Code graph analysis (1)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonStoreFactory.java (1)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/pubsub/BaseStoreFactory.java (1)
  • BaseStoreFactory (32-161)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build (25) / build
  • GitHub Check: build (21) / build
  • GitHub Check: build (17) / build
  • GitHub Check: Analyze (java-kotlin)
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (2)
netty-socketio-core/src/main/java/com/socketio4j/socketio/store/HazelcastPubSubStore.java (1)

75-88: unsubscribe(): per-listener try/catch improves robustness

Using map.remove(name) to detach the registration list and then wrapping each topic.removeMessageListener(id) in its own try/catch is a solid pattern: one failing removal won’t prevent the rest, and the warning log includes both listener id and topic name for diagnosis. No functional issues spotted here.

netty-socketio-core/src/main/java/com/socketio4j/socketio/store/RedissonStoreFactory.java (1)

133-137: createMap(): straightforward delegation to Redisson client

Overriding createMap to delegate directly to redisClient.getMap(name) aligns with the BaseStoreFactory contract and keeps map creation centralized on the main client. No issues here.

Copy link
Collaborator

@NeatGuyCoding NeatGuyCoding left a comment

Choose a reason for hiding this comment

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

LGTM

@NeatGuyCoding NeatGuyCoding merged commit 319ce60 into main Nov 30, 2025
8 checks passed
@NeatGuyCoding NeatGuyCoding deleted the concurrent-performance-bug branch November 30, 2025 03:06
@coderabbitai coderabbitai bot mentioned this pull request Jan 1, 2026
18 tasks
@coderabbitai coderabbitai bot mentioned this pull request Jan 9, 2026
18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants