Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/adr/0009-hot-path-result-dto.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ FeatureResult = Success { FeatureValue }
| Failure { FailureCode code, String detail }

FeatureValue = { FeatureRef feature,
Number value, // BigDecimal cents for SUM (P3); long for COUNT/DISTINCT
BigDecimal value, // uniform: integer-valued for COUNT/DISTINCT, cents for SUM (P3)
Exactness exactness, // EXACT | APPROXIMATE (FR-7)
ReadYourWriteLevel readYourWriteLevel, // ATOMIC | SNAPSHOT | BESTEFFORT (ADR 0007)
String definitionVersionHash, // FR-40; nullable until R12 lands
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi;

import com.codeheadsystems.velocity.spi.model.ApplyContext;
import com.codeheadsystems.velocity.spi.model.ApplyResult;
import com.codeheadsystems.velocity.spi.model.FeatureResult;
import com.codeheadsystems.velocity.spi.model.Intent.CountIntent;
import com.codeheadsystems.velocity.spi.model.QueryContext;
import com.codeheadsystems.velocity.spi.model.QueryTuple;
import java.util.List;

/**
* The {@code COUNT} aggregation mix-in (ADR 0003): apply and query count intents.
*
* <p>A backend implements this only if it declares {@link
* com.codeheadsystems.velocity.spi.model.AggregationType#COUNT COUNT} in its {@link
* com.codeheadsystems.velocity.spi.model.BackendCapabilities capabilities}. Each returned {@link
* FeatureResult} is a value or a distinguishable failure — never a silent {@code 0} (ADR 0009).
*/
public interface CountStore extends VelocityBackend {

/**
* Applies count writes. Returns an {@link ApplyResult} carrying, per intent, the {@link
* com.codeheadsystems.velocity.spi.model.ApplyStatus apply status} ({@code
* APPLIED|FAILED|SKIPPED} — a {@code SKIPPED} being a backend-owned outcome such as an idempotent
* replay) alongside the {@link FeatureResult} (ADR 0009, FR-34).
*
* @param ctx the namespace + optional deadline context
* @param intents the count intents to apply
* @return the per-intent apply outcomes, positionally aligned with {@code intents}
*/
ApplyResult applyCount(ApplyContext ctx, List<CountIntent> intents);

/**
* Queries counts, returning one result per tuple in order.
*
* @param ctx the namespace + optional deadline context
* @param tuples the query tuples to read
* @return the per-tuple results, positionally aligned with {@code tuples}
*/
List<FeatureResult> queryCount(QueryContext ctx, List<QueryTuple> tuples);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi;

import com.codeheadsystems.velocity.spi.model.ApplyContext;
import com.codeheadsystems.velocity.spi.model.ApplyResult;
import com.codeheadsystems.velocity.spi.model.FeatureResult;
import com.codeheadsystems.velocity.spi.model.Intent.DistinctIntent;
import com.codeheadsystems.velocity.spi.model.QueryContext;
import com.codeheadsystems.velocity.spi.model.QueryTuple;
import java.util.List;

/**
* The {@code DISTINCT} aggregation mix-in (ADR 0003): apply and query distinct-cardinality intents.
*
* <p>Distinct is <strong>exact where cheap, HLL above a per-{@code (subject, bucket)}
* threshold</strong> (ADR 0006): a bucket starts exact and, once it crosses the backend-clamped
* threshold, becomes permanently approximate; a multi-bucket window is approximate if any
* constituent bucket is HLL, with exact buckets HLL-folded at read time. HLL is
* <strong>tumbling-only</strong> (ADR 0005) — sliding distinct is exact and capped ({@link
* com.codeheadsystems.velocity.spi.model.FailureCode#CARDINALITY_CAP_EXCEEDED}). A backend
* implements this only if it declares {@link
* com.codeheadsystems.velocity.spi.model.AggregationType#DISTINCT DISTINCT}. Members are opaque,
* pre-hashed tokens the backend must not interpret. Each returned {@link FeatureResult} is a value
* or a distinguishable failure — never a silent {@code 0} (ADR 0009).
*/
public interface DistinctStore extends VelocityBackend {

/**
* Applies distinct writes. Returns an {@link ApplyResult} carrying, per intent, the {@link
* com.codeheadsystems.velocity.spi.model.ApplyStatus apply status} ({@code
* APPLIED|FAILED|SKIPPED}) alongside the {@link FeatureResult} (ADR 0009, FR-34).
*
* @param ctx the namespace + optional deadline context
* @param intents the distinct intents to apply
* @return the per-intent apply outcomes, positionally aligned with {@code intents}
*/
ApplyResult applyDistinct(ApplyContext ctx, List<DistinctIntent> intents);

/**
* Queries distinct cardinalities, returning one result per tuple in order.
*
* @param ctx the namespace + optional deadline context
* @param tuples the query tuples to read
* @return the per-tuple results, positionally aligned with {@code tuples}
*/
List<FeatureResult> queryDistinct(QueryContext ctx, List<QueryTuple> tuples);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi;

import com.codeheadsystems.velocity.spi.model.BucketValue;
import com.codeheadsystems.velocity.spi.model.Feature;
import com.codeheadsystems.velocity.spi.model.Namespace;
import com.codeheadsystems.velocity.spi.model.Subject;
import java.util.List;

/**
* Optional mix-in: seed pre-computed historical aggregates (FR-32, ADR 0008).
*
* <p>Seeding places per-bucket aggregate values into the same buckets a windowed query later merges
* (FR-14) — a single total is explicitly not a valid seed, since it cannot be apportioned across a
* window's buckets (ADR 0008). Seeded state is flagged onboarding-seeded, distinct from organically
* recorded state.
*
* <p>This mix-in is <strong>optional</strong>: a backend that cannot represent seeded buckets
* simply does not implement it and declares {@code seedSupported == false}. A backend that does
* implement it MUST reject a seed it cannot store — in particular an HLL sketch it did not itself
* produce (opaque, same-implementation-only bytes, ADR 0006) or an HLL sketch on a sliding feature
* (ADR 0005). Seed is an admin operation, rate-isolated from the hot path (FR-33).
*/
public interface SeedSupport extends VelocityBackend {

/**
* Seeds pre-computed per-bucket aggregates for a {@code (namespace, subject, feature)}.
*
* @param namespace the namespace to seed within
* @param subject the subject the aggregates are attributed to
* @param feature the feature being seeded
* @param buckets the per-bucket aggregate values to place
*/
void seed(Namespace namespace, Subject subject, Feature feature, List<BucketValue> buckets);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi;

/**
* Marker mix-in: the backend supports true sliding windows (ADR 0003, ADR 0005).
*
* <p>A sliding window covers {@code [now - duration, now]} with the <strong>backend clock as
* authority</strong> (FR-3), not the stateless pod's clock. Distinct on a sliding window is
* <strong>exact-only and capped</strong> — there is no HLL on sliding (ADR 0005), because a sketch
* cannot evict aging members; exceeding the cap is a declared, flagged {@link
* com.codeheadsystems.velocity.spi.model.FailureCode#CARDINALITY_CAP_EXCEEDED} condition.
*
* <p>This interface declares no methods: it is a capability flag whose coherence with {@link
* com.codeheadsystems.velocity.spi.model.BackendCapabilities} is asserted by the conformance TCK
* (ADR 0004).
*/
public interface SlidingSupport extends VelocityBackend {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi;

import com.codeheadsystems.velocity.spi.model.ApplyContext;
import com.codeheadsystems.velocity.spi.model.ApplyResult;
import com.codeheadsystems.velocity.spi.model.FeatureResult;
import com.codeheadsystems.velocity.spi.model.Intent.SumIntent;
import com.codeheadsystems.velocity.spi.model.QueryContext;
import com.codeheadsystems.velocity.spi.model.QueryTuple;
import java.util.List;

/**
* The {@code SUM} aggregation mix-in (ADR 0003): apply and query integer-cents value intents.
*
* <p>Values are {@code BigDecimal} cents (P3): no binary float and no silent overflow (FR-10). A
* backend implements this only if it declares {@link
* com.codeheadsystems.velocity.spi.model.AggregationType#SUM SUM}. Each returned {@link
* FeatureResult} is a value or a distinguishable failure — never a silent {@code 0} (ADR 0009).
*/
public interface SumStore extends VelocityBackend {

/**
* Applies sum writes. Returns an {@link ApplyResult} carrying, per intent, the {@link
* com.codeheadsystems.velocity.spi.model.ApplyStatus apply status} ({@code
* APPLIED|FAILED|SKIPPED}) alongside the {@link FeatureResult} (ADR 0009, FR-34).
*
* @param ctx the namespace + optional deadline context
* @param intents the sum intents to apply
* @return the per-intent apply outcomes, positionally aligned with {@code intents}
*/
ApplyResult applySum(ApplyContext ctx, List<SumIntent> intents);

/**
* Queries sums, returning one result per tuple in order.
*
* @param ctx the namespace + optional deadline context
* @param tuples the query tuples to read
* @return the per-tuple results, positionally aligned with {@code tuples}
*/
List<FeatureResult> querySum(QueryContext ctx, List<QueryTuple> tuples);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi;

/**
* Marker mix-in: the backend supports tumbling windows (ADR 0003, ADR 0005).
*
* <p>Tumbling windows are aligned, fixed buckets; a multi-bucket window is the merge of its
* buckets, <strong>edge-approximate at the current boundary</strong> (FR-14). Tumbling is the
* <strong>only window type on which HLL-distinct is valid</strong> (ADR 0005): a bucket is a closed
* interval that never needs to shed members, and HLL's lossless union merges buckets at read time.
*
* <p>This interface declares no methods: it is a capability flag whose coherence with {@link
* com.codeheadsystems.velocity.spi.model.BackendCapabilities} is asserted by the conformance TCK
* (ADR 0004).
*/
public interface TumblingSupport extends VelocityBackend {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi;

import com.codeheadsystems.velocity.spi.model.BackendCapabilities;
import com.codeheadsystems.velocity.spi.model.Namespace;
import com.codeheadsystems.velocity.spi.model.Subject;
import org.jspecify.annotations.Nullable;

/**
* The base of every backend: the capability descriptor plus admin erasure (ADR 0003).
*
* <p>A backend never implements this interface alone; it composes the aggregation mix-ins ({@link
* CountStore}, {@link SumStore}, {@link DistinctStore}), the window-capability markers ({@link
* SlidingSupport}, {@link TumblingSupport}), and optionally {@link SeedSupport} for the
* capabilities it declares. The set of mix-ins implemented MUST agree with {@link #capabilities()};
* the conformance TCK (ADR 0004) asserts that agreement.
*/
public interface VelocityBackend {

/**
* The single source of truth for what this backend can do (ADR 0003, P18).
*
* @return the backend's declared capabilities
*/
BackendCapabilities capabilities();

/**
* Admin erasure of stored state (FR-23).
*
* <p>Erases all state for the namespace, or — when {@code subject} is non-null — only that
* subject's state within the namespace.
*
* @param namespace the namespace to erase within
* @param subject the subject to erase; {@code null} erases the whole namespace
*/
void purge(Namespace namespace, @Nullable Subject subject);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi.model;

import java.util.Objects;
import org.jspecify.annotations.Nullable;

/**
* What a feature aggregates: a {@link AggregationType} and, for {@link AggregationType#DISTINCT
* DISTINCT}, the named dimension whose distinct values are counted (FR-11).
*
* <p>Invariant: {@code dimension} is non-null and non-blank <em>if and only if</em> the type is
* {@code DISTINCT}. For {@code COUNT} and {@code SUM} it MUST be null. Use the static factories.
*
* @param type the aggregation kind
* @param dimension the distinct dimension name (only for {@code DISTINCT}); otherwise null
*/
public record Aggregation(AggregationType type, @Nullable String dimension) {

/** Enforces the dimension-iff-DISTINCT invariant. */
public Aggregation {
Objects.requireNonNull(type, "type");
if (type == AggregationType.DISTINCT) {
Objects.requireNonNull(dimension, "dimension is required for DISTINCT");
if (dimension.isBlank()) {
throw new IllegalArgumentException("dimension must not be blank for DISTINCT");
}
} else if (dimension != null) {
throw new IllegalArgumentException("dimension must be null for " + type);
}
}

/**
* A COUNT aggregation.
*
* @return a {@code COUNT} aggregation with no dimension
*/
public static Aggregation count() {
return new Aggregation(AggregationType.COUNT, null);
}

/**
* A SUM aggregation.
*
* @return a {@code SUM} aggregation with no dimension
*/
public static Aggregation sum() {
return new Aggregation(AggregationType.SUM, null);
}

/**
* A DISTINCT aggregation over the given dimension.
*
* @param dimension the non-blank dimension name
* @return a {@code DISTINCT} aggregation for the dimension
*/
public static Aggregation distinct(String dimension) {
return new Aggregation(AggregationType.DISTINCT, dimension);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi.model;

/**
* The kind of aggregation a feature computes (D2, FR-11).
*
* <p>A closed set: {@link #COUNT} (how many events), {@link #SUM} (how much, in integer cents per
* P3/FR-10), and {@link #DISTINCT} (how many distinct values of a named dimension). Adding a new
* aggregation is an additive SPI change realized as a new capability mix-in defaulting to
* unsupported (ADR 0003, NFR-17), not by mutating this enum's meaning.
*/
public enum AggregationType {
/** Count of events for a subject in a window. */
COUNT,
/** Sum of a {@code BigDecimal}-cents value for a subject in a window (FR-10). */
SUM,
/** Cardinality of a named dimension's distinct values for a subject in a window (FR-11). */
DISTINCT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi.model;

import java.time.Duration;
import java.util.Objects;
import org.jspecify.annotations.Nullable;

/**
* The out-of-band context for an {@code apply()} call (ADR 0003).
*
* <p>Carries the namespace every SPI operation is scoped to (§2.1) and an optional caller deadline.
*
* @param namespace the tenancy scope
* @param deadline the caller deadline; {@code null} means unbounded — but a backend SHOULD still
* fail fast and return {@link FailureCode#DEADLINE_EXCEEDED} rather than block indefinitely
*/
public record ApplyContext(Namespace namespace, @Nullable Duration deadline) {

/** Validates the namespace is present ({@code deadline} may be null). */
public ApplyContext {
Objects.requireNonNull(namespace, "namespace");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi.model;

import java.util.List;
import java.util.Objects;

/**
* The aggregate result of an {@code apply()} fan-out: one {@link PerFeature} entry per touched
* feature (ADR 0009).
*
* @param perFeature the per-feature outcomes; defensively copied to an unmodifiable list
*/
public record ApplyResult(List<PerFeature> perFeature) {

/** Stores an unmodifiable defensive copy of the per-feature outcomes. */
public ApplyResult {
Objects.requireNonNull(perFeature, "perFeature");
perFeature = List.copyOf(perFeature);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.spi.model;

/**
* The per-feature outcome of an {@code apply()} fan-out (FR-34, ADR 0009).
*
* <p>Because one {@code record()} fans out across the matching features, each touched feature
* carries its own status alongside its {@link FeatureResult} in a {@link PerFeature} entry.
*/
public enum ApplyStatus {
/** The write was applied to the feature. */
APPLIED,
/**
* The write failed for the feature; the accompanying {@link FeatureResult} carries the reason.
*/
FAILED,
/** The write was intentionally skipped (e.g. idempotent replay, load-shed) for the feature. */
SKIPPED
}
Loading
Loading