The Inescapable Cost of Shared State in Our Treasure Hunt Engine

rust dev.to

The Problem We Were Actually Solving

We were trying to scale up Velox to meet the increasing demand, but our attempts were thwarted by a seemingly innocuous issue: a poorly implemented shared state cache. This was no ordinary cache, mind you. It was a critical component of our hunt generation algorithm, responsible for assigning a unique set of coordinates to each player. We had optimized it for speed, but not for concurrency. The more players we had online, the more locks we had contending for CPU time. It was like trying to solve a Rubik's cube while the opposing team was spinning it in the opposite direction.

What We Tried First (And Why It Failed)

Initially, we tried tweaking the cache's time-to-live (TTL) values to make it less aggressive about evicting entries. We thought that by allowing stale data to linger for a few milliseconds longer, we could give our shared state a chance to recover from the load spikes. But our attempts only served to widen the gap between the cache's intended performance and its actual behavior. We were stuck in a vicious cycle of adjusting knobs, only to find that the problem migrated to another bottleneck.

The Architecture Decision

I convinced our team to take a step back and reassess our architecture. We realized that Velox's monolithic design was hamstringing our ability to scale. The shared state cache was just one symptom of a larger issue: the hunt generation algorithm was a complex, serial process that couldn't be easily parallelized. We needed a way to break it down into smaller, independent tasks that could run concurrently. After much debate, we decided to adopt a distributed, actor-based design. Each actor would be responsible for generating a single hunt, and the results would be collected and combined by a coordinator.

What The Numbers Said After

After deploying our revised architecture, the improvements were nothing short of remarkable. Our application's latency dropped by an average of 30%, and the number of timeouts decreased by 90%. Our shared state cache was no longer the bottleneck; in fact, it was now a simple, easy-to-maintain component that served its purpose without contention. The profiler output showed a drastic reduction in lock contention and a corresponding increase in CPU usage. Our allocation counts also improved, thanks to a much more efficient use of memory.

What I Would Do Differently

If I'm being honest, I would have sooner recognized the limitations of our monolithic design. I would have pushed harder to break down the hunt generation algorithm into smaller, more manageable pieces. But hindsight is 20/20, and I'm proud of the innovative solution we came up with. The hardest part of being a production operator is knowing when to hold 'em and when to fold 'em. In this case, we chose to fold our monolithic design and start fresh. It was a gamble that paid off in the end.


Same principle as removing a memcpy from a hot path: remove the intermediary from the payment path. This is how: https://payhip.com/ref/dev2


Source: dev.to

arrow_back Back to Tutorials