ctlproto

package module
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

README

ctlproto — Control-plane protocol

Wire types and codecs for the storage control plane, including tenant validation, bounded batch requests, dedup-index operations, pack associations, and presigned access.

Go module: github.com/scitrera/memorylayer-storage/ctlproto. Initial version: 0.7.1, licensed under AGPL-3.0-only.

The .proto file describes the contract; the Go implementation provides the currently supported codecs. Coordinate wire changes with both clients and servers.

From the repository root:

cd ctlproto
go test ./...
go build ./...

The root go.work resolves sibling modules for development. Published module versions use ctlproto/vX.Y.Z tags. See development and releases and operations.

Documentation

Overview

Package ctlproto defines the control-plane message contract, a swappable codec, and NATS subject conventions shared by the blobgw server and the mlfs-node client (RemoteDedupStore).

Architecture reference

See ADR-001 §2.5: the internal node ↔ blobgw control plane uses NATS request-reply. blobgw replicas subscribe to subjects of the form blobgw.<tenant>.* as a queue group so NATS load-balances across replicas — no external load balancer is required for control-plane scale-out.

Codec

The ADR commits to protobuf as the wire format (schema evolution + python edge client contract). The generated Go types and a ProtobufCodec will be provided once protoc-gen-go is available in the build toolchain. Until then the wire types are hand-written Go structs and the default codec is JSONCodec. The Codec interface is intentionally the only abstraction here: callers hold a Codec and the swap to protobuf requires only switching which Codec implementation is passed in — no call-site changes. See controlplane.proto for the canonical message schema.

NATS payload limits

NATS defaults to a 1 MB max payload. Callers MUST split batches larger than MaxChunkHashesPerBatch (for LookupBatch / RecordRequest) or MaxPresignKeysPerBatch (for PresignBatchRequest) across multiple requests. Both constants are sized conservatively so a full batch fits well within 1 MB; see their doc comments for the sizing rationale.

Tenant validation

Tenant strings are used directly as NATS subject tokens. A tenant string MUST NOT contain dots ('.'), spaces, or the NATS wildcard characters ('*', '>'); these would break subject routing. ValidateTenant returns an error for any such input and all subject helpers call it internally.

Index

Constants

View Source
const MaxChunkHashesPerBatch = 4_000

MaxChunkHashesPerBatch is the maximum number of chunk hashes a caller should include in a single LookupBatchRequest or RecordRequest.

Sizing — the binding worst case is the RESPONSE direction, not the request:

RecordRequest:        one ChunkLocation per entry ≈ 228 bytes (64-char
                      chunk hash + 64-char pack hash + offset + size +
                      JSON field names and delimiters). 4 000 × 228 ≈ 912 KB.

LookupBatchResponse:  at full hit-rate the response is a map[string]PackRef
                      ≈ 213 bytes per entry (64-char chunk hash key +
                      PackRef fields). 4 000 × 213 ≈ 852 KB.

LookupBatchRequest:   request side is only the hash array ≈ 70 bytes per
                      hash; 4 000 × 70 ≈ 280 KB — well under the limit.

All three fit within 900 KB, leaving margin under the NATS 1 MB default max_payload for NATS framing and domain/field overhead. Callers MUST split larger sets across multiple sequential requests.

View Source
const MaxPackHashesPerBatch = 4_000

MaxPackHashesPerBatch is the maximum number of pack hashes a caller should include in a single PurgePacksRequest.

Sizing: a PurgePacksRequest carries only pack hashes (no PackRef values), so the per-entry cost is lower (~70 bytes per 64-char hex hash). We use the same conservative value as MaxChunkHashesPerBatch (4 000) so a single constant governs all chunked RPC loops and the math stays straightforward: 4 000 × 70 ≈ 280 KB, well within the 900 KB target. Callers MUST split larger sets across multiple sequential requests.

View Source
const MaxPresignKeysPerBatch = 2_000

MaxPresignKeysPerBatch is the maximum number of S3 object keys a caller should include in a single PresignBatchRequest.

Sizing: S3 object keys are up to 1024 bytes. Assuming a typical pack key (~80 bytes) and URL response (~300 bytes each), 2 000 keys fits within ~760 KB — safely under the NATS 1 MB limit. Callers MUST split larger sets across multiple sequential requests.

View Source
const QueueGroup = "blobgw"

QueueGroup is the NATS queue group name that all blobgw replicas subscribe under for control-plane subjects. NATS delivers each incoming request to exactly one member of the group, providing load-balanced scale-out without an external load balancer. To scale blobgw horizontally, run more replicas subscribing to the same subjects with this queue group name.

Variables

This section is empty.

Functions

func AssocObliterateSubject

func AssocObliterateSubject(tenant string) string

AssocObliterateSubject returns the NATS subject for ObliterateContext RPCs for the given tenant: blobgw.<tenant>.assoc.obliterate

func AssocRecordSubject

func AssocRecordSubject(tenant string) string

AssocRecordSubject returns the NATS subject for Associate RPCs for the given tenant: blobgw.<tenant>.assoc.record

func GCPurgeSubject

func GCPurgeSubject(tenant string) string

GCPurgeSubject returns the NATS subject for PurgePacks RPCs for the given tenant: blobgw.<tenant>.gc.purge

func IndexLookupSubject

func IndexLookupSubject(tenant string) string

IndexLookupSubject returns the NATS subject for LookupBatch RPCs for the given tenant: blobgw.<tenant>.index.lookup

blobgw replicas subscribe as queue group QueueGroup; NATS load-balances requests across them.

func IndexRecordSubject

func IndexRecordSubject(tenant string) string

IndexRecordSubject returns the NATS subject for Record RPCs for the given tenant: blobgw.<tenant>.index.record

func PresignSubject

func PresignSubject(tenant string) string

PresignSubject returns the NATS subject for PresignBatch RPCs for the given tenant: blobgw.<tenant>.presign

func TenantFromSubject

func TenantFromSubject(subject string) (tenant string, ok bool)

TenantFromSubject extracts the tenant token from a control-plane subject of the form blobgw.<tenant>.<op...> (e.g. blobgw.acme.index.lookup → "acme"). It is the inverse of the *Subject helpers and is used by the blobgw server, which subscribes with a wildcard (blobgw.*.index.lookup, …) across all tenants and must recover the concrete tenant from the delivered subject.

ok is false when subject does not have the blobgw.<tenant>.* shape or when the extracted token is not a valid tenant (per ValidateTenant). Callers should still re-run ValidateTenant if they want the specific error; this helper only reports whether a usable tenant token was found.

func ValidateTenant

func ValidateTenant(tenant string) error

ValidateTenant checks that tenant is safe to use as a NATS subject token. A tenant string MUST NOT contain '.', ' ', '*', or '>' because these characters have special meaning in NATS subject routing and would silently mismatch or broadcast to unintended subscribers. An empty tenant is also rejected.

Types

type AssociateRequest

type AssociateRequest struct {
	Domain       string            `json:"domain"`
	Associations []PackAssociation `json:"associations"`
}

AssociateRequest records that each context references each pack, so reclamation can later scope itself to an owner without walking the store. A filesystem node issues this on its write path; only the association WRITE crosses the wire, because reclamation needs the durable index and the blob store together and therefore runs blobgw-side.

type AssociateResponse

type AssociateResponse struct {
	Error        string `json:"error,omitempty"`
	Obliterating bool   `json:"obliterating,omitempty"`
}

AssociateResponse is returned after an Associate RPC. Error is non-empty on failure. Obliterating is set when the batch was refused because a named pack is being reclaimed — the caller must treat its write as failed and retry, which is distinct from a transport or backend error.

type ChunkLocation

type ChunkLocation struct {
	ChunkHash string  `json:"chunk_hash"`
	PackRef   PackRef `json:"pack_ref"`
}

ChunkLocation pairs a chunk's content hash with its PackRef. It is the unit a RecordRequest conveys — one entry per chunk that has been packed and uploaded, mirroring the pack_manifest(domain, chunk_hash, pack_hash, offset, length) row.

type Codec

type Codec interface {
	Marshal(v any) ([]byte, error)
	Unmarshal(data []byte, v any) error
}

Codec is the swappable serialisation interface for control-plane messages. The committed target format is protobuf (see controlplane.proto and ADR-001 §2.5). JSONCodec is the default until protoc-gen-go is in the build chain.

var DefaultCodec Codec = JSONCodec{}

DefaultCodec is the codec used when callers do not supply one explicitly. Swap this (or pass a Codec to your RPC layer) to switch to protobuf once protoc-gen-go is available.

type JSONCodec

type JSONCodec struct{}

JSONCodec marshals/unmarshals using encoding/json. It is the interim default pending the protobuf migration; see the package doc.

func (JSONCodec) Marshal

func (JSONCodec) Marshal(v any) ([]byte, error)

Marshal serialises v to JSON.

func (JSONCodec) Unmarshal

func (JSONCodec) Unmarshal(data []byte, v any) error

Unmarshal deserialises JSON data into v.

type LookupBatchRequest

type LookupBatchRequest struct {
	Domain      string   `json:"domain"`
	ChunkHashes []string `json:"chunk_hashes"`
}

LookupBatchRequest asks blobgw to resolve a batch of chunk hashes to their pack locations within Domain. Chunk hashes not present in the index are simply absent from the response map (they are novel chunks that the caller must pack and upload).

Batch size: callers MUST split slices longer than MaxChunkHashesPerBatch across multiple requests to stay within the NATS 1 MB payload limit.

type LookupBatchResponse

type LookupBatchResponse struct {
	Locations map[string]PackRef `json:"locations"`
	Error     string             `json:"error,omitempty"`
}

LookupBatchResponse carries the resolved chunk locations. Locations contains only the hashes that were found in the index. Error is non-empty when the server encountered a fatal error; on error, Locations may be nil or partial.

type ObliterateContextRequest

type ObliterateContextRequest struct {
	Domain  string `json:"domain"`
	Context string `json:"context"`
}

ObliterateContextRequest asks blobgw to reclaim the bytes an owner context held, using the mark-based sequence. The node cannot do this itself: it needs the durable association index and blob deletion authority at once.

type ObliterateContextResponse

type ObliterateContextResponse struct {
	Error                string `json:"error,omitempty"`
	PacksConsidered      int    `json:"packs_considered,omitempty"`
	PacksReclaimed       int    `json:"packs_reclaimed,omitempty"`
	PacksStillReferenced int    `json:"packs_still_referenced,omitempty"`
	PacksHeldByOther     int    `json:"packs_held_by_other,omitempty"`
	PacksSkippedError    int    `json:"packs_skipped_error,omitempty"`
	BytesReclaimed       int64  `json:"bytes_reclaimed,omitempty"`
}

ObliterateContextResponse reports what a reclamation pass did. The counters mirror snapshot.ReclaimResult so an operator sees the same numbers on both sides of the wire.

type PackAssociation

type PackAssociation struct {
	PackHash string `json:"pack_hash"`
	Context  string `json:"context"`
}

PackAssociation binds one pack to one owning context. It mirrors snapshot.PackAssociation on the wire.

type PackRef

type PackRef struct {
	PackHash string `json:"pack_hash"`
	Offset   int64  `json:"offset"`
	Size     int64  `json:"size"`
}

PackRef locates a content-addressed chunk within a pack blob. It is the value half of the dedup mapping (chunk hash → where the chunk physically lives in the object store). The blobgw server maps this to and from snapshot.PackRef; the mlfs client maps it to its own local representation.

type PresignBatchRequest

type PresignBatchRequest struct {
	Domain     string    `json:"domain"`
	Op         PresignOp `json:"op"`
	Keys       []string  `json:"keys"`
	TTLSeconds int32     `json:"ttl_seconds"`
}

PresignBatchRequest asks blobgw to mint a batch of presigned S3 URLs for the given object keys within Domain. Op selects GET (read) or PUT (write). TTLSeconds is the requested validity duration; blobgw may cap it based on the credential impl (e.g. AWS STS session limit — see ADR-001 §2.9).

Batch size: callers MUST split slices longer than MaxPresignKeysPerBatch across multiple requests to stay within the NATS 1 MB payload limit.

type PresignBatchResponse

type PresignBatchResponse struct {
	URLs          map[string]string `json:"urls"`
	ExpiresAtUnix int64             `json:"expires_at_unix"`
	Error         string            `json:"error,omitempty"`
}

PresignBatchResponse carries the minted presigned URLs. URLs maps each requested key to its presigned URL. ExpiresAtUnix is the Unix timestamp (seconds) at which all URLs in this batch expire; the node should schedule a refresh before this time (refresh-on-expiry is required by the AWS STS impl — see ADR-001 §2.9). Error is non-empty on failure.

type PresignOp

type PresignOp string

PresignOp is the HTTP verb a presigned URL will be valid for.

Codec migration note: the current JSONCodec encodes PresignOp as its string value ("GET" or "PUT"). The .proto definition declares PresignOp as an enum (GET=1, PUT=2); switching to a ProtobufCodec changes the wire representation from the string name to an integer. A rolling upgrade requires all nodes to adopt the new codec simultaneously, or the proto enum must be annotated to preserve string-name encoding (e.g. via protojson or a custom codec). Plan the codec migration as a coordinated cutover, not a gradual rollout, to avoid mixed-codec clusters misrouting presign requests.

const (
	// PresignGet mints a presigned URL that authorises a GET (read) of the
	// object. Used by mlfs nodes fetching pack blobs from the tenant S3 bucket.
	PresignGet PresignOp = "GET"

	// PresignPut mints a presigned URL that authorises a PUT (write) of the
	// object. Used by mlfs nodes uploading newly assembled pack blobs.
	PresignPut PresignOp = "PUT"
)

type PurgePacksRequest

type PurgePacksRequest struct {
	Domain     string   `json:"domain"`
	PackHashes []string `json:"pack_hashes"`
}

PurgePacksRequest asks blobgw to remove every index entry in Domain that points to one of the given pack hashes. GC calls this after it reclaims a pack blob so the index never hands out a reference to collected bytes. Purging an absent pack is a no-op (idempotent).

type PurgePacksResponse

type PurgePacksResponse struct {
	Error string `json:"error,omitempty"`
}

PurgePacksResponse is returned by blobgw after a PurgePacks RPC. Error is non-empty on failure.

type RecordRequest

type RecordRequest struct {
	Domain    string          `json:"domain"`
	Locations []ChunkLocation `json:"locations"`
}

RecordRequest asks blobgw to persist a batch of chunk locations in Domain. This is called after pack blobs have been durably uploaded to S3 (ADR-001 §4 invariant I1: durability before pointer). Recording is idempotent — first-writer-wins at the chunk_hash primary key.

Batch size: callers MUST split slices longer than MaxChunkHashesPerBatch across multiple requests.

type RecordResponse

type RecordResponse struct {
	Error string `json:"error,omitempty"`
}

RecordResponse is returned by blobgw after a Record RPC. Error is non-empty on failure.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL