-
Notifications
You must be signed in to change notification settings - Fork 194
[backend] fix(manager-factory): full initialization at construction and disallow concurrent calls (#4680) #4696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/current
Are you sure you want to change the base?
Conversation
…nd disallow concurrent calls
1e80762 to
4c0f3b1
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## release/current #4696 +/- ##
=====================================================
+ Coverage 52.74% 53.08% +0.34%
- Complexity 4119 4199 +80
=====================================================
Files 963 972 +9
Lines 28942 29374 +432
Branches 2152 2216 +64
=====================================================
+ Hits 15265 15594 +329
- Misses 12779 12877 +98
- Partials 898 903 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this 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 addresses issue #4680 by ensuring the Manager class is fully initialized at construction time and preventing concurrent calls to critical sections. The key changes involve calling monitorIntegrations() immediately after Manager instantiation and adding a lock to prevent race conditions.
Key changes:
- Manager initialization now includes
monitorIntegrations()call in ManagerFactory - Added
@Lockannotation tomonitorIntegrations()method to prevent concurrent execution - Refactored
LockResourceTypeenum to support configurable stripe counts per lock type
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| openaev-api/src/main/java/io/openaev/integration/ManagerFactory.java | Adds monitorIntegrations() call during Manager initialization to ensure full setup |
| openaev-api/src/main/java/io/openaev/integration/Manager.java | Adds @Lock annotation to monitorIntegrations() to prevent concurrent access |
| openaev-api/src/main/java/io/openaev/aop/lock/LockResourceType.java | Refactors enum to include stripe counts, adds MANAGER_FACTORY with single stripe for exclusive access |
| openaev-api/src/main/java/io/openaev/aop/lock/LockAspect.java | Updates initialization to use stripe counts from enum instead of hardcoded values |
| openaev-api/src/main/java/io/openaev/scheduler/jobs/EngineSyncExecutionJob.java | Removes unused @LogExecutionTime import |
| public Manager getManager() throws Exception { | ||
| if (this.manager == null) { | ||
| this.manager = new Manager(factories); | ||
| this.manager.monitorIntegrations(); | ||
| } | ||
| return this.manager; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lazy initialization pattern with null check is not thread-safe. Multiple threads could pass the null check simultaneously, creating multiple Manager instances and calling monitorIntegrations() multiple times. Consider using double-checked locking with volatile, or making this method synchronized, or using a safer initialization pattern.
| INJECT, | ||
| SECURITY_COVERAGE | ||
| INJECT(4096), | ||
| SECURITY_COVERAGE(4096), |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The choice of a single stripe (1) for MANAGER_FACTORY should be documented. While this effectively creates an exclusive lock across all keys of this type, it's not immediately obvious why this differs from the 4096 stripes used for other lock types. Consider adding a comment explaining that this is intentional to ensure exclusive access during manager initialization.
| SECURITY_COVERAGE(4096), | |
| SECURITY_COVERAGE(4096), | |
| // Use a single stripe to enforce an exclusive lock across all manager factory keys, | |
| // ensuring only one manager initialization can proceed at a time. |
antoinemzs
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving to expedite, but solving the race condition on getManager() is a must do. Thanks!
|
|
||
| public Manager getManager() throws Exception { | ||
| if (this.manager == null) { | ||
| this.manager = new Manager(factories); | ||
| this.manager.monitorIntegrations(); | ||
| } | ||
| return this.manager; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible race condition on getManager(), so there should be a lock here too like we discussed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, after thinking it through and looking at how the Manager is accessed, it felt odd to duplicate logic instead of clearly separating responsibilities.
I’ve updated the design and introduced a clearer approach, let me know what you think
The manager field is initialized only via initializeAndSync(), which is protected by a distributed lock.
The getManager() method is read-only and never performs initialization.
monitorIntegrations() is intended to be called exclusively by ManagerFactory.
…nd disallow concurrent calls (#4680)
5f034d7 to
3494674
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
| public Manager getManager() { | ||
| Manager local = manager; | ||
| if (local == null) { | ||
| throw new IllegalStateException("Manager not initialized yet"); | ||
| } | ||
| return local; | ||
| } |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getManager() method now throws an IllegalStateException if called before initialization, but there's no mechanism to ensure initialization happens at application startup. This can lead to the same race condition the PR intends to fix, where ExecutionExecutorService calls getManager() before the scheduled job has run initializeAndSync(). Consider adding an @eventlistener(ApplicationReadyEvent.class) method to guarantee initialization happens early in the application lifecycle.
| @Override | ||
| @Transactional(rollbackFor = Exception.class) | ||
| @LogExecutionTime | ||
| public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The removal of @LogExecutionTime from EngineSyncExecutionJob appears unrelated to the stated purpose of fixing the manager initialization race condition. This should either be explained in the PR description or removed from this PR to keep changes focused.
| @Transactional | ||
| @Lock(type = MANAGER_FACTORY, key = "manager-factory") | ||
| public void initializeAndSync() throws Exception { | ||
| if (manager == null) { | ||
| manager = new Manager(factories); | ||
| } | ||
| return this.manager; | ||
| manager.monitorIntegrations(); | ||
| } |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new initializeAndSync() method lacks test coverage. Given that this method addresses a critical race condition bug, it should have comprehensive tests covering: 1) successful initialization on first call, 2) thread safety when called concurrently, 3) proper synchronization behavior with the Lock annotation, and 4) that subsequent calls don't re-initialize but do call monitorIntegrations().
| SECURITY_COVERAGE | ||
| INJECT(4096), | ||
| SECURITY_COVERAGE(4096), | ||
| MANAGER_FACTORY(1); |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting MANAGER_FACTORY to use only 1 stripe effectively serializes all calls to initializeAndSync(), which may be overly conservative. While this is acceptable for initialization, consider whether this lock configuration will cause unnecessary blocking in future use cases. Document why 1 stripe was chosen versus the 4096 used for other resource types.
| public Manager getManager() { | ||
| Manager local = manager; | ||
| if (local == null) { | ||
| throw new IllegalStateException("Manager not initialized yet"); |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message "Manager not initialized yet" should be more informative to help developers diagnose the issue. Consider a message like "Manager has not been initialized. Ensure initializeAndSync() is called during application startup or by the scheduled ManagerIntegrationsSyncJob before accessing the manager."
| throw new IllegalStateException("Manager not initialized yet"); | |
| throw new IllegalStateException( | |
| "Manager has not been initialized. Ensure initializeAndSync() is called during " | |
| + "application startup or by the scheduled ManagerIntegrationsSyncJob before " | |
| + "accessing the manager."); |
|
|
||
| LockResourceType(int stripes) { | ||
| this.stripes = stripes; | ||
| } | ||
|
|
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stripes() method and the enum constructor lack documentation. Add Javadoc explaining what "stripes" means in this context (the number of locks in the striped lock implementation) and why different resource types require different stripe counts.
| LockResourceType(int stripes) { | |
| this.stripes = stripes; | |
| } | |
| /** | |
| * Creates a new {@link LockResourceType} with the given number of stripes. | |
| * | |
| * <p>In this context, a <em>stripe</em> represents one lock in a striped lock implementation. | |
| * Each resource type can be protected by multiple underlying locks; the {@code stripes} value | |
| * defines how many distinct locks are available. Using more stripes spreads lock acquisition | |
| * across more lock instances, which can reduce contention and increase throughput for | |
| * high-concurrency resources, at the cost of additional lock objects and slightly higher | |
| * management overhead. | |
| * | |
| * @param stripes the number of locks (stripes) to use when guarding this resource type | |
| */ | |
| LockResourceType(int stripes) { | |
| this.stripes = stripes; | |
| } | |
| /** | |
| * Returns the number of stripes configured for this resource type. | |
| * | |
| * <p>The stripe count is the number of underlying locks used by the striped lock implementation | |
| * when synchronizing access to resources of this type. Resource types that are accessed | |
| * frequently or by many concurrent requests typically use a higher stripe count to reduce lock | |
| * contention, while resource types with low contention can use fewer stripes to minimize | |
| * overhead. | |
| * | |
| * @return the number of locks (stripes) associated with this resource type | |
| */ |
| @Transactional | ||
| public class ManagerFactory { | ||
| private final List<IntegrationFactory> factories; | ||
| private Manager manager = null; |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manager field should be declared as volatile to ensure visibility guarantees when accessed from multiple threads. The current implementation uses a local variable copy in getManager() which suggests an attempt at implementing double-checked locking, but without volatile, this pattern is broken and can lead to returning a partially constructed Manager object or null even after initialization.
| private Manager manager = null; | |
| private volatile Manager manager = null; |
Proposed changes
Testing Instructions
Related issues
Checklist
Further comments
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...