slot-state-machine.md 5.44 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# Slot State Machine Specification

## Overview

The slot module uses a single entry-level `ParkingMutex<EventState>` that holds
all per-entry state including waker registration. This design eliminates
stale-completion races by construction — there is no separate lock or atomic
guard whose ordering could allow stale results to leak across generations.

## State Variables

All fields are protected by a single `parking_lot::Mutex`:

```rust
struct EventState {
    last_triggered: Generation,              // highest completed generation
    active_generation: Option<Generation>,   // currently pending generation
    wakers: Vec<Waker>,                      // registered waiter wakers
    poisoned: BTreeMap<Generation, PoisonArc>, // poison history per generation
    retired: bool,                           // permanently unusable
}
```

## Lifecycle Phases

```
    begin_generation()
Idle ──────────────────> Active
  ^                        |
  |   finalize_completion()|
  |                        v
  +──────────────────── Completing
```

All transitions happen under a single lock acquisition.

### Idle

- `active_generation = None`
- `wakers` is empty (drained by prior `finalize_completion`)
- Entry is available for reuse via the free list

### Active

- `active_generation = Some(gen)`
- Waiters may register wakers via `poll_waiter`
- Only one generation can be active at a time

### Completing

- `finalize_completion` sets `last_triggered`, clears `active_generation`,
  stores poison (if applicable), drains wakers, then wakes them
- Transitions back to Idle

## Operations

### begin_generation

1. Acquire lock
2. Validate: no active generation, not retired, not overflowed
3. Compute `next = last_triggered + 1`
4. Drain stale wakers (`std::mem::take`)
5. Set `active_generation = Some(next)`
6. Release lock
7. Wake stale wakers (outside lock)

### finalize_completion(generation, completion)

1. Acquire lock
2. Validate: `active_generation == Some(generation)`
3. Set `last_triggered = generation`
4. Clear `active_generation`
5. Insert/remove from poison map
6. Drain wakers
7. Release lock
8. Wake all drained wakers (outside lock)

### register_local_waiter(generation)

1. Acquire lock
2. If `generation <= last_triggered`: return Ready or Poisoned
3. If `active_generation == Some(generation)`: return Pending
4. Otherwise: return InvalidGeneration error

### poll_waiter(observed_generation, cx)

1. Acquire lock
2. If `observed_generation <= last_triggered`: return completion result
3. If `active_generation.is_none()`: return "generation expired" error
4. Register waker with deduplication (`will_wake` check)
5. Return Pending

### try_to_poison(generation, poison)

1. Acquire lock
2. If `generation <= last_triggered`:
   - If `poisoned.contains_key(generation)`: return `AlreadyPoisoned`
   - Else: return `AlreadyCompleted` error
3. Validate: `active_generation == Some(generation)`
4. Set `last_triggered = generation`
5. Clear `active_generation`
6. Insert into poison map
7. Drain wakers
8. Release lock
9. Wake all drained wakers (outside lock)
10. Return `Poisoned`

This is equivalent to an atomic `status_for` + `finalize_completion(Poisoned)`,
eliminating the TOCTOU window when the two are called separately.

### retire

1. Acquire lock
2. Debug-assert: wakers list is empty (callers should ensure all waiters resolved before retirement)
3. Set `retired = true`, clear `active_generation`
4. Drain wakers (defensive, prevents silent hangs if invariant violated)
5. Release lock
6. Wake drained wakers (outside lock)

## Invariants

- **I1: Generation monotonicity**`last_triggered` only increases. Each
  `begin_generation` computes `last_triggered + 1`.

- **I2: Single completion per generation**`active_generation` guard ensures
  only one generation is active. `finalize_completion` validates
  `active_generation == Some(generation)`.

- **I3: Completion visibility** — Waiter resolution is determined by
  `observed_generation <= last_triggered` (success) plus the `poisoned`
  BTreeMap (error). Both are set under the same lock that the waiter reads.

- **I4: No stale completion leakage** — There is no stored completion value
  that could leak. Waiters resolve via generation comparison + poison map.
  `begin_generation` unconditionally drains stale wakers.

- **I5: No lost wakeups**`finalize_completion` sets `last_triggered` and
  drains wakers in the same lock scope. Any waiter that registered before
  the drain will be woken. Any waiter that polls after the drain will see
  `observed_generation <= last_triggered` and resolve immediately.

- **I6: Stale waiter resolution** — Waiters from generation N check
  `observed_generation <= last_triggered` on every poll. After generation N
  completes, this check succeeds regardless of what generation is currently
  active. `begin_generation` also flushes stale wakers defensively.

## Concurrency Rules

A single `parking_lot::Mutex` per entry serializes all state mutations. This
eliminates the need for:

- Atomic `waiter_count` (no conditional clearing)
- Atomic `completed` flag (redundant with lock-protected state)
- Separate slot-level `generation` counter (entry-level suffices)
- Manual waker deduplication across lock boundaries

The only concurrency pattern is: acquire lock, read/write state, release lock,
then wake drained wakers outside the lock (waker invocation only enqueues
tasks on the runtime, it does not poll them synchronously).