# Exercise 04 — The launch that stalls for exactly 1.5 seconds

**Failure mode:** lost wakeup, and a wakeup treated as a promise
**Language:** Swift 6 · **Runtime:** about 25 seconds · **Difficulty:** the one people get half right

---

## The prompt

> Two reports from the same codebase.
>
> One: "the app occasionally sits on the launch screen for about a second and a half, then
> carries on normally, and we cannot reproduce it."
>
> Two: "a background reader crashes taking an item from an empty queue, roughly once a
> week."
>
> Both come from the same misunderstanding of one primitive. Find it, explain why the two
> symptoms are the same bug, and fix both with one change.

Answer before you run anything: **when you signal a condition variable and nobody is
waiting, where does the signal go?**

---

## What you are given

| Path | What it is |
| --- | --- |
| `broken/handoff.swift` | The starting point. Both symptoms, deterministically. |
| `fixed/handoff.swift` | One correct repair. |
| `solution.patch` | The diff between the two, applies with `patch -p1`. |
| `check.sh` | Proves both broken symptoms and both fixed results. |

The program has three parts. **Part A** is the 1.5-second stall. **Part B** is the crash.
**Part C** is a reference measurement that is identical in both builds and exists to make
the distinction concrete.

---

## Reproduce the failure

```bash
cd exercises/04-lost-wakeup
swiftc -swift-version 6 -O broken/handoff.swift -o /tmp/handoff_broken
swiftc -swift-version 6 -O fixed/handoff.swift  -o /tmp/handoff_fixed
/tmp/handoff_broken
/tmp/handoff_fixed
```

Expected:

```
  partA waitedMs=1503 dataWasAlreadyPublished=true
  partB consumed=120 expected=120 wouldHaveCrashed=16 spuriousAbsorbed=0
  partC semaphorePermitSurvivedTheGap=true waitedMs=0
EX04 build=broken sleptThroughPublishedData=true partAWaitedMs=1503 wouldHaveCrashed=16 ... correct=false

  partA waitedMs=0 dataWasAlreadyPublished=true
  partB consumed=120 expected=120 wouldHaveCrashed=0 spuriousAbsorbed=18
  partC semaphorePermitSurvivedTheGap=true waitedMs=0
EX04 build=fixed sleptThroughPublishedData=false partAWaitedMs=0 wouldHaveCrashed=0 ... correct=true
```

**Timeout and safety.** Every wait has a finite deadline, every consumer loop has an
iteration cap, and a 60-second watchdog calls `_exit(75)`. Both builds exit **0**.

`wouldHaveCrashed` is a count, not a crash: the fixture counts the moment where real code
would have called `items.removeFirst()` on an empty array and trapped. It is written that
way so a validation run can measure the defect instead of dying from it.

---

## The evidence to collect

Part A is the cleanest evidence in this bundle, because it is **fully deterministic**: the
loader publishes and signals before any consumer exists, so there is no race and no luck.
The broken consumer waits its entire 1.5-second deadline for data that was already in
memory when it started. The fixed consumer waits 0 ms.

Part C is the control. It runs the *same* "signal before anyone waits" sequence through a
`DispatchSemaphore` instead, and the permit is still there:

```
partC semaphorePermitSurvivedTheGap=true waitedMs=0
```

So the distinction is not "signals get lost". It is:

| | Stores state? | Signal with no waiter |
| --- | --- | --- |
| `DispatchSemaphore` | Yes — a **count** | Permit is kept; the next waiter takes it |
| `NSCondition` / `pthread_cond_t` | **No** | The signal evaporates |

And the conclusion is neither of those on its own: **a permit is durable, an announcement
is not, and neither of them is your predicate.**

---

## Success criteria

- [ ] The broken build still shows both symptoms — you have not edited it.
- [ ] Your fixed build reports `sleptThroughPublishedData=false` and `partAWaitedMs` under
      250 on two consecutive runs.
- [ ] `wouldHaveCrashed=0`, with `spuriousAbsorbed` greater than zero — you absorbed the
      bad wakeups rather than removing them.
- [ ] `consumed=120 expected=120`: nothing was dropped on the floor.
- [ ] One change fixed both symptoms. If you wrote two unrelated fixes, you have not found
      the bug yet.

Run `./check.sh` to have all of that checked for you.

---

## Hints

<details>
<summary>Hint 1 — read the two waits side by side</summary>

Look at Part A's wait and Part B's wait. One of them never asks whether the thing it is
waiting for has already happened. The other asks once, and then believes the answer
forever. Those are the same mistake at two different moments: **before** the wait and
**after** it.
</details>

<details>
<summary>Hint 2 — what a wakeup actually means</summary>

A wakeup from a condition variable means *"the predicate may have changed"*. It does not
mean "the item is yours". Two things can make that distinction bite:

- the kernel is permitted to wake you for no reason at all (a spurious wakeup);
- another consumer may have been woken first and taken the item before you ran.

The second happens far more often than the first, and neither is rare enough to ignore.
</details>

<details>
<summary>Hint 3 — the shape of the repair</summary>

```swift
while !predicate {
    cond.wait(until: deadline)
}
```

Notice that this one line does two jobs: the check **before** the first wait, and the
re-check **after** every wakeup. That is why it fixes both symptoms at once.

Then ask a second question: when one state change can satisfy more than one waiter, is
`signal()` or `broadcast()` correct?
</details>

---

## Solution

<details>
<summary>Reveal the solution</summary>

### The repair

Part A — check before waiting:

```swift
cond.lock()
while configuration == nil {
    if !cond.wait(until: deadline) { break }      // finite deadline
}
let published = configuration != nil
cond.unlock()
```

Part B — re-check after every wakeup:

```swift
func take(deadline: Date) -> Int? {
    cond.lock(); defer { cond.unlock() }
    while items.isEmpty && !closed {
        if !cond.wait(until: deadline) { break }
        if items.isEmpty && !closed { spuriousAbsorbed += 1 }
    }
    guard !items.isEmpty else { return nil }
    return items.removeFirst()
}
```

And `publish` upgrades `signal()` to `broadcast()`.

### Why one change fixes both

**The shared predicate is the state. The condition variable is only a notification that
the state may have changed.** Everything follows from that:

- *Before* the first wait, the loop's condition is a check: if the thing already happened,
  the body never runs and you proceed immediately. A lost announcement becomes harmless,
  because you were never relying on the announcement — you were relying on the state.
- *After* each wakeup, the loop's condition is a re-check: you only leave when the
  predicate is actually true. A spurious wakeup, or an item another consumer took first,
  costs you one re-check instead of a crash.

The two symptoms were the same bug seen from two sides, which is why `while` is not a
style preference. `if` is a defect.

### `signal` versus `broadcast`

`signal()` wakes one waiter; `broadcast()` wakes all of them. Use `broadcast` when a
single state change can satisfy more than one waiter, or when waiters are waiting on
*different* predicates over the same lock — with `signal` you may wake the one waiter who
still cannot proceed while the one who could stays asleep. **Termination must always
broadcast**, or a consumer sleeps through the shutdown.

With a correct predicate loop an unnecessary wakeup costs a re-check. Without one it costs
a bug. That asymmetry is why `broadcast` is the safer default and `signal` is the
optimisation.

### The three primitives, and when each is right

| Need | Primitive | Why |
| --- | --- | --- |
| Wait for an arbitrary condition over shared state | Condition variable + predicate loop | The only one that can express "wait until X" |
| Count a bounded resource; hand off a permit | Semaphore | The count IS the state, and it is durable |
| Wait for a group of work to finish | `DispatchGroup` / task group | Structured, and cannot be miscounted |

Using a semaphore where you needed a predicate is the mirror-image mistake: the count
drifts out of step with the state it was standing in for, and now you have two sources of
truth.

### What the interviewer is listening for

- That you said "the predicate is the state" rather than "you need a while loop".
- That you connected the 1.5-second stall and the weekly crash **before** being told they
  were related.
- That you knew a wakeup can come from another consumer stealing the item, not only from
  the kernel.
- That you raised `signal` versus `broadcast` without prompting.

</details>

---

## Limitations

- `wouldHaveCrashed` counts a would-be trap rather than trapping. Real code calling
  `removeFirst()` on an empty array crashes, and the crash report points at the *consumer*,
  not at the missing `while`.
- Part B's count varies between runs (14–24 on the reference machine) because it depends on
  how often a signalled consumer loses the item to another. Its **floor** is structural —
  three consumers parked on an empty queue, rattled three times — so the check asserts
  "at least one", never a figure.
- Genuine kernel spurious wakeups are rare. This fixture injects them deliberately so the
  defect is reproducible in one run rather than once a month in production. The injection
  is the scaffolding; the missing predicate loop is the bug.
- Part A's 1.5 seconds is this fixture's deadline. Real code that waits with no deadline at
  all turns the same defect into a permanent hang.
