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
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -48,13 +54,22 @@ public class MyJsonServer {
*/
private final JSONServer server;

private static final long INITIAL_RETRY_DELAY_SECONDS = 5;
private static final double RETRY_DELAY_MULTIPLIER = 1.5;
private static final long MAX_RETRY_DELAY_SECONDS = 60; // maximum delay between retries is 60 seconds
private static final long MAX_TOTAL_RETRY_TIME_SECONDS = 600; // maximum total retry time is 600 seconds

// All implemented Profiles
private final ServerCoreProfile coreProfile;
private final ServerFirmwareManagementProfile firmwareProfile;
private final ServerLocalAuthListProfile localAuthListProfile = new ServerLocalAuthListProfile();
private final ServerRemoteTriggerProfile remoteTriggerProfile = new ServerRemoteTriggerProfile();
private final ServerReservationProfile reservationProfile = new ServerReservationProfile();
private final ServerSmartChargingProfile smartChargingProfile = new ServerSmartChargingProfile();
private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
protected final Map<String, UUID> pendingSessions = new ConcurrentHashMap<>();
private final Map<String, AtomicInteger> retryAttempts = new ConcurrentHashMap<>();
private final Map<String, Long> totalRetryTime = new ConcurrentHashMap<>();

public MyJsonServer(EvcsOcppServer parent) {
this.parent = parent;
Expand Down Expand Up @@ -92,11 +107,22 @@ public void newSession(UUID sessionIndex, SessionInformation information) {

MyJsonServer.this.parent.ocppSessions.put(ocppIdentifier, sessionIndex);

var presentEvcss = MyJsonServer.this.parent.ocppEvcss.get(ocppIdentifier);
// 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
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

MyJsonServer.this
.logDebug("EVCS not yet configured for ocppId " + ocppIdentifier + ". Will retry.");
// Store the session for later processing
MyJsonServer.this.pendingSessions.put(ocppIdentifier, sessionIndex);
// Optionally, schedule a retry after a delay
MyJsonServer.this.retryNewSession(sessionIndex, information, ocppIdentifier);
return;
}

// Proceed as normal
MyJsonServer.this.parent.activeEvcsSessions.put(sessionIndex, presentEvcss);

for (AbstractManagedOcppEvcsComponent evcs : presentEvcss) {
Expand Down Expand Up @@ -146,7 +172,7 @@ protected void deactivate() {
*
* @param session unique session id referring to the corresponding Evcs
* @param request given request that needs to be sent
* @return CompletitionStage
* @return CompletionStage
* @throws OccurenceConstraintException OccurenceConstraintException
* @throws UnsupportedFeatureException UnsupportedFeatureException
* @throws NotConnectedException NotConnectedException
Expand Down Expand Up @@ -215,6 +241,67 @@ protected void sendPermanentRequests(List<AbstractManagedOcppEvcsComponent> evcs
}
}

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

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

private void scheduleRetry(UUID sessionIndex, SessionInformation information, String ocppIdentifier) {
int attempt = MyJsonServer.this.retryAttempts.get(ocppIdentifier).getAndIncrement();
long delay = MyJsonServer.this.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 = MyJsonServer.this.totalRetryTime.get(ocppIdentifier) + delay / 1000; // Convert milliseconds to seconds
MyJsonServer.this.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.pendingSessions.remove(ocppIdentifier);
MyJsonServer.this.retryAttempts.remove(ocppIdentifier);
MyJsonServer.this.totalRetryTime.remove(ocppIdentifier);
return;
}

MyJsonServer.this.scheduledExecutorService.schedule(() -> {
List<AbstractManagedOcppEvcsComponent> presentEvcss = MyJsonServer.this.parent.ocppEvcss
.get(ocppIdentifier);
if (presentEvcss != null) {
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);
}
// Remove from pending sessions and retry attempts
MyJsonServer.this.pendingSessions.remove(ocppIdentifier);
MyJsonServer.this.retryAttempts.remove(ocppIdentifier);
MyJsonServer.this.totalRetryTime.remove(ocppIdentifier);
} else {
MyJsonServer.this.logDebug("Still waiting for EVCS configuration for ocppId " + ocppIdentifier);
// Schedule the next retry
MyJsonServer.this.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;
}

private HashMap<String, String> getConfiguration(UUID sessionIndex) {
var hash = new HashMap<String, String>();
var request = new GetConfigurationRequest();
Expand Down
Loading