Skip to content

Commit 584aafa

Browse files
authored
Merge pull request #925 from conveyal/dev
January 2024 Point Release 7.1
2 parents 6363b28 + b7d02fd commit 584aafa

File tree

15 files changed

+134
-75
lines changed

15 files changed

+134
-75
lines changed

build.gradle

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ plugins {
33
id 'application'
44
id 'maven-publish'
55
id 'com.palantir.git-version' version '2.0.0'
6+
id 'com.github.johnrengelman.shadow' version '8.1.1'
67
}
78

89
group = 'com.conveyal'
@@ -19,7 +20,7 @@ jar {
1920
// For Java 11+ Modules, specify a module name.
2021
// Do not create module-info.java until all our dependencies specify a module name.
2122
// Main-Class BackendMain will start a local backend.
22-
// Build-Jdk-Spec mimics a Maven manifest entry that helps us automatically install the right JVM.
23+
// Build-Jdk-Spec mimics a Maven manifest entry that helps us automatically install or select the right JVM.
2324
// Implementation-X attributes are needed for ImageIO (used by Geotools) to initialize in some environments.
2425
manifest {
2526
attributes 'Automatic-Module-Name': 'com.conveyal.r5',
@@ -31,6 +32,10 @@ jar {
3132
}
3233
}
3334

35+
shadowJar {
36+
mergeServiceFiles()
37+
}
38+
3439
// Allow reflective access by ObjectDiffer to normally closed Java internals. Used for round-trip testing serialization.
3540
// IntelliJ seems not to pass these JVM arguments when running tests from within the IDE, so the Kryo serialization
3641
// tests may only succeed under command line Gradle.
@@ -42,8 +47,8 @@ test {
4247
'--add-opens=java.base/java.lang=ALL-UNNAMED']
4348
}
4449

45-
// `gradle publish` will upload both shadow and simple JAR to Github Packages
46-
// On GH Actions, GITHUB_ACTOR env variable is supplied without specifying it in action yml.
50+
// Set up publication of jar files to GitHub Packages Maven repository.
51+
// On GitHub Actions, GITHUB_ACTOR env variable is supplied without specifying it in action yml.
4752
publishing {
4853
repositories {
4954
maven {
@@ -56,10 +61,12 @@ publishing {
5661
}
5762
}
5863
publications {
59-
// The presence of the shadow plugin somehow causes the shadow-jar to also be automatically included in this
60-
// publication. Ideally we want to produce the shadow jar and upload it to S3 as a worker, but only publish the
61-
// much smaller plain JAR without dependencies to Github Packages. On the other hand, we may want to publish
62-
// shadow jars for tagged releases.
64+
// The shadow plugin automatically creates and registers a component called "shadow" for integration with this
65+
// Maven publish plugin. `gradle publish` will then upload both shadow jar and simple jar to Github Packages.
66+
// See https://imperceptiblethoughts.com/shadow/getting-started/#default-java-groovy-tasks
67+
// To run R5 with dependencies, Conveyal does not use shadow jars anymore, only the zip distribution or runBackend.
68+
// For development builds and tests we don't need to produce a shadow jar, only publish the much smaller plain
69+
// jar without dependencies to Github Packages. For now, we continue to attach shadow jars to tagged releases.
6370
gpr(MavenPublication) {
6471
from(components.java)
6572
}

src/main/java/com/conveyal/analysis/components/HttpApi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private void respondToException(Exception e, Request request, Response response,
179179
// Include a stack trace except when the error is known to be about unauthenticated or unauthorized access,
180180
// in which case we don't want to leak information about the server to people scanning it for weaknesses.
181181
if (type != UNAUTHORIZED && type != FORBIDDEN) {
182-
body.put("stackTrace", errorEvent.stackTrace);
182+
body.put("stackTrace", errorEvent.filteredStackTrace);
183183
}
184184
response.status(code);
185185
response.type("application/json");

src/main/java/com/conveyal/analysis/components/broker/Broker.java

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,23 @@ public synchronized void enqueueTasksForRegionalJob (RegionalAnalysis regionalAn
177177
LOG.error("Someone tried to enqueue job {} but it already exists.", templateTask.jobId);
178178
throw new RuntimeException("Enqueued duplicate job " + templateTask.jobId);
179179
}
180+
// Create the Job object to share with the MultiOriginAssembler, but defer adding this job to the Multimap of
181+
// active jobs until we're sure the result assembler was constructed without any errors. Always add and remove
182+
// the Job and corresponding MultiOriginAssembler as a unit in the same synchronized block of code (see #887).
180183
WorkerTags workerTags = WorkerTags.fromRegionalAnalysis(regionalAnalysis);
181184
Job job = new Job(templateTask, workerTags);
182-
jobs.put(job.workerCategory, job);
183185

184186
// Register the regional job so results received from multiple workers can be assembled into one file.
187+
// If any parameters fail checks here, an exception may cause this method to exit early.
185188
// TODO encapsulate MultiOriginAssemblers in a new Component
186-
// Note: if this fails with an exception we'll have a job enqueued, possibly being processed, with no assembler.
187-
// That is not catastrophic, but the user may need to recognize and delete the stalled regional job.
188189
MultiOriginAssembler assembler = new MultiOriginAssembler(regionalAnalysis, job, fileStorage);
189190
resultAssemblers.put(templateTask.jobId, assembler);
190191

192+
// A MultiOriginAssembler was successfully put in place. It's now safe to register and start the Job.
193+
jobs.put(job.workerCategory, job);
194+
195+
// If this is a fake job for testing, don't confuse the worker startup code below with its null graph ID.
191196
if (config.testTaskRedelivery()) {
192-
// This is a fake job for testing, don't confuse the worker startup code below with null graph ID.
193197
return;
194198
}
195199

@@ -385,14 +389,20 @@ public synchronized void markTaskCompleted (Job job, int taskId) {
385389
}
386390

387391
/**
388-
* When job.errors is non-empty, job.isErrored() becomes true and job.isActive() becomes false.
392+
* Record an error that happened while a worker was processing a task on the given job. This method is tolerant
393+
* of job being null, because it's called on a code path where any number of things could be wrong or missing.
394+
* This method also ensures synchronization of writes to Jobs from any non-synchronized sections of an HTTP handler.
395+
* Once job.errors is non-empty, job.isErrored() becomes true and job.isActive() becomes false.
389396
* The Job will stop delivering tasks, allowing workers to shut down, but will continue to exist allowing the user
390397
* to see the error message. User will then need to manually delete it, which will remove the result assembler.
391-
* This method ensures synchronization of writes to Jobs from the unsynchronized worker poll HTTP handler.
392398
*/
393399
private synchronized void recordJobError (Job job, String error) {
394400
if (job != null) {
395-
job.errors.add(error);
401+
// Limit the number of errors recorded to one.
402+
// Still using a Set<String> instead of just String since the set of errors is exposed in a UI-facing API.
403+
if (job.errors.isEmpty()) {
404+
job.errors.add(error);
405+
}
396406
}
397407
}
398408

@@ -488,21 +498,23 @@ public void handleRegionalWorkResult(RegionalWorkResult workResult) {
488498
// Once the job is retrieved, it can be used below to requestExtraWorkersIfAppropriate without synchronization,
489499
// because that method only uses final fields of the job.
490500
Job job = null;
491-
MultiOriginAssembler assembler;
492501
try {
502+
MultiOriginAssembler assembler;
493503
synchronized (this) {
494504
job = findJob(workResult.jobId);
505+
// Record any error reported by the worker and don't pass bad results on to regional result assembly.
506+
// This will mark the job as errored and not-active, stopping distribution of tasks to workers.
507+
// To ensure that happens, record errors before any other conditional that could exit this method.
508+
if (workResult.error != null) {
509+
recordJobError(job, workResult.error);
510+
return;
511+
}
495512
assembler = resultAssemblers.get(workResult.jobId);
496513
if (job == null || assembler == null || !job.isActive()) {
497514
// This will happen naturally for all delivered tasks after a job is deleted or it errors out.
498515
LOG.debug("Ignoring result for unrecognized, deleted, or inactive job ID {}.", workResult.jobId);
499516
return;
500517
}
501-
if (workResult.error != null) {
502-
// Record any error reported by the worker and don't pass bad results on to regional result assembly.
503-
recordJobError(job, workResult.error);
504-
return;
505-
}
506518
// Mark tasks completed first before passing results to the assembler. On the final result received,
507519
// this will minimize the risk of race conditions by quickly making the job invisible to incoming stray
508520
// results from spurious redeliveries, before the assembler is busy finalizing and uploading results.

src/main/java/com/conveyal/analysis/components/broker/Job.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,11 @@ private RegionalTask makeOneTask (int taskNumber) {
123123
public int deliveryPass = 0;
124124

125125
/**
126-
* If any error compromises the usabilty or quality of results from any origin, it is recorded here.
126+
* If any error compromises the usability or quality of results from any origin, it is recorded here.
127127
* This is a Set because identical errors are likely to be reported from many workers or individual tasks.
128+
* The presence of an error here causes the job to be considered "errored" and "inactive" and stop delivering tasks.
129+
* There is some risk here of accumulating unbounded amounts of large error messages (see #919).
130+
* The field type could be changed to a single String instead of Set, but it's exposed on a UI-facing API as a Set.
128131
*/
129132
public final Set<String> errors = new HashSet();
130133

src/main/java/com/conveyal/analysis/components/eventbus/ErrorEvent.java

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import com.conveyal.r5.util.ExceptionUtils;
44

5+
import static com.conveyal.r5.util.ExceptionUtils.filterStackTrace;
6+
57
/**
68
* This Event is fired each time a Throwable (usually an Exception or Error) occurs on the backend. It can then be
79
* recorded or tracked in various places - the console logs, Slack, etc. This could eventually be used for errors on
810
* the workers as well, but we'd have to be careful not to generate hundreds of messages at once.
911
*/
1012
public class ErrorEvent extends Event {
1113

12-
// We may serialize this object, so we convert the Throwable to two strings to control its representation.
14+
// All Events are intended to be eligible for serialization into a log or database, so we convert the Throwable to
15+
// some Strings to determine its representation in a simple way.
1316
// For flexibility in event handlers, it is tempting to hold on to the original Throwable instead of derived
1417
// Strings. Exceptions are famously slow, but it's the initial creation and filling in the stack trace that are
15-
// slow. Once the instace exists, repeatedly examining its stack trace should not be prohibitively costly. Still,
16-
// we do probably gain some efficiency by converting the stack trace to a String once and reusing that.
18+
// slow. Once the instance exists, repeatedly examining its stack trace should not be prohibitively costly.
1719

1820
public final String summary;
1921

@@ -23,11 +25,16 @@ public class ErrorEvent extends Event {
2325
*/
2426
public final String httpPath;
2527

28+
/** The full stack trace of the exception that occurred. */
2629
public final String stackTrace;
2730

31+
/** A minimal stack trace showing the immediate cause within Conveyal code. */
32+
public final String filteredStackTrace;
33+
2834
public ErrorEvent (Throwable throwable, String httpPath) {
2935
this.summary = ExceptionUtils.shortCauseString(throwable);
3036
this.stackTrace = ExceptionUtils.stackTraceString(throwable);
37+
this.filteredStackTrace = ExceptionUtils.filterStackTrace(throwable);
3138
this.httpPath = httpPath;
3239
}
3340

@@ -54,25 +61,9 @@ public String traceWithContext (boolean verbose) {
5461
if (verbose) {
5562
builder.append(stackTrace);
5663
} else {
57-
builder.append(filterStackTrace(stackTrace));
64+
builder.append(filteredStackTrace);
5865
}
5966
return builder.toString();
6067
}
6168

62-
private static String filterStackTrace (String stackTrace) {
63-
if (stackTrace == null) return null;
64-
final String unknownFrame = "Unknown stack frame, probably optimized out by JVM.";
65-
String error = stackTrace.lines().findFirst().get();
66-
String frame = stackTrace.lines()
67-
.map(String::strip)
68-
.filter(s -> s.startsWith("at "))
69-
.findFirst().orElse(unknownFrame);
70-
String conveyalFrame = stackTrace.lines()
71-
.map(String::strip)
72-
.filter(s -> s.startsWith("at com.conveyal."))
73-
.filter(s -> !frame.equals(s))
74-
.findFirst().orElse("");
75-
return String.join("\n", error, frame, conveyalFrame);
76-
}
77-
7869
}

src/main/java/com/conveyal/analysis/results/CsvResultWriter.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ public abstract class CsvResultWriter extends BaseResultWriter implements Region
4141
*/
4242
public abstract CsvResultType resultType ();
4343

44-
/** Override to provide column names for this CSV writer. */
44+
/**
45+
* Override to provide column names for this CSV writer.
46+
* NOTE: Due to Java weirdness, subclass implementations of this method will be called by the CsvResultWriter
47+
* constructor at a time when fields of the subclass remain initialized, but uninitialized final primitive
48+
* fields are still readable! Do not read subclass fields in these implementations until/unless this is restructured.
49+
*/
4550
protected abstract String[] columnHeaders ();
4651

4752
/** Override to extract row values from a single origin result. */

src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import com.conveyal.file.FileStorage;
88
import com.conveyal.file.FileStorageFormat;
99
import com.conveyal.r5.analyst.PointSet;
10+
import com.conveyal.r5.analyst.cluster.PathResult;
11+
import com.conveyal.r5.analyst.cluster.RegionalTask;
1012
import com.conveyal.r5.analyst.cluster.RegionalWorkResult;
1113
import com.conveyal.r5.util.ExceptionUtils;
1214
import org.slf4j.Logger;
@@ -89,21 +91,27 @@ public MultiOriginAssembler (RegionalAnalysis regionalAnalysis, Job job, FileSto
8991
this.job = job;
9092
this.nOriginsTotal = job.nTasksTotal;
9193
this.originsReceived = new BitSet(job.nTasksTotal);
92-
// Check that origin and destination sets are not too big for generating CSV files.
93-
if (!job.templateTask.makeTauiSite &&
94-
job.templateTask.destinationPointSetKeys[0].endsWith(FileStorageFormat.FREEFORM.extension)
95-
) {
96-
// This requires us to have already loaded this destination pointset instance into the transient field.
97-
PointSet destinationPointSet = job.templateTask.destinationPointSets[0];
98-
if ((job.templateTask.recordTimes || job.templateTask.includePathResults) && !job.templateTask.oneToOne) {
99-
if (nOriginsTotal * destinationPointSet.featureCount() > MAX_FREEFORM_OD_PAIRS ||
100-
destinationPointSet.featureCount() > MAX_FREEFORM_DESTINATIONS
101-
) {
102-
throw new AnalysisServerException(String.format(
103-
"Freeform requests limited to %d destinations and %d origin-destination pairs.",
104-
MAX_FREEFORM_DESTINATIONS, MAX_FREEFORM_OD_PAIRS
105-
));
106-
}
94+
// If results have been requested for freeform origins, check that the origin and
95+
// destination pointsets are not too big for generating CSV files.
96+
RegionalTask task = job.templateTask;
97+
if (!task.makeTauiSite && task.destinationPointSetKeys[0].endsWith(FileStorageFormat.FREEFORM.extension)) {
98+
// This requires us to have already loaded this destination pointset instance into the transient field.
99+
PointSet destinationPointSet = task.destinationPointSets[0];
100+
int nDestinations = destinationPointSet.featureCount();
101+
int nODPairs = task.oneToOne ? nOriginsTotal : nOriginsTotal * nDestinations;
102+
if (task.recordTimes &&
103+
(nDestinations > MAX_FREEFORM_DESTINATIONS || nODPairs > MAX_FREEFORM_OD_PAIRS)) {
104+
throw AnalysisServerException.badRequest(String.format(
105+
"Travel time results limited to %d destinations and %d origin-destination pairs.",
106+
MAX_FREEFORM_DESTINATIONS, MAX_FREEFORM_OD_PAIRS
107+
));
108+
}
109+
if (task.includePathResults &&
110+
(nDestinations > PathResult.MAX_PATH_DESTINATIONS || nODPairs > MAX_FREEFORM_OD_PAIRS)) {
111+
throw AnalysisServerException.badRequest(String.format(
112+
"Path results limited to %d destinations and %d origin-destination pairs.",
113+
PathResult.MAX_PATH_DESTINATIONS, MAX_FREEFORM_OD_PAIRS
114+
));
107115
}
108116
}
109117

@@ -152,8 +160,11 @@ public MultiOriginAssembler (RegionalAnalysis regionalAnalysis, Job job, FileSto
152160
regionalAnalysis.resultStorage.put(csvWriter.resultType(), csvWriter.fileName);
153161
}
154162
}
163+
} catch (AnalysisServerException e) {
164+
throw e;
155165
} catch (Exception e) {
156-
throw new RuntimeException("Exception while creating multi-origin assembler: " + ExceptionUtils.stackTraceString(e));
166+
// Handle any obscure problems we don't want end users to see without context of MultiOriginAssembler.
167+
throw new RuntimeException("Exception while creating multi-origin assembler: " + e.toString(), e);
157168
}
158169
}
159170

src/main/java/com/conveyal/data/census/TigerLineSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void load (ShapeDataStore store) throws Exception {
3434
for (SimpleFeatureIterator it = sfc.features(); it.hasNext();) {
3535
GeobufFeature feat = new GeobufFeature(it.next());
3636
feat.id = null;
37-
feat.numericId = Long.parseLong((String) feat.properties.get("GEOID10"));
37+
feat.numericId = Long.parseLong((String) feat.properties.get("GEOID20"));
3838
feat.properties = new HashMap<>();
3939
store.add(feat);
4040
}

src/main/java/com/conveyal/r5/analyst/TemporalDensityResult.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ public class TemporalDensityResult {
3333

3434
/**
3535
* The temporal density of opportunities. For each destination set, for each percentile, for each minute of
36-
* travel from 0 to 120, the number of opportunities reached in travel times from i (inclusive) to i+1 (exclusive).
36+
* travel m from 0 to 119, the number of opportunities reached in travel times from m (inclusive) to m+1
37+
* (exclusive).
3738
*/
3839
public final double[][][] opportunitiesPerMinute;
3940

@@ -57,7 +58,7 @@ public void recordOneTarget (int target, int[] travelTimePercentilesSeconds) {
5758
break; // If any percentile is unreached, all higher ones are also unreached.
5859
}
5960
int m = travelTimePercentilesSeconds[p] / 60;
60-
if (m <= 120) {
61+
if (m < 120) {
6162
opportunitiesPerMinute[d][p][m] += dps.getOpportunityCount(target);
6263
}
6364
}

0 commit comments

Comments
 (0)