Skip to content
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

Fix Race Condition in EVCS Component Initialization and OCPP Server Startup #2912

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from

Conversation

Sn0w3y
Copy link
Contributor

@Sn0w3y Sn0w3y commented Dec 5, 2024

Description

This pull request addresses a race condition in the OpenEMS Edge project, where charging stations may attempt to connect to the OCPP server before the corresponding EVCS components are fully initialized. The solution implements an exponential backoff retry mechanism with optional randomness to handle these cases gracefully.


Problem

When the OCPP server starts, it begins accepting connections from charging stations immediately. However, the EVCS components may not be fully initialized at that time. If a charging station connects before its corresponding EVCS component is ready, the server fails to associate the session with the EVCS, leading to errors and unsuccessful connections.


Solution

  • Retry Mechanism: Implement a retry mechanism in the MyJsonServer class to handle new sessions when the EVCS component is not yet initialized.
  • Exponential Backoff: Use an exponential backoff strategy to increase the delay between retries, preventing the system from being overwhelmed.
  • Random Jitter: Add optional randomness to the delay to avoid synchronized retries, reducing the likelihood of simultaneous retry attempts.
  • Maximum Retry Time: Introduce a maximum total retry time to prevent indefinite retries if the EVCS component fails to initialize.

Changes Made

1. Modified newSession Method

In MyJsonServer.java, the newSession method now checks if the EVCS component is initialized. If not, it stores the session in a pendingSessions map and initiates the retry mechanism.

@Override
public void newSession(UUID sessionIndex, SessionInformation information) {
    // Existing code...

    // Try to get the presentEvcss
    List<AbstractManagedOcppEvcsComponent> presentEvcss = MyJsonServer.this.parent.ocppEvcss.get(ocppIdentifier);

    if (presentEvcss == null) {
        // EVCS not yet configured, store session and retry later
        MyJsonServer.this.logDebug("EVCS not yet configured for ocppId " + ocppIdentifier + ". Will retry.");
        // Store the session for later processing
        MyJsonServer.this.parent.pendingSessions.put(ocppIdentifier, sessionIndex);
        // Schedule a retry
        retryNewSession(sessionIndex, information, ocppIdentifier);
        return;
    }

    // Proceed as normal
    // Existing code...
}

2. Implemented retryNewSession Method with Exponential Backoff

Added a new method retryNewSession that schedules retries using a ScheduledExecutorService. The delay between retries increases exponentially, capped at a maximum value.

private void retryNewSession(UUID sessionIndex, SessionInformation information, String ocppIdentifier) {
    // Initialize retry attempt counter and total retry time for this ocppIdentifier
    retryAttempts.putIfAbsent(ocppIdentifier, new AtomicInteger(0));
    totalRetryTime.putIfAbsent(ocppIdentifier, 0L);

    // Schedule the first retry attempt
    scheduleRetry(sessionIndex, information, ocppIdentifier);
}

private void scheduleRetry(UUID sessionIndex, SessionInformation information, String ocppIdentifier) {
    int attempt = retryAttempts.get(ocppIdentifier).getAndIncrement();
    long delay = calculateNextDelay(attempt);

    // Optional: Add randomness (jitter) to delay to prevent synchronized retries
    long randomJitter = ThreadLocalRandom.current().nextLong(0, 1000); // 0 to 1000 milliseconds
    delay += randomJitter;

    // Update total retry time
    long totalTime = totalRetryTime.get(ocppIdentifier) + delay / 1000; // Convert milliseconds to seconds
    totalRetryTime.put(ocppIdentifier, totalTime);

    // Optional: Check if total retry time exceeds maximum allowed
    if (totalTime > MAX_TOTAL_RETRY_TIME_SECONDS) {
        MyJsonServer.this.logWarn("Maximum total retry time exceeded for ocppId " + ocppIdentifier);
        // Clean up
        MyJsonServer.this.parent.pendingSessions.remove(ocppIdentifier);
        retryAttempts.remove(ocppIdentifier);
        totalRetryTime.remove(ocppIdentifier);
        return;
    }

    scheduledExecutorService.schedule(() -> {
        List<AbstractManagedOcppEvcsComponent> presentEvcss = MyJsonServer.this.parent.ocppEvcss.get(ocppIdentifier);
        if (presentEvcss != null) {
            // Proceed with session setup
            MyJsonServer.this.logDebug("EVCS configured for ocppId " + ocppIdentifier + ". Proceeding with session setup.");
            MyJsonServer.this.parent.activeEvcsSessions.put(sessionIndex, presentEvcss);

            for (AbstractManagedOcppEvcsComponent evcs : presentEvcss) {
                evcs.newSession(MyJsonServer.this.parent, sessionIndex);
                MyJsonServer.this.sendInitialRequests(sessionIndex, evcs);
            }

            // Clean up
            MyJsonServer.this.parent.pendingSessions.remove(ocppIdentifier);
            retryAttempts.remove(ocppIdentifier);
            totalRetryTime.remove(ocppIdentifier);
        } else {
            MyJsonServer.this.logDebug("Still waiting for EVCS configuration for ocppId " + ocppIdentifier);
            // Schedule the next retry
            scheduleRetry(sessionIndex, information, ocppIdentifier);
        }
    }, delay, TimeUnit.MILLISECONDS);
}

private long calculateNextDelay(int attempt) {
    long delay = (long)(INITIAL_RETRY_DELAY_SECONDS * 1000 * Math.pow(RETRY_DELAY_MULTIPLIER, attempt));
    delay = Math.min(delay, MAX_RETRY_DELAY_SECONDS * 1000); // Ensure delay does not exceed maximum
    return delay;
}

3. Added Necessary Data Structures

Introduced maps to keep track of retry attempts and total retry time per ocppIdentifier.

// In MyJsonServer class
// Constants for retry logic
private static final long INITIAL_RETRY_DELAY_SECONDS = 5;
private static final long MAX_RETRY_DELAY_SECONDS = 300; // 5 minutes maximum delay
private static final double RETRY_DELAY_MULTIPLIER = 1.5;
private static final long MAX_TOTAL_RETRY_TIME_SECONDS = 1800; // 30 minutes

// Maps to track retry attempts and total retry time
private final Map<String, AtomicInteger> retryAttempts = new ConcurrentHashMap<>();
private final Map<String, Long> totalRetryTime = new ConcurrentHashMap<>();

// Scheduled executor for retries
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

4. Moved pendingSessions to EvcsOcppServer Class

To maintain consistency with other session management fields, moved pendingSessions to the EvcsOcppServer class.

// In EvcsOcppServer.java
protected final Map<String, UUID> pendingSessions = new ConcurrentHashMap<>();

5. Properly Shutdown Scheduled Executor Service

Ensured that the ScheduledExecutorService is properly shut down when the server is deactivated to prevent resource leaks.

protected void deactivate() {
    this.server.close();
    scheduledExecutorService.shutdownNow();
}

Related Issues
Issue #2909: Race Condition in Component Initialization and OCPP Server Startup

Copy link

codecov bot commented Dec 5, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #2912   +/-   ##
==========================================
  Coverage      56.63%   56.63%           
  Complexity      9504     9504           
==========================================
  Files           2251     2251           
  Lines          96038    96038           
  Branches        7090     7090           
==========================================
  Hits           54378    54378           
  Misses         39659    39659           
  Partials        2001     2001           


if (presentEvcss == null) {
// EVCS not yet configured, store session and retry later
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm concerned that there's a possibility of sending CALL messages simultaneously from the following three locations, making it difficult to meet the requirements of Section 4.1.1 (Synchronicity) in the specification. Therefore, I think it might be an option not to boot MyJsonServer until everything is properly set up. What do you think? @Sn0w3y

4.1.1. Synchronicity

A Charge Point or Central System SHOULD NOT send a CALL message to the other party unless all the CALL messages it sent before have been responded to or have timed out. A CALL message has been responded to when a CALLERROR or CALLRESULT message has been received with the message ID of the CALL message.

A CALL message has timed out when:

• it has not been responded to, and
• an implementation-dependent timeout interval has elapsed since the message was sent.

Implementations are free to choose this timeout interval. It is RECOMMENDED that they take into account the kind of network used to communicate with the other party. Mobile networks typically have much longer worst-case round-trip times than fixed lines.

https://openchargealliance.org/download/7b06ab293c68fb6b4f4ae0960e502579c1c5516aa2b7acf0fdcedba585b9ea7f

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