|
| 1 | +# YouTube Video Script: Implementing `reset()` for the Redis Adapter |
| 2 | + |
| 3 | +**Project**: [Ganesha](https://github.com/ackintosh/ganesha) — PHP Circuit Breaker library |
| 4 | +**Task**: Implement the `reset()` method in the Redis storage adapter |
| 5 | +**Estimated duration**: 15–20 minutes |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Scene 1 — Introduction (camera on screen, terminal open) |
| 10 | + |
| 11 | +> "Hey everyone, welcome back. Today I'm going to do some coding on an open-source PHP library called **Ganesha** — which is a circuit breaker implementation I've been maintaining. |
| 12 | +
|
| 13 | +> If you're not familiar with the circuit breaker pattern — it's a design pattern used in distributed systems to prevent cascading failures. Think of it like an electrical circuit breaker: when too many failures happen, the circuit 'trips' and stops sending requests to a broken service, giving it time to recover. |
| 14 | +
|
| 15 | +> Ganesha supports two strategies for detecting failures: a **Rate strategy**, which tracks the failure rate as a percentage over a sliding time window, and a **Count strategy**, which simply counts how many failures have occurred. |
| 16 | +
|
| 17 | +> Today's task is pretty focused. I found a TODO comment in the codebase, and I want to take care of it." |
| 18 | +
|
| 19 | +--- |
| 20 | + |
| 21 | +## Scene 2 — Finding the TODO (open `src/Ganesha/Storage/Adapter/Redis.php`) |
| 22 | + |
| 23 | +> "Let me open the Redis adapter — this is the storage backend that uses Redis to persist circuit breaker state." |
| 24 | +
|
| 25 | +*Navigate to [src/Ganesha/Storage/Adapter/Redis.php](src/Ganesha/Storage/Adapter/Redis.php), scroll to the `reset()` method around line 147.* |
| 26 | + |
| 27 | +> "Here it is — line 148. The `reset()` method has a TODO comment: `Implement reset() method`. So right now, if you call `reset()` on Ganesha when using the Redis adapter, nothing actually happens. That's a bug we should fix. |
| 28 | +
|
| 29 | +> The `reset()` method is supposed to clear all circuit breaker state — failure counts, statuses, everything. This is useful in tests, or when you want to manually recover a tripped circuit. |
| 30 | +
|
| 31 | +> Let me look at how the other adapters implement this to get a sense of what we need to do." |
| 32 | +
|
| 33 | +--- |
| 34 | + |
| 35 | +## Scene 3 — Exploring other adapters (open `src/Ganesha/Storage/Adapter/Memcached.php`) |
| 36 | + |
| 37 | +*Open [src/Ganesha/Storage/Adapter/Memcached.php](src/Ganesha/Storage/Adapter/Memcached.php) and the APCu adapter.* |
| 38 | + |
| 39 | +> "Looking at the other adapters... okay, I can see the pattern. The reset method needs to delete all keys that Ganesha has created in the storage backend. |
| 40 | +
|
| 41 | +> Now let me think about what data Redis is actually storing for each service." |
| 42 | +
|
| 43 | +--- |
| 44 | + |
| 45 | +## Scene 4 — Understanding the Redis data structure |
| 46 | + |
| 47 | +*Stay on [src/Ganesha/Storage/Adapter/Redis.php](src/Ganesha/Storage/Adapter/Redis.php), walk through the methods.* |
| 48 | + |
| 49 | +> "Let me trace through the code. When a failure is recorded, the `increment()` method adds a timestamped entry to a **sorted set** in Redis — the key is the service name. So for a service called `payment-api`, we'd have a sorted set at a key like `ganesha_payment-api_failure`. |
| 50 | +
|
| 51 | +> Then `saveStatus()` saves the circuit status — tripped or calmed down — as a plain string key. |
| 52 | +
|
| 53 | +> So to reset, we need to delete both the sorted set and the status key for every service that Ganesha is tracking. |
| 54 | +
|
| 55 | +> The tricky part is: how do we know which keys belong to Ganesha? Let me look at the `StorageKeys` class." |
| 56 | +
|
| 57 | +*Open [src/Ganesha/Storage/StorageKeys.php](src/Ganesha/Storage/StorageKeys.php).* |
| 58 | + |
| 59 | +> "Okay, so Ganesha uses a configurable prefix for its keys — by default it's `ganesha_`. That means we can use Redis's `KEYS` command or better, `SCAN`, to find all keys matching `ganesha_*` and delete them. |
| 60 | +
|
| 61 | +> I'll use `SCAN` instead of `KEYS` because `KEYS` blocks the Redis server while it scans — that can cause latency spikes in production. `SCAN` is non-blocking and iterates in batches. This is an important distinction for production systems." |
| 62 | +
|
| 63 | +--- |
| 64 | + |
| 65 | +## Scene 5 — Writing the implementation |
| 66 | + |
| 67 | +*Edit [src/Ganesha/Storage/Adapter/Redis.php](src/Ganesha/Storage/Adapter/Redis.php), replace the `reset()` method.* |
| 68 | + |
| 69 | +> "Alright, let me write this. I'll use the `SCAN` command to iterate over all matching keys and then delete them in batches." |
| 70 | +
|
| 71 | +```php |
| 72 | +public function reset(): void |
| 73 | +{ |
| 74 | + $cursor = null; |
| 75 | + $prefix = $this->configuration->storageKeys()->prefix(); |
| 76 | + |
| 77 | + do { |
| 78 | + $result = $this->redis->scan($cursor, $prefix . '*'); |
| 79 | + if ($result === false) { |
| 80 | + break; |
| 81 | + } |
| 82 | + [$cursor, $keys] = $result; |
| 83 | + if (!empty($keys)) { |
| 84 | + $this->redis->del($keys); |
| 85 | + } |
| 86 | + } while ($cursor !== 0); |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +> "Let me walk through this. We start with a `null` cursor, which tells Redis to begin the scan from the start. On each iteration, `SCAN` returns a new cursor and a batch of matching keys. We delete that batch, then loop again until the cursor comes back as zero — that means we've gone through the entire keyspace. |
| 91 | +
|
| 92 | +> The `prefix()` method gives us the configured key prefix, so we only delete Ganesha's own keys and nothing else in Redis. That's important — we don't want to accidentally wipe unrelated data." |
| 93 | +
|
| 94 | +--- |
| 95 | + |
| 96 | +## Scene 6 — Writing the test |
| 97 | + |
| 98 | +*Open the tests directory, find the Redis adapter test file.* |
| 99 | + |
| 100 | +> "Now let's write a test. Good software means tests. Let me find where the Redis adapter tests live." |
| 101 | +
|
| 102 | +*Navigate to the relevant test file.* |
| 103 | + |
| 104 | +> "I'll add a test case that: |
| 105 | +> 1. Trips the circuit breaker by recording failures |
| 106 | +> 2. Calls `reset()` |
| 107 | +> 3. Asserts that the circuit is available again and all counts are cleared |
| 108 | +
|
| 109 | +> This covers the main use case — a developer who wants to clear state, for example between test runs or after a manual recovery." |
| 110 | +
|
| 111 | +```php |
| 112 | +public function testReset(): void |
| 113 | +{ |
| 114 | + // Trip the circuit by recording failures |
| 115 | + for ($i = 0; $i < $this->configuration->failureCountThreshold(); $i++) { |
| 116 | + $this->ganesha->failure('test-service'); |
| 117 | + } |
| 118 | + |
| 119 | + $this->assertFalse($this->ganesha->isAvailable('test-service')); |
| 120 | + |
| 121 | + // Reset all state |
| 122 | + $this->ganesha->reset(); |
| 123 | + |
| 124 | + // Circuit should be available again |
| 125 | + $this->assertTrue($this->ganesha->isAvailable('test-service')); |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +> "Simple and clear. The test tells a story: trip the breaker, reset it, confirm it's back to normal." |
| 130 | +
|
| 131 | +--- |
| 132 | + |
| 133 | +## Scene 7 — Running the tests |
| 134 | + |
| 135 | +*Open terminal.* |
| 136 | + |
| 137 | +> "Let me run the test suite to make sure everything passes." |
| 138 | +
|
| 139 | +```bash |
| 140 | +vendor/bin/phpunit |
| 141 | +``` |
| 142 | + |
| 143 | +> "While this runs — one thing I appreciate about this project is that the tests actually hit a real Redis instance running in Docker. Some projects mock the database layer in tests, but that can hide bugs when the real database behaves differently. Here we're testing against the actual thing, which gives me much more confidence." |
| 144 | +
|
| 145 | +*Tests pass.* |
| 146 | + |
| 147 | +> "All green. |
| 148 | +
|
| 149 | +> Let me also run the static analysis tool — Psalm — just to make sure there are no type errors." |
| 150 | +
|
| 151 | +```bash |
| 152 | +vendor/bin/psalm |
| 153 | +``` |
| 154 | + |
| 155 | +> "Clean. Psalm is a static analysis tool for PHP that catches type errors before runtime. It's really useful for a library like this where you want to guarantee the API is correct." |
| 156 | +
|
| 157 | +--- |
| 158 | + |
| 159 | +## Scene 8 — Wrap-up |
| 160 | + |
| 161 | +> "And that's it! We fixed a TODO that's been sitting in the codebase — implemented the `reset()` method for the Redis adapter using `SCAN` for safe, non-blocking key iteration. |
| 162 | +
|
| 163 | +> To summarize what we did today: |
| 164 | +> - We explored the circuit breaker pattern and how Ganesha implements it |
| 165 | +> - We found a TODO in the Redis adapter's `reset()` method |
| 166 | +> - We looked at the Redis data structures being used — sorted sets for failure timestamps, plain strings for circuit status |
| 167 | +> - We implemented `reset()` using Redis `SCAN` instead of `KEYS` to avoid blocking the server |
| 168 | +> - We wrote a test to verify the behavior |
| 169 | +> - And all tests pass |
| 170 | +
|
| 171 | +> If you found this useful, give it a thumbs up. I'll be posting more videos like this — working through real open-source code. See you next time." |
| 172 | +
|
| 173 | +--- |
| 174 | + |
| 175 | +## Notes for Recording |
| 176 | + |
| 177 | +- **Editor**: VS Code with the file tree visible on the left sidebar |
| 178 | +- **Font size**: Increase to at least 18pt for readability on video |
| 179 | +- **Terminal**: Keep it in the same window, use a split pane if possible |
| 180 | +- **Pace**: Pause after each code block to give viewers time to read |
| 181 | +- **Pronunciation tips**: |
| 182 | + - "circuit breaker" → SIR-kit BRAY-ker |
| 183 | + - "cascading" → kas-KAY-ding |
| 184 | + - "threshold" → THRESH-old |
| 185 | + - "iterate" → IT-er-ate |
| 186 | + - "cursor" → KER-ser |
0 commit comments