docs(arch): A-2 + A-6 — async-trait trait family, pinned execution model
Resolves review 001 findings A-2 (critical) and A-6 (major) — the same signature surface: - ADR-012 §1: registry traits amended to #[async_trait] (bare async fn in traits is not dyn-compatible, E0038; ops sit behind Arc<dyn GitRegistryStore>). Desugared boxed Future form pinned in OQ-03's freeze inventory. async-trait = "0.1" added to the manifest. - backend.md concurrency model: the five-trait family is #[async_trait] Send + Sync dyn-compatible; the wire layer enforces ADR-009's pipeline-concurrency budget itself (permit acquired around each GitPackGen/GitPackIngest call — the concrete admission point); impls must not block the async executor and own their internal threading (gix impls run spawn_blocking inside the impl — POC-2's shape restated at its true layer). - transport.md §fetch: spawn_blocking sentence rephrased to the trait-contract version (the wire spec stops speaking gix). - ADR-009: enforcement point of the blocking-pool budget made concrete. - ADR-013 §6: ingestion spawn_blocking line aligned. - review 001: A-2, A-6 marked resolved. Verification: cargo test, clippy -D warnings, fmt --check, doc --no-deps, check --no-default-features, check --all-features — all clean.
This commit is contained in:
1 parent
c4b9c53674
commit
d067cf558a
7 files changed
+55
-15
No files matched your search
@@ -38,6 +38,11 @@ sha256 = ["gix-hash/sha256"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
alkcall = "0.8"
|
alkcall = "0.8"
|
||||||
|
# Trait-family dyn-compatibility (ADR-012 §1: all backend traits are
|
||||||
|
# #[async_trait] — bare `async fn` in a trait is not dyn-compatible,
|
||||||
|
# review 001 A-2; the family-wide pattern: alktty TtyBackend, alkcall
|
||||||
|
# ProtocolHandler).
|
||||||
|
async-trait = "0.1"
|
||||||
gix-odb = { version = "0.84", optional = true, default-features = false, features = ["sha1"] }
|
gix-odb = { version = "0.84", optional = true, default-features = false, features = ["sha1"] }
|
||||||
gix-pack = { version = "0.74", optional = true, features = ["sha1"] }
|
gix-pack = { version = "0.74", optional = true, features = ["sha1"] }
|
||||||
gix-ref = { version = "0.67", optional = true }
|
gix-ref = { version = "0.67", optional = true }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-09-25
|
last_updated: 2026-09-29
|
||||||
---
|
---
|
||||||
|
|
||||||
# Backend traits: the storage seam
|
# Backend traits: the storage seam
|
||||||
@@ -52,7 +52,8 @@ gix types:
|
|||||||
validated ref updates; the transaction itself is applied by `GitRefs`
|
validated ref updates; the transaction itself is applied by `GitRefs`
|
||||||
(single CAS home — ingest validates, refs commits; one transaction per
|
(single CAS home — ingest validates, refs commits; one transaction per
|
||||||
push is what makes `atomic` correct — ADR-013 §7). Budgeted (ADR-009
|
push is what makes `atomic` correct — ADR-013 §7). Budgeted (ADR-009
|
||||||
max pack size); blocking-thread friendly.
|
max pack size); the impl runs its blocking work off the async
|
||||||
|
executor (concurrency model below).
|
||||||
|
|
||||||
Minimal-vs-full was the open sub-question; resolved as **full family** —
|
Minimal-vs-full was the open sub-question; resolved as **full family** —
|
||||||
each trait is one or two methods plus types, and collapsing them (e.g.
|
each trait is one or two methods plus types, and collapsing them (e.g.
|
||||||
@@ -96,13 +97,31 @@ Two independent seams, two default-on features:
|
|||||||
## Concurrency model
|
## Concurrency model
|
||||||
|
|
||||||
- `gix` structures: `parking_lot` short-held locks; per-session handles
|
- `gix` structures: `parking_lot` short-held locks; per-session handles
|
||||||
moved into `spawn_blocking` tasks (POC-2's shape: store shared, handle
|
and `spawn_blocking` live inside the gix impls (POC-2's shape: store
|
||||||
per session, generation on blocking threads).
|
shared, handle per session, generation on blocking threads).
|
||||||
- Poisoned locks: `unwrap_or_else(|e| e.into_inner())` (convention 2).
|
- Poisoned locks: `unwrap_or_else(|e| e.into_inner())` (convention 2).
|
||||||
- The traits are `Send + Sync` object-safe; impls run under the
|
- **Trait execution model** (review 001 A-6): all five traits are
|
||||||
adapter's tokio context. `GitRegistry::resolve` is async (one call per
|
`#[async_trait]` — `Send + Sync`, dyn-compatible (E0038 forbids bare
|
||||||
session/request, before the first protocol byte — ADR-011 §6); no
|
`async fn`), signatures per ADR-012 §1. `GitRegistry::resolve` is
|
||||||
sync-hot-path constraint exists here, unlike alkcall's accept loop.
|
async (one call per session/request, before the first protocol byte —
|
||||||
|
ADR-011 §6); no sync-hot-path constraint exists here, unlike alkcall's
|
||||||
|
accept loop. The execution model the signatures imply:
|
||||||
|
- **The wire layer enforces the pipeline-concurrency budget itself**
|
||||||
|
(ADR-009's "max concurrent blocking pipeline tasks" — a permit
|
||||||
|
acquired in the wire layer around each `GitPackGen`/`GitPackIngest`
|
||||||
|
call; the admission point of "enforced at assembly/acceptance
|
||||||
|
time"). The wire layer is backend-trait-only (ADR-010) and knows
|
||||||
|
nothing of stores, handles, or threads — so the permit, not
|
||||||
|
`spawn_blocking`, is its entire concurrency contract.
|
||||||
|
- **Implementations must not block the async executor** and own their
|
||||||
|
internal threading: the gix impls run pack generation/ingestion on
|
||||||
|
`spawn_blocking` with the owned handle moved in *inside the trait
|
||||||
|
impl* (POC-2's shape, restated at its true layer — it is the impl's
|
||||||
|
internal structure, invisible from the wire layer).
|
||||||
|
- The traits are async so an embedder whose storage is async (DB
|
||||||
|
registries, network object stores) implements them natively; the
|
||||||
|
gix impl's blocking work is an internal detail, not part of the
|
||||||
|
seam.
|
||||||
|
|
||||||
## Public API surface
|
## Public API surface
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ gets the hard cap.
|
|||||||
*aggregate* work, not internal buffering.
|
*aggregate* work, not internal buffering.
|
||||||
- The blocking-pool budget is enforced at assembly/acceptance time
|
- The blocking-pool budget is enforced at assembly/acceptance time
|
||||||
(reject/slow-path excess concurrent generations), not per-byte.
|
(reject/slow-path excess concurrent generations), not per-byte.
|
||||||
|
Enforcement point (review 001 A-6): the wire layer — a permit
|
||||||
|
acquired around each `GitPackGen`/`GitPackIngest` call; impls own
|
||||||
|
their internal `spawn_blocking` (backend.md concurrency model).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
- `docs/research/vision.md` §"Guiding principles" 7
|
- `docs/research/vision.md` §"Guiding principles" 7
|
||||||
|
|||||||
@@ -42,11 +42,21 @@ and with the registry store (which needs no gitoxide at all).
|
|||||||
|
|
||||||
### 1. Read/write split on the record family
|
### 1. Read/write split on the record family
|
||||||
|
|
||||||
|
*(Signatures amended per review 001 A-2: bare `async fn` in a trait is
|
||||||
|
not dyn-compatible (E0038), and §3's ops sit behind `Arc<dyn
|
||||||
|
GitRegistryStore>`. All five backend traits carry `#[async_trait]` —
|
||||||
|
the family-wide pattern (alktty `TtyBackend`, alkcall
|
||||||
|
`ProtocolHandler`); `async-trait = "0.1"` is in the manifest. The
|
||||||
|
desugared `Pin<Box<dyn Future + Send>>` boxed form is the published
|
||||||
|
vtable shape — pinned deliberately here, in OQ-03's freeze inventory.)*
|
||||||
|
|
||||||
```
|
```
|
||||||
|
#[async_trait]
|
||||||
pub trait GitRegistry: Send + Sync {
|
pub trait GitRegistry: Send + Sync {
|
||||||
async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError>;
|
async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
pub trait GitRegistryStore: GitRegistry {
|
pub trait GitRegistryStore: GitRegistry {
|
||||||
async fn put_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;
|
async fn put_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;
|
||||||
async fn update_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;
|
async fn update_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ verification of the ingestion composition. Captures are recorded in
|
|||||||
concurrency budget apply (the generation budget covers ingestion —
|
concurrency budget apply (the generation budget covers ingestion —
|
||||||
same `spawn_blocking` pool, amended in ADR-009's table).
|
same `spawn_blocking` pool, amended in ADR-009's table).
|
||||||
- Ingestion runs on `spawn_blocking` (POC-2's shape: store shared,
|
- Ingestion runs on `spawn_blocking` (POC-2's shape: store shared,
|
||||||
handle per session).
|
handle per session — inside the gix impl; the wire layer's
|
||||||
|
pipeline-concurrency permit wraps the trait call per ADR-009,
|
||||||
|
backend.md concurrency model).
|
||||||
- fsck/connectivity: `gix-fsck::Connectivity` over the odb handle
|
- fsck/connectivity: `gix-fsck::Connectivity` over the odb handle
|
||||||
after indexing, for each new tip reachable from the pushed refs;
|
after indexing, for each new tip reachable from the pushed refs;
|
||||||
missing objects → `unpack ng <reason>`.
|
missing objects → `unpack ng <reason>`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-09-25
|
last_updated: 2026-09-29
|
||||||
---
|
---
|
||||||
|
|
||||||
# alkgit: Git Smart Protocol (wire layer)
|
# alkgit: Git Smart Protocol (wire layer)
|
||||||
@@ -70,9 +70,10 @@ substrate property (per-request state), not a protocol fork.
|
|||||||
re-sends wants + commons each round — negotiation-captures.md).
|
re-sends wants + commons each round — negotiation-captures.md).
|
||||||
Advertisement text is unchanged: `fetch=wait-for-done`.
|
Advertisement text is unchanged: `fetch=wait-for-done`.
|
||||||
- Pack generation via `GitPackGen` (ADR-004), streamed over sideband on
|
- Pack generation via `GitPackGen` (ADR-004), streamed over sideband on
|
||||||
duplex / sideband-in-response on http; generation runs on
|
duplex / sideband-in-response on http; the call is async-trait, with
|
||||||
`spawn_blocking` with the owned handle moved in (POC-2's shape: store
|
the pipeline-concurrency permit acquired around the call by the wire
|
||||||
shared, handle per session, generation on blocking threads).
|
layer (ADR-009's enforcement point — backend.md concurrency model;
|
||||||
|
the gix impl's internal `spawn_blocking` is its own detail).
|
||||||
- Round/haves budgets enforced here (ADR-009); an empty resulting pack
|
- Round/haves budgets enforced here (ADR-009); an empty resulting pack
|
||||||
(client already has everything) is a valid zero-object packfile.
|
(client already has everything) is a valid zero-object packfile.
|
||||||
|
|
||||||
|
|||||||
@@ -759,11 +759,11 @@ criticals are ADR-writing work, not code):
|
|||||||
| ID | Finding | Recommended fix | Effort | Risk | Status |
|
| ID | Finding | Recommended fix | Effort | Risk | Status |
|
||||||
|----|---------|----------------|--------|------|--------|
|
|----|---------|----------------|--------|------|--------|
|
||||||
| A-1 | op-gate OR not expressible in `AccessControl` | new ADR (or ADR-012 §3 amendment): handler-side two-tier check, create keeps static scope gate | small | none | **resolved (ADR-015)** — option (a) shape with the OR-term generalized to the `manage` grant |
|
| A-1 | op-gate OR not expressible in `AccessControl` | new ADR (or ADR-012 §3 amendment): handler-side two-tier check, create keeps static scope gate | small | none | **resolved (ADR-015)** — option (a) shape with the OR-term generalized to the `manage` grant |
|
||||||
| A-2 | `async fn` traits not dyn-compatible | ADR-012 §1 + backend.md amendment: `#[async_trait]`; add `async-trait = "0.1"` to manifest | small | none | open |
|
| A-2 | `async fn` traits not dyn-compatible | ADR-012 §1 + backend.md amendment: `#[async_trait]`; add `async-trait = "0.1"` to manifest | small | none | **resolved** — all five traits `#[async_trait]`, desugared boxed form pinned in the freeze inventory (OQ-03), dep in manifest |
|
||||||
| A-3 | native preamble / service dimension unpinned | new ADR: open-op params `{repo, service}`, session tuple + stateless entry gain the service selector, `GitAdapter` preamble pinned | moderate | wire-format (freeze inventory) | open |
|
| A-3 | native preamble / service dimension unpinned | new ADR: open-op params `{repo, service}`, session tuple + stateless entry gain the service selector, `GitAdapter` preamble pinned | moderate | wire-format (freeze inventory) | open |
|
||||||
| A-4 | done-round boundary set unverified | ADR-014 §2 + transport.md clause: boundary = `common_haves`-filtered haves | trivial | none | open |
|
| A-4 | done-round boundary set unverified | ADR-014 §2 + transport.md clause: boundary = `common_haves`-filtered haves | trivial | none | open |
|
||||||
| A-5 | consumer half unspecified | user scope decision, then amendment or small ADR (recommended: thin wrapper, deps carried with purpose) | small | scope | open |
|
| A-5 | consumer half unspecified | user scope decision, then amendment or small ADR (recommended: thin wrapper, deps carried with purpose) | small | scope | open |
|
||||||
| A-6 | trait execution model unspecified | backend.md paragraph + transport.md rephrase: async traits, wire-layer permit, impl-internal spawn_blocking | small | none | open |
|
| A-6 | trait execution model unspecified | backend.md paragraph + transport.md rephrase: async traits, wire-layer permit, impl-internal spawn_blocking | small | none | **resolved** — backend.md concurrency model: wire layer enforces the ADR-009 permit around gen/ingest trait calls; impls own internal `spawn_blocking` (ADR-009/ADR-013 aligned) |
|
||||||
| D-1 | stale Internal-ops + OQ-list text | supersession notes (vision, alk-stack, AGENTS) | trivial | none | open |
|
| D-1 | stale Internal-ops + OQ-list text | supersession notes (vision, alk-stack, AGENTS) | trivial | none | open |
|
||||||
| D-2 | ADR-007 step-3 mechanism superseded | amendment note on ADR-007 | trivial | none | open |
|
| D-2 | ADR-007 step-3 mechanism superseded | amendment note on ADR-007 | trivial | none | open |
|
||||||
| D-3 | authorized-repo marker missing from tuples | add to transport.md tuples + backend.md API list | trivial | none | open |
|
| D-3 | authorized-repo marker missing from tuples | add to transport.md tuples + backend.md API list | trivial | none | open |
|
||||||
|
|||||||
Reference in new issue
Block a user