Releases: cinchapi/runway
Release list
Version 2.1.0
- Fixed a bug where loading an access-controlled
Recordthrough anAudience(e.g.,Audience#load(Class, long)) threwUnsupportedOperationExceptionwhen the class's registered visibilityScopeused a scoped navigation criteria (Criteria.where().scope(prefix, inner)). (GH-125) - Breaking change:
@Computedproperties are no longer materialized byRecord#map(),Record#json(), orAudience#frame()unless the caller positively names them. The@Computedannotation has always promised that "computed data is generally expensive to generate and should only be calculated when explicitly requested" — but the historical default materialized every@Computedproperty on any bare serialization call, eagerly invoking suppliers the caller never asked for. That behavior contradicted the annotation's contract and made@Computedoperationally indistinguishable from@Derived. This release corrects the longstanding bug by aligning the default with the documented contract. (GH-128)- Mitigations for callers depending on the legacy behavior:
- If the property should always be eagerly materialized, change the annotation from
@Computedto@Derived— this is the correct annotation for properties that are part of the default serialized representation. - If the property must remain
@Computedbut the call site needs the legacy materialization, passSerializationOptions.builder().includeComputedValuesByDefault(true).build()tomap(opts),json(opts), orframe(opts, keys, subject). - Positively naming the
@Computedkey in the keys list (e.g.,record.map("propertyName")) always fires its supplier regardless of the flag.
- If the property should always be eagerly materialized, change the annotation from
Audience#framehas a new overload,frame(SerializationOptions, Collection<String>, Record), that threads options through every level of the framing pipeline, including recursive frames of linkedAccessControlrecords. The existingframe(Collection<String>, Record)andframe(Record)overloads delegate withSerializationOptions.defaults()— which under the new default now exclude@Computedproperties.
- Mitigations for callers depending on the legacy behavior:
- Fixed a bug where
Record#refresh()left memoized@Computedvalues cached from before the refresh, so subsequent reads returned stale results instead of recomputing against the refreshed state. (GH-93) - Added an additive
+key prefix toRecord#map,Record#json, andAudience#framefor layering a single key onto the default payload — the escape hatch for naming one@Computedproperty without enumerating every other key (e.g.,record.map("+computedProperty")returns the defaults plus the computed value). The moment any bare positive key appears in the call, defaults are dropped and only the listed keys (bare or+-prefixed) appear in the result;-continues to exclude. Legacy call shapes are unchanged. (GH-133) - Fixed a bug where
Audience#framesilently dropped a--prefixed key against any restricted (non-ALL_KEYS) readable set, collapsing the call to an empty map instead of returning the readable defaults minus the excluded key. (GH-133) - Fixed a bug where
Audience#frameserialized aRecord-valued key (or a collection ofRecordvalues) differently depending on how it was requested. An explicitly named key returned a bare reference to the record; the same key, when included implicitly by default, returned the fully nested object. Both now return the nested object.
Version 2.0.0
Command API
This release adopts the Concourse Command API (prepare()/submit()), introduced in Concourse 1.0.0, to batch Runway's hottest read and write paths into the fewest possible server round trips. When the connected server is older than 1.0.0, Runway transparently falls back to the legacy per-call path.
- Batched multi-selection reads:
Runway.select(Selection...)now collapses an N-selection call into a singleprepare()/submit()round trip, regardless of whether the selections target the same or different classes. Replaces the prior combinable/isolated dispatch that issued one round trip per isolated selection plus one for the OR-merged combinable batch. (GH-103) - Batched saves:
Runway.save(Record...)andRunway.save(boolean, Record...)now drive every save — stage, stale-data audit, uniqueness validation, field writes, cascade-delete reads, and commit — through as fewprepare()/submit()round trips as the save permits:1round trip when no validation reads are needed (no@Uniquefields andpreventStaleWrites=false);2round trips otherwise. For a record withffields,u@Uniquefields, andpreventStaleWrites=true, the save cost drops from~3 + u + f + 2round trips to2. (GH-104, GH-105)
Transitive Navigation
Every load, find, and findAny operation now resolves linked Record data through a single, unified pre-fetch path built around Concourse's navigate() API plus the new * transitive modifier. The unified path covers every reachable destination in a single navigate() per class group (typed or untyped) and a follow-up bulk-select() cleanup pass that closes any gaps the navigate paths cannot reach — mutual-reference cycles whose field names alternate, dangling links, links into records reached through single-Record edges, etc. (GH-80, GH-98)
- Self-referential
Recordgraphs of arbitrary depth pre-fetch in a single round trip via the*transitive modifier; previously, self-referentialCollection<Record>fields (e.g.,Exchange.children: Set<Exchange>) and self-referential single-Recordfields (e.g.,Exchange.parent: Exchange) bounded pre-fetching at one level. Collection<Record>fields reached through single-Recordedges (e.g.,Document.metadata.tags) and non-cyclicCollection<Record>fields nested under self-referential ones are now fully pre-fetched.- Untyped loads through
loadAny/findAnynow also benefit fromnavigate()pre-fetch — the load pipeline groups discovered records by their section key and dispatches a class-awarenavigate()per group. An untyped load that touchesKclasses therefore costs one batched round trip per group, so the table's1applies to typed loads and to untyped loads whose results all belong to a single class. - The single-record
load(Class, id)codepath is unified with the bulk pre-fetch path, so every load surface shares one mechanism. - Select-side path computation is unchanged.
Transitive navigation also cuts the number of server round trips a load costs, and the saving grows with how deeply records are linked. Consider a record type whose instances form a tree. It has a self-referential collection field, children: Set<Exchange>, and a parent reference back up the tree. Each node also links to a few other record types, some of them subtypes of a shared polymorphic hierarchy. The depth of such a tree — the number of levels of children that exist at run time — is set by the data, not by the schema.
On the 1.14.x line, running against Concourse 0.12.x, pre-fetching could descend a self-referential link only one level per round trip. Loading the root of the tree above cost roughly one round trip for each level of depth: the first round trip fetched the root's direct children, the second fetched their children, and so on down to the leaves. Because that depth is a property of the data, the cost had no ceiling. The deeper the tree, the more round trips every load took.
On 2.0.0, running against Concourse 1.0.x, the new * transitive modifier removes the per-level cost. A navigate path written children* tells a single navigate() call to follow the children link repeatedly, to whatever depth the data reaches. That one call pre-fetches the whole tree, every level at once, along with the other record types each node links to. A graph that used to cost one round trip per level now costs one round trip in total, however deep it runs. The table below states the upper bound on round trips for two representative graph shapes.
| Linked-record prefetch | Non-cyclic graph (depth L) |
Self-referential tree (depth D) |
|---|---|---|
1.14.x / 0.12.x — NONE, no prefetch (the default) |
1 + N |
1 + N |
1.14.x / 0.12.x — BULK_SELECT, batched per level |
1 + L |
1 + D |
2.0.0 / 1.0.x — Command API and * modifier |
1 |
1 |
2.0.0 / 0.12.x — legacy fallback (no Command API or *) |
2 |
2 + D |
Each cell is the largest number of server round trips needed to fully resolve a single load or find. The formulas use three quantities:
N— the number of linked records the load reaches.L— the length of the longest chain of non-cyclicRecord-to-Recordlinks. This is fixed by the schema, so it stays small and constant.D— the depth of a self-referential tree. This is set by the data and has no fixed upper bound.
No pre-fetch mechanism in the 1.14.x line could resolve a self-referential tree in a number of round trips that did not grow with D. The 2.0.0 mechanism is the first that can.
A 2.0.0 load has two independent optimization opportunities, both introduced in Concourse 1.0.0:
-
The Command API (
prepare()/submit()) lets Runway pack the per-loadselectfor the record itself and thenavigatefor the records it links to into a single round trip rather than two. -
The
*transitive modifier tonavigate()lets a singlenavigate()call follow a self-referential link to any depth. Runway emits*paths (e.g.,children*) for every self-referential edge in the source graph.
Concourse 1.0.x provides both features; older Concourse servers provide neither. The third row of the table is the both-on case; the fourth row is the both-off case.
Whichever paths a single navigate() cannot reach are gathered by a follow-up bulk-select() cleanup pass that walks the missing graph one round trip at a time. Two classes of edge always require the cleanup, regardless of server version: a cycle that alternates between two different field names (for example, a type A that links to B through a field named bs while B links back through a field named as — the * modifier can only repeat one field name), and a link whose target has been deleted. Against an older server, every self-referential edge also needs the cleanup, because the navigate-path set has had its * paths stripped.
The third row's 1 falls out of both optimizations being in effect: the Command API combines the select and the navigate into one round trip, and the * modifier makes that single navigate cover an arbitrarily deep self-referential tree, so the cleanup pass has nothing to walk for a pure tree. A graph with alternating-field cycles still costs one cleanup round trip per level of the part * could not express.
The fourth row's 2 + D is the same calculation with both optimizations absent: two round trips for the separate select and navigate, plus one cleanup round trip per level of every self-referential edge (+ D for a tree of depth D). Non-cyclic graphs do not depend on * paths, so they pay only the Command-API cost: 2 round trips.
API Breaks and Deprecations
- Removed the
CachingConcourseinfrastructure and the relatedRunway.Buildercache configuration.Runway.Builder.cache(Cache<Long, Record>)andRunway.Builder.withCache(Cache<Long, Map<String, Set<Object>>>)have been deleted along with thecom.cinchapi.runway.cachepackage (CachingConcourse,CachingConnectionPool,LeasingCache,NoOpLeasingCache,NoOpCache). Connection-level data caching is removed in this release; the plannedprepare()/submit()write transport made the per-method invalidation model untenable, and the implementation had latent invalidation bugs (e.g., missing overrides forconsolidate,link,unlink, and severalclear/insertvariants). Callers that relied onwithCache(...)should remove the configuration; the thread-local reservation API (Runway#reserve()/Runway#unreserve()) is unaffected. (GH-81) - Removed the
ReadStrategyenum and theRunway.Builder.readStrategy(...),Runway.Builder.streamingReadBufferSize(...), andRunway.Builder.recordsPerSelectBufferSize(...)configuration. Every read now fetches the matching records in bulk. The former streaming read strategy deferred each record's read until it was consumed, but with linked-Recordpre-fetching now unconditional it produced the same fully-loadedRecordgraph as a bulk read, so it offered no behavior worth configuring. Callers that set aReadStrategyor buffer size must remove those calls. - A
@Uniqueconstraint violation detected duringRunway.save()now surfaces as aRecord.ConstraintViolationException(a subtype ofRunwayException) rather than ajava.lang.IllegalStateException. The violation still makessave()returnfalsewith the exception recorded on the offendingRecord, but callers that catch or type-checkIllegalStateExceptionto det...
Version 1.14.6
- Fixed a bug where loading a
Recordgraph that contained a nestedRecordwith a danglingLink(one whose target had been cleared) inside aCollection<Record>field would throwInvalidArgumentException, making the graph unloadable until the danglingLinkwas removed manually. (GH-94) - Fixed a bug where loading a
Recordunder a non-defaultCollectionPreSelectStrategywould throwNullPointerExceptionand abort the entire load whenever aLinktarget was missing from the pre-fetched destination data. (GH-95)
Version 1.14.5
- Fixed a bug where an anonymous audience could not discover access-controlled records that were readable or writable by anonymous unless discoverability was also explicitly granted, unlike non-anonymous audiences who could implicitly discover any record they were permitted to read or write
Version 1.13.1
- Fixed a bug where an anonymous audience could not discover access-controlled records that were readable or writable by anonymous unless discoverability was also explicitly granted, unlike non-anonymous audiences who could implicitly discover any record they were permitted to read or write
Version 1.14.4
- Fixed a bug where
Selectionobjects passed to theRunway.select()method did not track state or results. The results were correctly available on the returnedSelectionscontainer, but the inputSelectionobjects should have also tracked this data. (GH-90) - Fixed a bug that allowed filtered
Selectionreads to poison the reservation cache and cause subsequent reads with the same parameters but a different or absent filter to return incorrect results. For example, a read through anAudiencecould cause subsequentRunway-wide reads to return results that were still narrowed by that audience's visibility rules. (GH-89) - Fixed a bug where multiple
Selectionobjects with the same query parameters but different filters passed to a singleRunway.select()call shared results instead of executing independently, causing the second selection to receive the first selection's filtered results. (GH-92) - Fixed a bug where injecting a
nullor no-op filter viaSelection.withInjectedFilter()into aSelectionthat already had a client-side filter would silently discard the original filter, causing the resultingSelectionto return unfiltered results.
Version 1.14.3
- Fixed a bug where
AdHocDataSourcerecords were invisible toRunway.select()when executing multiple selections simultaneously, causing count and data queries to return empty results (GH-86) - Fixed a bug where
Runway#close()could leave dangling instances in the static registry if closing the connection pool threw an exception, which could interfere with subsequent implicit saves (GH-87)
Version 1.14.2
- Fixed a bug where
Record#matches(Criteria)returned incorrect results for navigation keys that traverse two or more consecutive collection-valued fields (e.g.,tenants.seats.user.userIdwhere bothtenantsandseatsareSetfields). Only single-hop collection navigation worked correctly; paths with multiple collection hops always failed to match. This causedScope-based visibility rules that use multi-hop navigation to incorrectly filter out records that should have been visible. - Fixed a bug where
Record#matches(Criteria)always returnedfalseforLINKS_TOqueries that use navigation keys terminating at aRecord-valued field (e.g.,orgs.seats.member LINKS_TO 12345). These queries now correctly match when the navigated record's id satisfies theLINKS_TOcondition. - Behavioral change:
Record#get(String)with multi-hop navigation keys through consecutive collection-valued fields (e.g.,friends.friends.label) now returns a flat collection of leaf values instead of nested collections-of-collections. The previous nesting was erroneous — the flat result is consistent with how Concourse resolves the same navigation key server-side.
Version 1.14.1
- Fixed a bug where the Selection API (
Selection.ofandSelection.ofAny) did not support unique-result queries, forcing callers to use the legacyfindUnique/findAnyUniquemethods instead. AddedSelection.ofUnique(Class),Selection.ofAnyUnique(Class), and a chainable.unique()method onInitialBuilderandQueryBuilderthat produce aUniqueSelection— returning the single matching record (ornull) and throwingDuplicateEntryExceptionwhen more than one match exists. - Fixed a bug where passing duplicate
Selectionobjects toRunway#select(Selection...)caused redundant database queries instead of reusing the result from the first occurrence.
Version 1.14.0
- Static Visibility Scopes: Added
Scopeand static scope registration to theAccessControlframework as a class-level alternative to instance-based visibility checks. When aScopeis registered for anAccessControltype, it is applied duringAudience.select()in place of the per-instance$isDiscoverableBycheck:Scope.of(Criteria)pushes visibility filtering to the database as a query constraint, ensuring only matching records are returned rather than loading all records and filtering post-load. This is significantly more performant when only a small fraction of records for a class are visible to a given audience.Scope.unrestricted()short-circuits to return all records without any filtering.Scope.none()short-circuits to return no records without any database query.Scope.unsupported()signals that scope-based visibility is not applicable; instance-based checks are used instead.AccessControl.registerVisibilityScope(Class, Function<Audience, Scope>)registers a scope provider for a single class.AccessControl.registerVisibilityScopeHierarchy(Class, Function<Audience, Scope>)registers a scope provider for a class and all known subclasses discovered at runtime, without overwriting any explicit per-class registrations already made.- Instance-based permissions remain the default and are recommended for most use cases. Static scopes are best suited when access rules can be expressed as a well-defined
Criteria(to push filtering to the database) or when access is uniformly all-or-nothing across an entire class.
- Selection API: Added
Selection,Selections, andRunway#select(Selection...)for declaring and executing multiple data retrieval operations together. Theselect()API possibly executes multiple reads in as little as a single database round trips, reducing overhead regardless of any other configuration. - Read Reservations: Added
Runway#reserve()andRunway#unreserve()to activate and deactivate a thread-local reserve that works with both the Selection API and direct read methods. When the reserve is active,select()caches its results so that subsequent calls toselect(),find(),count(),load()— including reads through theAudienceframework — return the cached data instead of re-querying the database. This is designed for the middleware/handler pattern: middleware callsreserve()andselect()to pre-fetch data, route handlers read throughfind()/count()/load()orAudiencemethods and transparently benefit from the cache, andunreserve()clears everything at the end of the request. - Added
Runway#getKnownRecordTypes()to return all knownRecordsubclasses discovered on the classpath at runtime. - Fixed a bug where
Pagination.applyFilterAndPagewould throw aNullPointerExceptionwhen invoked with anullfilter ornullpage. - Fixed a bug where local
Criteriaevaluation viaConcourseCompilerdid not account for non-readable fields, producing results that diverged from how Concourse would resolve the sameCriteriaserver-side. Non-readable (e.g., private) fields are stored in the database and indexed like any other field, so server-side resolution always considers them. Local evaluation now includes all fields regardless of visibility, matching server-side behavior. - Added
Record#matches(Criteria)to test whether aRecordsatisfies aCriterialocally. Navigation keys are fully supported, including traversal through private fields and collections of linkedRecords. - Upgraded the
concourse-driver-javadependency to0.12.4to fix a bug that caused localCriteriaevaluation viaConcourseCompilerto provide inconsistent and unexpected results for records that did not contain a value stored under one or more keys in the inputCriteria.