Skip to content

feat: PR2 — provider integration, invariants, fallback reconnect gating#6

Open
mrmidi wants to merge 1 commit into
pr1c/teardown-ownershipfrom
pr2/provider-integration
Open

feat: PR2 — provider integration, invariants, fallback reconnect gating#6
mrmidi wants to merge 1 commit into
pr1c/teardown-ownershipfrom
pr2/provider-integration

Conversation

@mrmidi

@mrmidi mrmidi commented Jul 20, 2026

Copy link
Copy Markdown
Owner

PR2 — Wrapper/Provider Integration

Base: pr1c/teardown-ownership (stacked PR chain)
Files: 6 — 468+/63-

Provider invariants

  • ProviderInvariant enum: multipleNativeClients, incompleteNativeTeardown
  • Logged once per type via logInvariantOnce()
  • Checked via native status after teardown (liveClients > 1, activeOperations != 0)

Bridge lifecycle safety

  • replaceWebSocketClient returns Bool, accepts expectedGeneration
  • Validates generation BEFORE detaching and BEFORE exposing
  • Rejects new-client installation during shutdown; removal always allowed
  • WebsocketClientBridge has one-shot lifecycle latch (created→started→stopped)
  • Once stop() wins, start() is permanently rejected — no resurrection
  • startWebSocket uses stopCurrent: true, stopOrigin: .swiftReconnect

Packet read ownership

  • PacketReadToken(session:generation:) with process-lifetime tunnelSession
  • Exactly one process-wide pending read (pendingReadToken == nil guard)
  • Only the owning callback clears the slot
  • Stale callback restarts current generation's read loop
  • stopTunnel does NOT clear pending-read state

Final native status preservation

  • lastNativeStatus captured AFTER stop/join (not before)
  • currentSnapshot() queries native status outside stateLock
  • Falls back to saved status when no live client attached

TunnelRuntimeSnapshot — all PR1A–1C fields

16 new fields: socket buffers, lifecycle counters, queue diagnostics, disconnect code/origin, active operations, stop cleanup status

VPNService fallback reconnect

  • Cancellable Task<Void, Never>? (cancelled on connected/connecting/reasserting/disconnect)
  • FallbackReconnectDecision: doNotReconnect / reconnectNow / recheckAfter
  • Recheck loop for fresh heartbeat (not single evaluation)
  • Typed heartbeat fields (localStopInitiator, nativeDisconnectCode, nativeStopOrigin)
  • Controlled-stop check uses correct raw values (app_disconnect, system_stop)
  • Budget off-by-one fixed (N allows N attempts)
  • Rechecks NE status after sleeping

Heartbeat typed fields

Optional backward-compatible fields on TunnelProviderHeartbeat. Old persisted heartbeats still decode.

Verification

  • Xcode Debug build: succeeded
  • Tests: 27/27 passed (3 new PR2 tests: enum mapping, typed heartbeat, backward compat)

Stack

PR-1  Measurement safety
PR0   Production native build
PR1A  Native fixed baseline
PR1B  Queue and batch bounds
PR1C  Native teardown ownership   (base)
PR2   Provider integration        ← this PR
PR3   C-based binary flight recorder

@mrmidi
mrmidi force-pushed the pr2/provider-integration branch 4 times, most recently from 99308c4 to a74e27f Compare July 21, 2026 06:08
Provider invariants:
- ProviderInvariant enum (bridgeReplacementOverlap, multipleNativeClients,
  incompleteNativeTeardown). Logged once per type via logInvariantOnce().
- replaceWebSocketClient checks for overlap and incomplete teardown.
- No manual activeBridgeCount — uses existing native status fields.

Generation ownership for pending reads:
- pendingReadGeneration captured when calling readPackets.
- Stale batch (old generation) dropped after reconnect instead of
  being sent through the new bridge.

Final native status preservation:
- lastNativeStatus / lastNativeStatusGeneration stored before
  detaching a client. currentSnapshot() falls back to saved status
  when no live client is attached.

currentSnapshot() outside lock:
- Swift fields copied under stateLock, bridge reference copied,
  lock released, then native status queried outside the lock.

TunnelRuntimeSnapshot enrichment:
- All PR1A–1C native fields: socket buffers, lifecycle counters,
  queue diagnostics, disconnect code/origin, active operations,
  stop cleanup status.

Stop-origin plumbing:
- replaceWebSocketClient accepts stopOrigin parameter (default
  .swiftTunnelStop). All call sites use correct origin.

VPNService fallback reconnect:
- Cancellable Task replaces boolean guard. Cancelled on
  connected/connecting/reasserting/disconnect().
- FallbackReconnectDecision: doNotReconnect / reconnectNow /
  recheckAfter(seconds). Fresh heartbeat (< 45s) defers recheck.
- Rechecks NE status after sleeping.
- Budget off-by-one fixed: check before decrement, N allows N attempts.

Tests: 24/24 passed. Debug build succeeded.
@mrmidi
mrmidi force-pushed the pr2/provider-integration branch from a74e27f to 6bf7e82 Compare July 21, 2026 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant