Skip to content

Conversation

@RomuDeuxfois
Copy link
Member

@RomuDeuxfois RomuDeuxfois commented Jan 8, 2026

Proposed changes

Testing Instructions

  1. Step-by-step how to test
  2. Environment or config notes

Related issues

Checklist

  • I consider the submitted work as finished
  • I tested the code for its functionality
  • I wrote test cases for the relevant uses case
  • I added/update the relevant documentation (either on github or on notion)
  • Where necessary I refactored code to improve the overall quality
  • For bug fix -> I implemented a test that covers the bug

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...

@RomuDeuxfois RomuDeuxfois changed the base branch from master to release/current January 8, 2026 13:13
@RomuDeuxfois RomuDeuxfois changed the title issue/4680 fix(manager-factory): full initialization at construction and disallow concurrent calls Jan 8, 2026
@RomuDeuxfois RomuDeuxfois changed the title fix(manager-factory): full initialization at construction and disallow concurrent calls [backend] fix(manager-factory): full initialization at construction and disallow concurrent calls Jan 8, 2026
@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

❌ Patch coverage is 52.94118% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.08%. Comparing base (5772942) to head (3494674).
⚠️ Report is 41 commits behind head on release/current.

Files with missing lines Patch % Lines
...in/java/io/openaev/integration/ManagerFactory.java 0.00% 7 Missing ⚠️
...aev/scheduler/jobs/ManagerIntegrationsSyncJob.java 0.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@SamuelHassine SamuelHassine requested a review from Copilot January 9, 2026 11:58
Copy link

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 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 @Lock annotation to monitorIntegrations() method to prevent concurrent execution
  • Refactored LockResourceType enum 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

Comment on lines 15 to 20
public Manager getManager() throws Exception {
if (this.manager == null) {
this.manager = new Manager(factories);
this.manager.monitorIntegrations();
}
return this.manager;
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
INJECT,
SECURITY_COVERAGE
INJECT(4096),
SECURITY_COVERAGE(4096),
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
Copy link
Member

@antoinemzs antoinemzs left a 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!

Comment on lines 14 to 20

public Manager getManager() throws Exception {
if (this.manager == null) {
this.manager = new Manager(factories);
this.manager.monitorIntegrations();
}
return this.manager;
Copy link
Member

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.

Copy link
Member Author

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.

@RomuDeuxfois RomuDeuxfois changed the title [backend] fix(manager-factory): full initialization at construction and disallow concurrent calls [backend] fix(manager-factory): full initialization at construction and disallow concurrent calls (#4680) Jan 9, 2026
@SamuelHassine SamuelHassine requested a review from Copilot January 10, 2026 10:32
Copy link

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

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

Comment on lines +17 to +23
public Manager getManager() {
Manager local = manager;
if (local == null) {
throw new IllegalStateException("Manager not initialized yet");
}
return local;
}
Copy link

Copilot AI Jan 10, 2026

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.

Copilot uses AI. Check for mistakes.
@Override
@Transactional(rollbackFor = Exception.class)
@LogExecutionTime
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
Copy link

Copilot AI Jan 10, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to 32
@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();
}
Copy link

Copilot AI Jan 10, 2026

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().

Copilot uses AI. Check for mistakes.
SECURITY_COVERAGE
INJECT(4096),
SECURITY_COVERAGE(4096),
MANAGER_FACTORY(1);
Copy link

Copilot AI Jan 10, 2026

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.

Copilot uses AI. Check for mistakes.
public Manager getManager() {
Manager local = manager;
if (local == null) {
throw new IllegalStateException("Manager not initialized yet");
Copy link

Copilot AI Jan 10, 2026

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."

Suggested change
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.");

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +13

LockResourceType(int stripes) {
this.stripes = stripes;
}

Copy link

Copilot AI Jan 10, 2026

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.

Suggested change
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
*/

Copilot uses AI. Check for mistakes.
@Transactional
public class ManagerFactory {
private final List<IntegrationFactory> factories;
private Manager manager = null;
Copy link

Copilot AI Jan 10, 2026

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.

Suggested change
private Manager manager = null;
private volatile Manager manager = null;

Copilot uses AI. Check for mistakes.
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.

[bug] Built-in integrations manager first initialisation may be too late

3 participants