iop/packages/go/streamgate/ingress_snapshot.go
toki d182eafeff feat: stream evidence gate core - full implementation
- Stream evidence gate routing in edge config and runtime
- Ingress snapshot and allocation
- Recovery coordinator and plan
- Runtime contract for gate filters
- OpenAI-compatible request rebuilder and stream gate dispatcher
- Comprehensive tests for all new components
- Updated contracts and roadmap milestones
2026-07-26 20:48:53 +09:00

936 lines
28 KiB
Go

package streamgate
import (
"errors"
"sync"
"sync/atomic"
)
var (
// ErrIngressSnapshotLimitExceeded is returned when retained bytes exceed max bytes limit.
ErrIngressSnapshotLimitExceeded = errors.New("streamgate: ingress snapshot limit exceeded")
// ErrIngressSnapshotInvalidHandle is returned when a backing handle is unissued or invalid.
ErrIngressSnapshotInvalidHandle = errors.New("streamgate: ingress snapshot invalid backing handle")
// ErrIngressSnapshotAlreadyBuilt is returned when mutating a builder that has already built a snapshot.
ErrIngressSnapshotAlreadyBuilt = errors.New("streamgate: ingress snapshot builder already built")
// ErrIngressSnapshotDuplicateCanonical is returned when setting canonical source more than once.
ErrIngressSnapshotDuplicateCanonical = errors.New("streamgate: ingress snapshot duplicate canonical")
// ErrIngressSnapshotDuplicateTyped is returned when registering a typed view with a duplicate name.
ErrIngressSnapshotDuplicateTyped = errors.New("streamgate: ingress snapshot duplicate typed view name")
// ErrIngressSnapshotClosed is returned when accessing a snapshot or lease that has been closed.
ErrIngressSnapshotClosed = errors.New("streamgate: ingress snapshot closed")
// ErrIngressSnapshotZeroMaxBytes is returned when maxBytes parameter is zero.
ErrIngressSnapshotZeroMaxBytes = errors.New("streamgate: ingress snapshot max bytes must be greater than zero")
// ErrIngressSnapshotNilCanonical is returned when Build is called without a canonical handle set.
ErrIngressSnapshotNilCanonical = errors.New("streamgate: ingress snapshot missing canonical handle")
// ErrIngressSnapshotRebuildCancelled is returned when a rebuild reservation is cancelled before commit.
ErrIngressSnapshotRebuildCancelled = errors.New("streamgate: ingress snapshot rebuild reservation cancelled")
// ErrIngressSnapshotRebuildInvalidState is returned when operating on a released or invalid rebuild guard.
ErrIngressSnapshotRebuildInvalidState = errors.New("streamgate: ingress snapshot rebuild guard invalid state")
)
type rebuildOverflowError struct{}
func (rebuildOverflowError) Error() string {
return "streamgate: ingress snapshot rebuild allocation overflow"
}
func (rebuildOverflowError) Unwrap() error {
return ErrIngressSnapshotLimitExceeded
}
// ErrIngressSnapshotRebuildOverflow is returned when temporary rebuild allocation exceeds peak limit.
var ErrIngressSnapshotRebuildOverflow error = rebuildOverflowError{}
// globalOwnerCounter is a process-wide monotonic counter for builder owner IDs.
var globalOwnerCounter uint64
// BackingHandle identifies a payload blob owned by a specific builder.
// It is opaque and unexported fields prevent forging handles across builders.
type BackingHandle struct {
ownerID uint64
id uint64
}
// BackingHandleZero is the zero-value handle used as a sentinel.
var BackingHandleZero = BackingHandle{}
// isValid returns true when the handle was issued by a real builder.
func (h BackingHandle) isValid() bool {
return h.ownerID != 0
}
// TypedSemanticView describes a named typed semantic view of a backing blob.
type TypedSemanticView struct {
name string
handle BackingHandle
}
// Name returns the typed view name.
func (v TypedSemanticView) Name() string { return v.name }
// Handle returns the backing handle referencing the typed view payload.
func (v TypedSemanticView) Handle() BackingHandle { return v.handle }
// snapshotBacking is the builder-owned payload blob referenced by a BackingHandle.
type snapshotBacking struct {
payload []byte
}
// IngressSnapshotBuilder accumulates canonical and typed backing blobs
// under a byte limit, then transfers ownership of the backing store to an
// IngressSnapshot without copying payloads (canonical single-copy).
type IngressSnapshotBuilder struct {
mu sync.Mutex
ownerID uint64
maxBytes int64
retainedBytes int64
backings map[BackingHandle]*snapshotBacking
canonicalHdl BackingHandle
typedViews []TypedSemanticView
typedNames map[string]BackingHandle
nextID uint64
builderClosed bool
builderBuilt bool
terminalErr error
}
// NewIngressSnapshotBuilder creates a builder with the given byte limit.
// maxBytes must be positive.
func NewIngressSnapshotBuilder(maxBytes int64) (*IngressSnapshotBuilder, error) {
if maxBytes <= 0 {
return nil, ErrIngressSnapshotZeroMaxBytes
}
oid := atomic.AddUint64(&globalOwnerCounter, 1)
return &IngressSnapshotBuilder{
ownerID: oid,
maxBytes: maxBytes,
backings: make(map[BackingHandle]*snapshotBacking),
typedNames: make(map[string]BackingHandle),
nextID: 1,
}, nil
}
// IssueBackingHandle copies data into the builder-owned backing store,
// checks the byte limit before storing, and returns a handle bound to this builder.
// On overflow it transitions to a terminal failed state and returns BackingHandleZero
// along with ErrIngressSnapshotLimitExceeded.
func (b *IngressSnapshotBuilder) IssueBackingHandle(data []byte) (BackingHandle, error) {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.requireOpen("IssueBackingHandle"); err != nil {
return BackingHandle{}, err
}
return b.issueBackingHandleLocked(data, true)
}
// IssueOwnedBackingHandle transfers ownership of data into the builder without
// copying it. On success the caller must not mutate or retain data beyond the
// snapshot lifecycle. On error ownership remains with the caller.
func (b *IngressSnapshotBuilder) IssueOwnedBackingHandle(data []byte) (BackingHandle, error) {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.requireOpen("IssueOwnedBackingHandle"); err != nil {
return BackingHandle{}, err
}
return b.issueBackingHandleLocked(data, false)
}
// issueBackingHandleLocked accepts one backing after the caller has checked
// builder state. Must be called while holding b.mu.
func (b *IngressSnapshotBuilder) issueBackingHandleLocked(data []byte, copyData bool) (BackingHandle, error) {
size := int64(len(data))
if size > b.maxBytes-b.retainedBytes {
return BackingHandle{}, b.failLocked(ErrIngressSnapshotLimitExceeded)
}
hdl := BackingHandle{ownerID: b.ownerID, id: b.nextID}
b.nextID++
payload := data
if copyData {
payload = append([]byte(nil), data...)
}
b.backings[hdl] = &snapshotBacking{payload: payload}
b.retainedBytes += size
return hdl, nil
}
// SetCanonicalWithHandle associates the given builder-owned backing handle
// as the canonical source. The payload is not re-accepted; only the handle
// is linked to the canonical slot.
func (b *IngressSnapshotBuilder) SetCanonicalWithHandle(hdl BackingHandle) error {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.requireOpen("SetCanonicalWithHandle"); err != nil {
return err
}
if !b.validateHandle(hdl) {
return ErrIngressSnapshotInvalidHandle
}
if b.canonicalHdl.isValid() {
return ErrIngressSnapshotDuplicateCanonical
}
b.canonicalHdl = hdl
return nil
}
// AddTypedView registers a named typed semantic view that references a
// builder-owned backing handle. The payload is not re-accepted; only the
// handle is linked to the typed view.
func (b *IngressSnapshotBuilder) AddTypedView(name string, hdl BackingHandle) error {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.requireOpen("AddTypedView"); err != nil {
return err
}
if name == "" {
return errors.New("streamgate: typed view name is required")
}
if !b.validateHandle(hdl) {
return ErrIngressSnapshotInvalidHandle
}
if _, dup := b.typedNames[name]; dup {
return ErrIngressSnapshotDuplicateTyped
}
b.typedViews = append(b.typedViews, TypedSemanticView{name: name, handle: hdl})
b.typedNames[name] = hdl
return nil
}
// Build moves the backing store and metadata into a new IngressSnapshot
// without copying payloads. The builder is poisoned into a built state
// and all subsequent mutations return ErrIngressSnapshotAlreadyBuilt.
func (b *IngressSnapshotBuilder) Build() (*IngressSnapshot, error) {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.requireOpen("Build"); err != nil {
return nil, err
}
if !b.validateHandle(b.canonicalHdl) {
return nil, ErrIngressSnapshotNilCanonical
}
snap := &IngressSnapshot{
maxBytes: b.maxBytes,
backings: b.backings,
canonical: b.canonicalHdl,
typedViews: b.typedViews,
}
b.backings = nil
b.typedViews = nil
b.typedNames = nil
b.canonicalHdl = BackingHandle{}
b.retainedBytes = 0
b.builderBuilt = true
return snap, nil
}
// Close idempotently releases builder-owned backing memory.
// Already-built or already-closed builders are no-ops.
// A failed (terminal) builder releases memory but does not clear the error state.
func (b *IngressSnapshotBuilder) Close() {
b.mu.Lock()
defer b.mu.Unlock()
if b.builderClosed || b.builderBuilt {
return
}
for _, sb := range b.backings {
sb.payload = nil
}
b.backings = nil
b.typedViews = nil
b.typedNames = nil
b.canonicalHdl = BackingHandle{}
b.retainedBytes = 0
b.builderClosed = true
}
// requireOpen returns an error if the builder cannot accept the named operation.
// Priority: built > terminal overflow > closed.
func (b *IngressSnapshotBuilder) requireOpen(op string) error {
if b.builderBuilt {
return ErrIngressSnapshotAlreadyBuilt
}
if b.terminalErr != nil {
return b.terminalErr
}
if b.builderClosed {
return ErrIngressSnapshotClosed
}
return nil
}
// validateHandle returns true when the handle was issued by this builder.
func (b *IngressSnapshotBuilder) validateHandle(hdl BackingHandle) bool {
return hdl.isValid() && hdl.ownerID == b.ownerID && b.backings[hdl] != nil
}
// failLocked transitions the builder to a terminal failed state and releases
// all retained references. Must be called while holding b.mu.
func (b *IngressSnapshotBuilder) failLocked(err error) error {
b.terminalErr = err
b.builderClosed = true
for _, sb := range b.backings {
sb.payload = nil
}
b.backings = nil
b.typedViews = nil
b.typedNames = nil
b.canonicalHdl = BackingHandle{}
b.retainedBytes = 0
return err
}
// fitsIngressSnapshotPeak returns true when retained+reserved+expected fits in maxBytes
// without int64 overflow. Uses subtraction-based checks so that any non-fitting
// combination (including near-MaxInt64 inputs) is rejected deterministically.
func fitsIngressSnapshotPeak(maxBytes, retained, reserved, expected int64) bool {
if retained < 0 || reserved < 0 || expected < 0 {
return false
}
remaining := maxBytes - retained
if remaining < 0 {
return false
}
if reserved > remaining {
return false
}
remaining -= reserved
if expected > remaining {
return false
}
return true
}
// IngressSnapshot is the immutable result of a successful Build.
// It owns the backing store and provides defensive-copy accessors and rebuild guards.
type IngressSnapshot struct {
mu sync.RWMutex
maxBytes int64
reservedTempBytes int64
backings map[BackingHandle]*snapshotBacking
canonical BackingHandle
typedViews []TypedSemanticView
closed bool
activeGuards []*IngressSnapshotRebuildGuard
}
// Accessor returns a read-only accessor for the snapshot.
func (s *IngressSnapshot) Accessor() IngressSnapshotAccessor {
return IngressSnapshotAccessor{snap: s}
}
// Close closes the snapshot idempotently, fans out to all active rebuild guards,
// and releases backing references.
func (s *IngressSnapshot) Close() {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return
}
s.closeActiveGuardsLocked(nil)
s.closeLocked()
}
// closeLocked releases backing references, marks the snapshot closed,
// and clears the active guard registry. Must be called while holding s.mu.
func (s *IngressSnapshot) closeLocked() {
s.closed = true
s.backings = nil
s.reservedTempBytes = 0
s.activeGuards = nil
}
// closeActiveGuardsLocked terminates all active rebuild guards except the
// optionally excluded current guard. For each sibling, if it holds a committed
// rebuilt snapshot the snapshot is closed, then the guard is released.
// Must be called while holding s.mu.
func (s *IngressSnapshot) closeActiveGuardsLocked(current *IngressSnapshotRebuildGuard) {
guards := make([]*IngressSnapshotRebuildGuard, len(s.activeGuards))
copy(guards, s.activeGuards)
for _, g := range guards {
if g == current {
continue
}
g.mu.Lock()
if !g.released {
if g.state == RebuildStateCommitted && g.rebuiltSnapshot != nil {
g.rebuiltSnapshot.Close()
}
g.releaseLocked(RebuildStateReleased)
}
g.mu.Unlock()
}
}
// MaxBytes returns the maximum byte limit for this snapshot.
func (s *IngressSnapshot) MaxBytes() int64 {
s.mu.RLock()
defer s.mu.RUnlock()
return s.maxBytes
}
// ReservedTempBytes returns the sum of active temporary rebuild reservations.
func (s *IngressSnapshot) ReservedTempBytes() int64 {
s.mu.RLock()
defer s.mu.RUnlock()
return s.reservedTempBytes
}
// retainedCount returns the number of unique backing blobs. Must hold s.mu (write or RLock).
func (s *IngressSnapshot) retainedCount() int {
return len(s.backings)
}
// retainedBytes returns the sum of backing payload sizes. Must hold s.mu.
func (s *IngressSnapshot) retainedBytes() int64 {
var total int64
for _, sb := range s.backings {
total += int64(len(sb.payload))
}
return total
}
// hasBacking returns true when the backing store references the handle.
func (s *IngressSnapshot) hasBacking(hdl BackingHandle) bool {
_, ok := s.backings[hdl]
return ok
}
// RebuildGuardState represents the lifecycle state of a rebuild allocation guard.
type RebuildGuardState int
const (
RebuildStateReserved RebuildGuardState = iota
RebuildStateCommitted
RebuildStateCancelled
RebuildStateOverflow
RebuildStateReleased
)
func (st RebuildGuardState) String() string {
switch st {
case RebuildStateReserved:
return "reserved"
case RebuildStateCommitted:
return "committed"
case RebuildStateCancelled:
return "cancelled"
case RebuildStateOverflow:
return "overflow"
case RebuildStateReleased:
return "released"
default:
return "unknown"
}
}
// IngressSnapshotRebuildGuard coordinates temporary rebuild output memory reservations,
// two-phase pre/post peak byte checks, and canonical/typed/temp reference release lifecycle.
type IngressSnapshotRebuildGuard struct {
mu sync.Mutex
snap *IngressSnapshot
expectedTempBytes int64
reservedBytes int64
tempPayload []byte
tempHandle BackingHandle
canonicalRef BackingHandle
typedRefs []TypedSemanticView
state RebuildGuardState
released bool
rebuiltSnapshot *IngressSnapshot
}
// ReserveRebuild creates a two-phase rebuild allocation guard on the snapshot after checking
// pre-peak memory limits against maxBytes.
func (s *IngressSnapshot) ReserveRebuild(expectedTempBytes int64) (*IngressSnapshotRebuildGuard, error) {
if expectedTempBytes < 0 {
return nil, errors.New("streamgate: expected temporary bytes cannot be negative")
}
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil, ErrIngressSnapshotClosed
}
retained := s.retainedBytes()
if !fitsIngressSnapshotPeak(s.maxBytes, retained, s.reservedTempBytes, expectedTempBytes) {
return nil, ErrIngressSnapshotRebuildOverflow
}
s.reservedTempBytes += expectedTempBytes
guard := &IngressSnapshotRebuildGuard{
snap: s,
expectedTempBytes: expectedTempBytes,
reservedBytes: expectedTempBytes,
canonicalRef: s.canonical,
typedRefs: append([]TypedSemanticView(nil), s.typedViews...),
state: RebuildStateReserved,
}
s.activeGuards = append(s.activeGuards, guard)
return guard, nil
}
// Commit completes the rebuild allocation by accepting actual output payload,
// checking post-peak memory limits, and returning a dispatchable IngressSnapshot.
func (g *IngressSnapshotRebuildGuard) Commit(actualOutput []byte) (*IngressSnapshot, error) {
return g.CommitTyped("", actualOutput)
}
// CommitTyped completes the rebuild allocation with an optional typed view name for the output.
func (g *IngressSnapshotRebuildGuard) CommitTyped(typedName string, actualOutput []byte) (*IngressSnapshot, error) {
return g.commitTyped(typedName, actualOutput, true)
}
// CommitOwnedTyped transfers ownership of actualOutput into the committed
// snapshot without copying it. On success the caller must not mutate or retain
// actualOutput beyond the returned snapshot lifecycle. On error ownership
// remains with the caller.
func (g *IngressSnapshotRebuildGuard) CommitOwnedTyped(typedName string, actualOutput []byte) (*IngressSnapshot, error) {
return g.commitTyped(typedName, actualOutput, false)
}
func (g *IngressSnapshotRebuildGuard) commitTyped(typedName string, actualOutput []byte, copyOutput bool) (*IngressSnapshot, error) {
if g == nil {
return nil, ErrIngressSnapshotRebuildInvalidState
}
g.snap.mu.Lock()
g.mu.Lock()
defer g.mu.Unlock()
defer g.snap.mu.Unlock()
// Guard state check first for stable errors on terminal guards.
if g.released || g.state != RebuildStateReserved {
switch g.state {
case RebuildStateOverflow:
return nil, ErrIngressSnapshotRebuildOverflow
case RebuildStateCancelled:
return nil, ErrIngressSnapshotRebuildCancelled
case RebuildStateCommitted:
return nil, errors.New("streamgate: rebuild reservation already committed")
case RebuildStateReleased:
return nil, ErrIngressSnapshotClosed
default:
return nil, ErrIngressSnapshotRebuildInvalidState
}
}
if g.snap.closed {
g.releaseLocked(RebuildStateReleased)
return nil, ErrIngressSnapshotClosed
}
actualSize := int64(len(actualOutput))
retained := g.snap.retainedBytes()
otherReserved := g.snap.reservedTempBytes - g.reservedBytes
if otherReserved < 0 {
otherReserved = 0
}
if !fitsIngressSnapshotPeak(g.snap.maxBytes, retained, otherReserved, actualSize) {
// Post-peak overflow: release current guard, fan out to all active siblings,
// close parent snapshot.
g.releaseLocked(RebuildStateOverflow)
g.snap.closeActiveGuardsLocked(g)
g.snap.closeLocked()
return nil, ErrIngressSnapshotRebuildOverflow
}
// Issue new backing handle for temporary rebuild output
oid := atomic.AddUint64(&globalOwnerCounter, 1)
newHdl := BackingHandle{ownerID: oid, id: 1}
payload := actualOutput
if copyOutput {
payload = append([]byte(nil), actualOutput...)
}
sb := &snapshotBacking{payload: payload}
// Build new backings map
newBackings := make(map[BackingHandle]*snapshotBacking, len(g.snap.backings)+1)
for h, b := range g.snap.backings {
newBackings[h] = b
}
newBackings[newHdl] = sb
// Build new typed views list
newTypedViews := make([]TypedSemanticView, 0, len(g.snap.typedViews)+1)
newTypedViews = append(newTypedViews, g.snap.typedViews...)
if typedName != "" {
newTypedViews = append(newTypedViews, TypedSemanticView{name: typedName, handle: newHdl})
}
rebuilt := &IngressSnapshot{
maxBytes: g.snap.maxBytes,
backings: newBackings,
canonical: g.snap.canonical,
typedViews: newTypedViews,
}
// Unreserve temporary bytes from parent snapshot
g.snap.reservedTempBytes -= g.reservedBytes
if g.snap.reservedTempBytes < 0 {
g.snap.reservedTempBytes = 0
}
g.reservedBytes = 0
g.tempPayload = sb.payload
g.tempHandle = newHdl
g.rebuiltSnapshot = rebuilt
g.state = RebuildStateCommitted
return rebuilt, nil
}
// Cancel releases the rebuild reservation and drops references in defined order (temp -> canonical -> typed).
func (g *IngressSnapshotRebuildGuard) Cancel() error {
if g == nil {
return nil
}
g.snap.mu.Lock()
g.mu.Lock()
defer g.mu.Unlock()
defer g.snap.mu.Unlock()
if g.released {
return nil
}
g.releaseLocked(RebuildStateCancelled)
return nil
}
// Close idempotently releases the rebuild guard resources.
func (g *IngressSnapshotRebuildGuard) Close() {
if g == nil {
return
}
g.snap.mu.Lock()
g.mu.Lock()
defer g.mu.Unlock()
defer g.snap.mu.Unlock()
if g.released {
return
}
g.releaseLocked(RebuildStateReleased)
}
// releaseLocked releases references in defined order: temporary -> canonical -> typed.
// Must be called while holding both g.snap.mu and g.mu.
func (g *IngressSnapshotRebuildGuard) releaseLocked(st RebuildGuardState) {
if g.released {
return
}
g.state = st
g.released = true
// Step 1: Release temporary reference
g.tempPayload = nil
g.tempHandle = BackingHandle{}
// Step 2: Release canonical reference
g.canonicalRef = BackingHandle{}
// Step 3: Release typed references
g.typedRefs = nil
// Step 4: Release rebuilt snapshot reference (committed path)
g.rebuiltSnapshot = nil
// Unreserve temporary bytes from parent snapshot
if g.reservedBytes > 0 && g.snap != nil {
g.snap.reservedTempBytes -= g.reservedBytes
if g.snap.reservedTempBytes < 0 {
g.snap.reservedTempBytes = 0
}
g.reservedBytes = 0
}
// Unregister from parent's active guard registry
if g.snap != nil {
snap := g.snap
for i, ag := range snap.activeGuards {
if ag == g {
snap.activeGuards = append(snap.activeGuards[:i], snap.activeGuards[i+1:]...)
break
}
}
}
}
// State returns the current lifecycle state of the guard.
func (g *IngressSnapshotRebuildGuard) State() RebuildGuardState {
if g == nil {
return RebuildStateReleased
}
g.mu.Lock()
defer g.mu.Unlock()
return g.state
}
// IsReleased returns true if the guard has been released or cancelled.
func (g *IngressSnapshotRebuildGuard) IsReleased() bool {
if g == nil {
return true
}
g.mu.Lock()
defer g.mu.Unlock()
return g.released
}
// ReservedBytes returns the number of expected temporary bytes currently reserved.
func (g *IngressSnapshotRebuildGuard) ReservedBytes() int64 {
if g == nil {
return 0
}
g.mu.Lock()
defer g.mu.Unlock()
return g.reservedBytes
}
// RebuiltSnapshot returns the committed rebuilt IngressSnapshot held by this guard,
// or nil if the guard has not committed successfully (overflow, cancelled, released).
func (g *IngressSnapshotRebuildGuard) RebuiltSnapshot() *IngressSnapshot {
if g == nil {
return nil
}
g.mu.Lock()
defer g.mu.Unlock()
return g.rebuiltSnapshot
}
// DispatchableSnapshot returns the committed rebuilt IngressSnapshot, or an error if not committed or released.
func (g *IngressSnapshotRebuildGuard) DispatchableSnapshot() (*IngressSnapshot, error) {
if g == nil {
return nil, ErrIngressSnapshotRebuildInvalidState
}
g.mu.Lock()
defer g.mu.Unlock()
if g.state == RebuildStateCommitted && g.rebuiltSnapshot != nil {
return g.rebuiltSnapshot, nil
}
switch g.state {
case RebuildStateOverflow:
return nil, ErrIngressSnapshotRebuildOverflow
case RebuildStateCancelled:
return nil, ErrIngressSnapshotRebuildCancelled
case RebuildStateReleased:
return nil, ErrIngressSnapshotClosed
default:
return nil, ErrIngressSnapshotRebuildInvalidState
}
}
// Accessor returns an IngressSnapshotAccessor for the committed snapshot, or a closed accessor if released.
func (g *IngressSnapshotRebuildGuard) Accessor() IngressSnapshotAccessor {
snap, err := g.DispatchableSnapshot()
if err != nil {
return IngressSnapshotAccessor{snap: &IngressSnapshot{closed: true}}
}
return snap.Accessor()
}
// HasTempReference returns true if the temporary handle/payload reference is active.
func (g *IngressSnapshotRebuildGuard) HasTempReference() bool {
if g == nil {
return false
}
g.mu.Lock()
defer g.mu.Unlock()
return g.tempHandle.isValid() || len(g.tempPayload) > 0
}
// HasCanonicalReference returns true if the canonical reference is active.
func (g *IngressSnapshotRebuildGuard) HasCanonicalReference() bool {
if g == nil {
return false
}
g.mu.Lock()
defer g.mu.Unlock()
return g.canonicalRef.isValid()
}
// HasTypedReferences returns true if typed references are active.
func (g *IngressSnapshotRebuildGuard) HasTypedReferences() bool {
if g == nil {
return false
}
g.mu.Lock()
defer g.mu.Unlock()
return len(g.typedRefs) > 0
}
// IngressSnapshotAccessor provides read-only, defensive-copy access to an IngressSnapshot.
type IngressSnapshotAccessor struct {
snap *IngressSnapshot
}
// MaxBytes returns the maximum byte limit configured for the snapshot.
func (a IngressSnapshotAccessor) MaxBytes() int64 {
return a.snap.MaxBytes()
}
// ReservedTempBytes returns the active temporary reserved bytes on the snapshot.
func (a IngressSnapshotAccessor) ReservedTempBytes() int64 {
return a.snap.ReservedTempBytes()
}
// IsClosed returns true if the snapshot has been closed.
func (a IngressSnapshotAccessor) IsClosed() bool {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
return a.snap.closed
}
// RetainedCount returns the number of unique backing blobs.
func (a IngressSnapshotAccessor) RetainedCount() int {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
return a.snap.retainedCount()
}
// RetainedBytes returns the sum of backing payload sizes.
func (a IngressSnapshotAccessor) RetainedBytes() int64 {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
return a.snap.retainedBytes()
}
// HasCanonical returns true if a canonical handle was set during build.
func (a IngressSnapshotAccessor) HasCanonical() bool {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
return a.snap.canonical.isValid()
}
// Canonical returns a defensive copy of the canonical payload, or an error.
func (a IngressSnapshotAccessor) Canonical() ([]byte, error) {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
if a.snap.closed {
return nil, ErrIngressSnapshotClosed
}
if !a.snap.canonical.isValid() {
return nil, ErrIngressSnapshotInvalidHandle
}
sb, ok := a.snap.backings[a.snap.canonical]
if !ok {
return nil, ErrIngressSnapshotInvalidHandle
}
out := make([]byte, len(sb.payload))
copy(out, sb.payload)
return out, nil
}
// CanonicalAlias returns a read-only alias of the canonical backing. The alias
// is valid only while the snapshot is open and must never be mutated. Callers
// that need independent ownership must use Canonical instead.
func (a IngressSnapshotAccessor) CanonicalAlias() ([]byte, error) {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
if a.snap.closed {
return nil, ErrIngressSnapshotClosed
}
if !a.snap.canonical.isValid() {
return nil, ErrIngressSnapshotInvalidHandle
}
sb, ok := a.snap.backings[a.snap.canonical]
if !ok {
return nil, ErrIngressSnapshotInvalidHandle
}
return sb.payload, nil
}
// TypedView returns a defensive copy of the typed view payload by name, or an error.
func (a IngressSnapshotAccessor) TypedView(name string) ([]byte, error) {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
if a.snap.closed {
return nil, ErrIngressSnapshotClosed
}
for _, v := range a.snap.typedViews {
if v.name == name {
sb, ok := a.snap.backings[v.handle]
if !ok {
return nil, ErrIngressSnapshotInvalidHandle
}
out := make([]byte, len(sb.payload))
copy(out, sb.payload)
return out, nil
}
}
return nil, ErrIngressSnapshotInvalidHandle
}
// TypedViewAlias returns a read-only alias of a typed-view backing. The alias
// is valid only while the snapshot is open and must never be mutated. Callers
// that need independent ownership must use TypedView instead.
func (a IngressSnapshotAccessor) TypedViewAlias(name string) ([]byte, error) {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
if a.snap.closed {
return nil, ErrIngressSnapshotClosed
}
for _, v := range a.snap.typedViews {
if v.name == name {
sb, ok := a.snap.backings[v.handle]
if !ok {
return nil, ErrIngressSnapshotInvalidHandle
}
return sb.payload, nil
}
}
return nil, ErrIngressSnapshotInvalidHandle
}
// TypedViewNames returns the names of all typed views in registration order.
func (a IngressSnapshotAccessor) TypedViewNames() []string {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
out := make([]string, 0, len(a.snap.typedViews))
for _, v := range a.snap.typedViews {
out = append(out, v.name)
}
return out
}
// RetainedBackings returns the set of backing handles retained by the snapshot.
func (a IngressSnapshotAccessor) RetainedBackings() []BackingHandle {
a.snap.mu.RLock()
defer a.snap.mu.RUnlock()
out := make([]BackingHandle, 0, len(a.snap.backings))
for hdl := range a.snap.backings {
out = append(out, hdl)
}
return out
}