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
39 changes: 38 additions & 1 deletion tests/chaos/child.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const (
envPrune = "CONDUIT_CHAOS_PRUNE"
envPaceMS = "CONDUIT_CHAOS_PACE_MS"
envPersistDelayMS = "CONDUIT_CHAOS_PERSIST_DELAY_MS"
envHoldAt = "CONDUIT_CHAOS_HOLD_AT"
envTotal = "CONDUIT_CHAOS_TOTAL"
envSnapshotK = "CONDUIT_CHAOS_SNAPSHOT_K"
envSnapshotPaceMS = "CONDUIT_CHAOS_SNAPSHOT_PACE_MS"
Expand All @@ -62,6 +63,7 @@ const (
markerFatal = "FATAL"
markerCorruptPo = "CORRUPT_POSITION"
markerHandoff = "HANDOFF" // Property 1/2: producer crossed the snapshot->stream boundary, see upstream.go/produceLoop
markerHeld = "HELD" // production ceiling reached, see chaosPlugin.holdAt (upstream.go)
markerAckOrder = "ACK_ORDER" // Property 3: per-key ack delivery ledger, see upstream.go/ackLoop
// markerSigtermDone is the SIGTERM/invariant-7 case's completion marker
// (sigterm_test.go, runChildSigterm) - deliberately distinct from
Expand Down Expand Up @@ -129,7 +131,21 @@ type childEnv struct {
// persistDelayMS overrides the persister debounce; 0 = default. See
// childConfig.persistDelayMS in harness.go for why this is configurable.
persistDelayMS int
total uint64

// holdAt caps how far this process's source may ever produce, making a
// mid-run SIGKILL's landing point bounded BY CONSTRUCTION instead of by
// the parent winning a race. Once position holdAt has been sent, the
// producer prints markerHeld and stops for good (it never reaches total),
// so no matter how long the parent is descheduled between deciding to
// kill and the signal actually landing, the durable committed watermark
// this process can leave behind is <= holdAt. See childConfig.holdAt in
// harness.go and chaosPlugin.holdAt in upstream.go for the full story.
//
// 0 (the default, and the value every scenario that does not need a
// bound leaves it at) disables the cap entirely - the producer behaves
// exactly as it did before this seam existed.
holdAt uint64
total uint64

// snapshotK/snapshotPaceMS: Property 1/2's two-phase producer knobs.
// snapshotK == 0 means "no distinct snapshot phase" (DBZ-1's original
Expand Down Expand Up @@ -181,6 +197,13 @@ func parseChildEnv() childEnv {
cfg.persistDelayMS = d
}

holdAt, err := strconv.ParseUint(os.Getenv(envHoldAt), 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: invalid %s: %v\n", markerFatal, envHoldAt, err)
os.Exit(exitBadArgs)
}
cfg.holdAt = holdAt

total, err := strconv.ParseUint(os.Getenv(envTotal), 10, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: invalid %s: %v\n", markerFatal, envTotal, err)
Expand Down Expand Up @@ -227,6 +250,19 @@ func parseChildEnv() childEnv {
fmt.Fprintf(os.Stderr, "%s: %s and %s are required\n", markerFatal, envDBDir, envUpstreamDir)
os.Exit(exitBadArgs)
}

// A cap at or above total bounds nothing - the producer stops at total on
// its own, so the cap would never be reached. The only legitimate use of
// a cap is strictly below total, where the child is crashable-only by
// design: it never reaches total, so the read loop never terminates and
// the process must be SIGKILLed (the same contract #2835's recovery
// child gives its holdAt seam). Any other combination could only surface
// as an opaque timeout or a silently pointless knob - fail loudly and
// immediately instead, like every other misconfiguration here.
if cfg.holdAt > 0 && cfg.total > 0 && cfg.holdAt >= cfg.total {
fmt.Fprintf(os.Stderr, "%s: %s (%d) must be below %s (%d) to bound anything\n", markerFatal, envHoldAt, cfg.holdAt, envTotal, cfg.total)
os.Exit(exitBadArgs)
}
return cfg
}

Expand Down Expand Up @@ -375,6 +411,7 @@ func buildChild(ctx context.Context, cfg childEnv) (*childBuilt, error) {
snapshotPaceMS: cfg.snapshotPaceMS,
numKeys: cfg.numKeys,
driftAt: cfg.driftAt,
holdAt: cfg.holdAt,
}

fetcher := staticFetcher{instance.Plugin: staticDispenser{source: plugin}}
Expand Down
21 changes: 21 additions & 0 deletions tests/chaos/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@ type childConfig struct {
// unchanged and just as strict. See sigkillCases.
persistDelayMS int

// holdAt caps how far a child's producer may ever go, making a mid-run
// SIGKILL's landing point bounded BY CONSTRUCTION instead of by the
// parent winning a race (the #2835 pattern, applied to this harness's
// chaosPlugin - see chaosPlugin.holdAt in upstream.go). Once position
// holdAt has been sent, the producer prints "HELD <pos>" (markerHeld)
// and stops for good: nothing past it is ever produced, so nothing past
// it can ever be acked or committed, however long the parent is
// descheduled between deciding to kill and the signal landing. A capped
// child also never reaches total, so its read loop blocks forever and it
// stays alive until the parent SIGKILLs it - the kill can never miss an
// exited process.
//
// 0 (the default, and the value every scenario that does not need a
// bound leaves it at) disables the cap entirely.
//
// The cap is a property of the FIRST (killed) child only: a RESUMED
// child must run to total unencumbered, so scenarios that set this also
// spawn their second child with it zeroed.
holdAt uint64

// snapshotK/snapshotPaceMS: DBZ-2 Property 1/2's two-phase producer
// knobs (see chaosPlugin's type doc, upstream.go). Zero values preserve
// DBZ-1's original single-phase behavior.
Expand Down Expand Up @@ -98,6 +118,7 @@ func (c childConfig) env() []string {
envDriftAt + "=" + strconv.FormatUint(c.driftAt, 10),
envSigtermMode + "=" + strconv.FormatBool(c.sigtermMode),
envPersistDelayMS + "=" + strconv.Itoa(c.persistDelayMS),
envHoldAt + "=" + strconv.FormatUint(c.holdAt, 10),
}
}

Expand Down
Loading
Loading