Project challenges / verified progress
Beacon: turn a program into a service

The engineering notebook

Keep recent results

How can concurrent checks share results without corrupting memory?

Loading statusStage 3 of 10

  • Workspace not ready
  • Agent not ready
Focus25:00
A small focus ritual

0 focus sessions completed. Every fourth session offers a longer break. Start each phase when you are ready.

Study time never unlocks verified lesson progress.

Loading...

Loading verified progress...

Loading GitHub account...
Phase 2 — Memory and history

Step 01 of 06

Learn the concept

The scheduler now creates a stream of results. Without storage, every result disappears after a log line. The first store is deliberately small: memory only, race-free, and bounded so success cannot become a leak.

ONE STORE TWO GUARDSMany checksresults arriveMutexshared state guardedsimpleChannelone owner receivesqueueRingoldest overwrittenbounded
The store can be protected by a mutex or owned by one goroutine through a channel. Beacon starts with a mutex because reads and writes are small, direct, and easy to reason about.
Step 01

The ideas this is made of

A data race means no one owns the truth

In Go, a data race occurs when two goroutines access the same memory at the same time, one writes, and there is no synchronization. The race detector instruments a run and reports these conflicts. Do not dismiss it because the printed results look fine. Races are timing-dependent; production supplies timing you did not test.

Mutexes make a critical section explicit

A sync.Mutex protects a small region where shared state is read or written. Lock, change the slice or map, unlock. Keep that region short and never call slow network code while holding it. A mutex is not primitive or shameful; it is a clear ownership rule for memory.

Channels move ownership instead of sharing it

With a channel design, check goroutines send results to one store goroutine. That goroutine alone mutates the buffer. This is excellent when writes are stream-shaped. Reads require request channels or snapshots, which can add ceremony. Choose the simpler rule your access pattern can defend.

A ring buffer turns memory into a fixed budget

An unbounded slice grows forever in a daemon. A ring buffer with capacity 1000 stores the newest thousand results and overwrites the oldest slot when full. That is not data loss by accident; it is a retention policy. Later SQLite keeps history. Memory is for the hot recent view.

A bounded integer ring
package main

import "fmt"

type Ring struct{ values []int; next int; full bool }

func (r *Ring) Add(v int) {
	if len(r.values) == 0 { return }
	r.values[r.next] = v
	r.next = (r.next + 1) % len(r.values)
	if r.next == 0 { r.full = true }
}

func main() {
	r := Ring{values: make([]int, 3)}
	for i := 1; i <= 5; i++ { r.Add(i) }
	fmt.Println(r.values, r.next, r.full)
}

After five writes into three slots, the ring still uses three integers of memory. The order needs a snapshot method, but the memory budget is already enforced.

Mutex or channel

ChoiceGood atWatch for

Mutex

Simple reads and writes

Holding lock too long

Channel owner

Serializing streams

Awkward snapshots

No guard

Nothing

Races and panics

What these are called on the job

  • Critical section — Code that reads or writes shared state while protected by a lock.

  • Race detector — Go instrumentation enabled with -race that reports unsynchronized shared memory access.

  • Ring buffer — A fixed-size buffer that wraps around and overwrites its oldest entries.