Conversation
bpapillon
force-pushed
the
bpapillon/sch-7508-schematic-csharp-credit-leases-reservations-and-preflight
branch
from
September 17, 2026 15:23
7f14385 to
2c626ba
Compare
Prewarm without a datastream resolves nothing instead of throwing, the reservation pins to the debited lease with no fallback, an empty pin matches nothing in the in-memory store, and the Redis scripts are pinned by hash because the fake backend cannot see drift in them.
The API's flag check takes a preflight now, so a check that falls back to it answers the same hypothetical the local engine would. A preflighted check skips the flag cache, which is keyed without it.
The conformance runner lists the vectors directory and fails on an expect key it does not assert on, so a vector synced from node cannot pass unrun. The SDK closes a Redis connection it opened itself and never one the caller supplied. A zero-usage check stays cacheable.
…onfig The datastream client folds an engine exception into a normal result, so the lease check never cancelled the reservation on one. A reservation is now sized from the rounded-up usage so it matches what the settle bills. Also validates config, re-checks stop under the lock, and writes a reservation hash and its expiry in one transaction.
A lease that lands after stop is left to the drain or server expiry, as in node; releasing it refunded a lease sibling pods share. Hold sizing and the settle debit go back to the spec's usage times rate. Also builds the lease Redis options the way the cache does, keeps reconnecting when Redis is down at startup, guards a null balance payload, bounds quantities, and pins LF on the Lua-bearing files.
The interface said the in-memory store gets per-slot atomicity from a per-slot lock; it takes one monitor over the whole table, as its own class doc says.
Validate ran at the end of the constructor, after the event buffer's flush loop and the datastream socket were already up, so a rejected config leaked a thread and a websocket per client. It now sits with the replicator-mode checks at the top, above everything the constructor starts.
Extend, once a caller's join budget was spent, still handed back whoever else's flight was registered, awaited with neither the caller's deadline nor a check that it asked for enough. Both are the invariants the budget exists to hold. Node's startExtend registers over the top instead, and Go passes joinsLeft > 0 as its join flag, so the exhausted caller issues its own extend; do the same. MaybeExtendInBackgroundAsync read the stopped flag outside the gate, so a Stop and its drain could snapshot an empty pending set between the check and the registration, landing an extend after the close released the lease. Acquire already re-checks under the gate, and Go's spawn does the check and the registration under one mutex. The datastream path dropped the caller's DefaultValue. The engine does not throw when it declines; it answers with the client-wide flag default under one of two reasons, so the fallback never ran and server mode honoured an option client mode ignored. Node resolves it the same way (wrapper.ts, "the engine declining to answer is the case defaultValue exists for"). Also wraps the lease teardown so it cannot take the event buffer flush and the socket close with it, and rejects a negative PrewarmResolveTimeout rather than letting it collapse into the cache-only branch.
Covers the fix in ca77961 from both sides: the predicate that tells a refusal to evaluate from a verdict, and an end-to-end check proving a verdict the engine did reach still wins over the caller's default. The refusal itself cannot be driven from a test here. The WASM engine is built inside the datastream client with no seam to disable it, so it initialises and answers; the lease path's equivalent predicate is what LeaseCheckEngineFailureTests exercises.
The expiry index is what the sweeper reaches a surviving byCredit field through, so consume dropped the index first and orphaned the field if it then failed: the tenant's reserved-credits sum read high forever. The per-tenant field goes first now, matching schematic-node 923dee5 and the Go and Java stores. The two lease stores also read an empty pin differently. The Lua takes it as no pin and credits whichever lease holds the slot; the per-process store takes it as a pin nothing matches and drops the refund. Node reaches neither case because its absent pin is undefined, but C# has no undefined: a missing leaseId field reads back as an empty string, and so does the default on the public ReservationRecord. Both reservation stores now make the call themselves and decline, which is the safe half and what the per-process store already did.
CheckResult tells callers to bind a credits-remaining counter to CreditSettled, but the REST check and server mode each build the rules engine entitlement by hand, and neither copied the credit split, the consumption rate, the event subtype or the warning tiers. Every check without a usage goes through one of those two, so the counter the docs point at read as null. The test walks the target's properties by reflection rather than listing them, so a field added upstream fails here instead of being dropped.
cbrady
reviewed
Sep 24, 2026
| { | ||
| Company = company, | ||
| User = user, | ||
| Quantity = options.Usage.Value, |
Contributor
There was a problem hiding this comment.
This sends a fractional Usage to the server as-is (Quantity is a double?), but TrackWithReservation settles (long)Math.Ceiling(actual). So Check(usage: 2.5) holds 2.5×rate and later bills 3×rate, which is more than it held.
Contributor
|
here are some claude suggestions: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings the C# SDK to parity with Node, Go, and Python on credit leases and reservations.
Checkwith a usage now reserves credits: in client mode against a lease held locally (in memory, or in Redis so a fleet shares one lease), in server mode with one check-and-reserve call.TrackWithReservationsettles the reservation and release refunds an unused one. The plain check takes usage, event usage, and credit cost preflight options, threaded to both the API body and the local engine. Identify can prewarm a lease. Extend and check-and-reserve each send a fresh idempotency key per call and keep the default retry policy. Acquire sends none, as in the other SDKs: the API returns the slot's active lease on a repeat.The Redis key layout and Lua scripts are Node's, byte for byte, so a mixed-language fleet shares leases.
conformance/is copied verbatim from schematic-node and the runner passes every vector on both backends. .NET has no in-process Redis, so the Redis backend runs against a fake that interprets the scripts, which is what the Node reference does too.C#-specific choices: the check flow reads DataStream through a narrow
ICheckDataStream, as Go does, rather than reaching into the client. The generatedPreflightRequestBodydoubles as the engine's options envelope since the shapes already match.EventBodyTrack.Quantityis a long, so a fractional settle rounds up as in Go.Prewarmtakes company keys directly. Server mode maps the API entitlement field by field because the existing converter drops the consumption rate and event subtype.One behaviour change outside the lease paths: a DataStream that throws on start is now logged and cleared instead of failing the constructor, so the client falls back to REST checks and auto mode resolves to server. This is what schematic-node does, and it is what keeps a bad base URL from taking down a client that REST can still answer for.