Skip to content

Commit

Permalink
core: api: transient error for electrical profile set
Browse files Browse the repository at this point in the history
  • Loading branch information
Baptiste Prevot committed May 12, 2023
1 parent d2143b3 commit 58b625f
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 46 deletions.
129 changes: 88 additions & 41 deletions core/src/main/java/fr/sncf/osrd/api/ElectricalProfileSetManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import fr.sncf.osrd.external_generated_inputs.ElectricalProfileMapping;
import fr.sncf.osrd.railjson.schema.external_generated_inputs.RJSElectricalProfileSet;
import okhttp3.OkHttpClient;
import okio.BufferedSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
Expand All @@ -13,14 +12,55 @@

/** Manager that fetches and stores the different electrical profile sets used. */
public class ElectricalProfileSetManager extends APIClient {
private final ConcurrentHashMap<String, CacheEntry> cache =
protected final ConcurrentHashMap<String, CacheEntry> cache =
new ConcurrentHashMap<>();
private static final Logger logger = LoggerFactory.getLogger(ElectricalProfileSetManager.class);

public ElectricalProfileSetManager(String baseUrl, String authorizationToken, OkHttpClient client) {
super(baseUrl, authorizationToken, client);
}

/**
* Get the electrical profile set with the given ID and store it in the cacheEntry.
*/
public void downloadSet(CacheEntry cacheEntry, String profileSetId) {
try {
logger.info("Electrical profile set {} is not cached", profileSetId);
var endpointPath = String.format("electrical_profile_set/%s/", profileSetId);
var request = buildRequest(endpointPath);

logger.info("Fetching from {}", request.url());
cacheEntry.setStatus(CacheStatus.DOWNLOADING);
RJSElectricalProfileSet rjsProfileSet;
try (var response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful())
throw new UnexpectedHttpResponse(response);
var body = response.body();
if (body == null)
throw new JsonDataException("empty response body");
cacheEntry.setStatus(CacheStatus.PARSING_JSON);
rjsProfileSet = RJSElectricalProfileSet.adapter.fromJson(body.source());
}
if (rjsProfileSet == null)
throw new JsonDataException("Empty electrical profile set JSON");

logger.info("Electrical profile set {} fetched, parsing it", profileSetId);
cacheEntry.setStatus(CacheStatus.PARSING_MODEL);
var mapping = new ElectricalProfileMapping();
mapping.parseRJS(rjsProfileSet);

logger.info("Electrical profile set {} parsed", profileSetId);
cacheEntry.mapping = mapping;
cacheEntry.setStatus(CacheStatus.CACHED);
} catch (IOException | UnexpectedHttpResponse e) {
logger.error("Soft error while loading electrical profile set", e);
cacheEntry.setStatus(CacheStatus.TRANSIENT_ERROR);
} catch (Exception e) {
logger.error("Hard error while loading electrical profile set", e);
cacheEntry.setStatus(CacheStatus.ERROR);
}
}

/**
* Return the electrical profile set corresponding to the given id, in a ready-to-use format.
*/
Expand All @@ -32,56 +72,63 @@ public Optional<ElectricalProfileMapping> getProfileMap(String profileSetId) {
cache.putIfAbsent(profileSetId, new CacheEntry(null));
var cacheEntry = cache.get(profileSetId);

if (cacheEntry.status == CacheEntryStatus.INITIALIZING) {
if (!cacheEntry.status.isStable) {
synchronized (cacheEntry) {
try {
logger.info("Electrical profile set {} is not cached", profileSetId);
var endpointPath = String.format("electrical_profile_set/%s/", profileSetId);
var request = buildRequest(endpointPath);
logger.info("Fetching it from {}", request.url());

RJSElectricalProfileSet rjsProfileSet;
try (var response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful())
throw new UnexpectedHttpResponse(response);
var body = response.body();
if (body == null)
throw new JsonDataException("empty response body");
rjsProfileSet = RJSElectricalProfileSet.adapter.fromJson(body.source());
}
if (rjsProfileSet == null)
throw new JsonDataException("Empty electrical profile set JSON");
logger.info("Electrical profile set {} fetched, parsing it", profileSetId);
var mapping = new ElectricalProfileMapping();
mapping.parseRJS(rjsProfileSet);
logger.info("Electrical profile set {} parsed", profileSetId);
cacheEntry.mapping = mapping;
cacheEntry.status = CacheEntryStatus.CACHED;
} catch (IOException | UnexpectedHttpResponse | JsonDataException e) {
logger.error("failed to get electrical profile set", e);
cacheEntry.status = CacheEntryStatus.ERROR;
}
downloadSet(cacheEntry, profileSetId);
}
}

if (cacheEntry.status == CacheEntryStatus.CACHED)
if (cacheEntry.status == CacheStatus.CACHED)
return Optional.of(cacheEntry.mapping);
return Optional.empty();
}

public enum CacheEntryStatus {
INITIALIZING,
CACHED,
ERROR;
}

private static class CacheEntry {
private CacheEntryStatus status;
protected static class CacheEntry {
protected CacheStatus status;
private ElectricalProfileMapping mapping;

public CacheEntry(ElectricalProfileMapping mapping) {
CacheEntry(ElectricalProfileMapping mapping) {
this.mapping = mapping;
this.status = CacheEntryStatus.INITIALIZING;
this.status = CacheStatus.INITIALIZING;
}

void setStatus(CacheStatus newStatus) {
assert status.canTransitionTo(newStatus);
status = newStatus;
}
}

protected enum CacheStatus {
INITIALIZING(false),
DOWNLOADING(false),
PARSING_JSON(false),
PARSING_MODEL(false),
CACHED(true),
// errors that are known to be temporary
TRANSIENT_ERROR(false),
ERROR(true);

CacheStatus(boolean isStable) {
this.isStable = isStable;
}

final boolean isStable;
private CacheStatus[] transitions;

boolean canTransitionTo(CacheStatus newStatus) {
for (var status : transitions)
if (status == newStatus)
return true;
return false;
}

static {
INITIALIZING.transitions = new CacheStatus[] { DOWNLOADING };
DOWNLOADING.transitions = new CacheStatus[] { PARSING_JSON, ERROR, TRANSIENT_ERROR };
PARSING_JSON.transitions = new CacheStatus[] { PARSING_MODEL, ERROR, TRANSIENT_ERROR };
PARSING_MODEL.transitions = new CacheStatus[] { CACHED, ERROR, TRANSIENT_ERROR };
// at the next try
TRANSIENT_ERROR.transitions = new CacheStatus[] { DOWNLOADING };
}
}
}
2 changes: 1 addition & 1 deletion core/src/test/java/fr/sncf/osrd/api/ApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private static String parseMockRequest(Request request, String regex) throws IOE
if (matcher.matches()) {
try {
var path = matcher.group(1);
return Files.readString(Paths.get(Helpers.getResourcePath(path).toUri()));
return Files.readString(Helpers.getResourcePath(path));
} catch (IOException e) {
throw new IOException("Failed to read mock file", e);
} catch (AssertionError e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,42 @@

import static fr.sncf.osrd.external_generated_inputs.ElectricalProfileMappingTest.verifyProfileMap;

import fr.sncf.osrd.Helpers;
import org.junit.jupiter.api.Test;
import java.io.IOException;


public class ElectricalProfileSetManagerTest extends ApiTest {
@Test
public void testGetProfileMap() {
var profileMap =
electricalProfileSetManagerMock.getProfileMap("small_infra/external_generated_inputs.json");
var profileMap = electricalProfileSetManagerMock.getProfileMap("small_infra/external_generated_inputs.json");

assert profileMap.isPresent();
verifyProfileMap(profileMap.get());
}

@Test
public void testGetProfileMapInvalid() {
var profileMap =
electricalProfileSetManagerMock.getProfileMap("small_infra/invalid.json");
var profileMap = electricalProfileSetManagerMock.getProfileMap("small_infra/invalid.json");

assert profileMap.isEmpty();
}

@Test
public void failAndRetryTest() throws IOException {
var testID = "small_infra/external_generated_inputs.json";
// move the file, try to load, put the file back, load correctly
var path = Helpers.getResourcePath(testID);
var newPath = path.resolveSibling("external_generated_inputs.json.tmp");

java.nio.file.Files.move(path, newPath);
var profileMap = electricalProfileSetManagerMock.getProfileMap(testID);
assert profileMap.isEmpty();
assert electricalProfileSetManagerMock.cache.get(testID).status
== ElectricalProfileSetManager.CacheStatus.TRANSIENT_ERROR;

java.nio.file.Files.move(newPath, path);
profileMap = electricalProfileSetManagerMock.getProfileMap(testID);
assert profileMap.isPresent();
}
}

0 comments on commit 58b625f

Please sign in to comment.