549
@yaml_yusuf ·

Shared HashMap behind a mutex or channels for twelve workers that mostly read Idiomatic?

Twelve async workers fetching pages, all consulting a map of about 50k domain to rate-limit entries. Reads are maybe 95% of accesses, writes happen when a new domain appears or a limit updates. I can either wrap it in Arc<RwLock<HashMap>> and share it, or give ownership to one task and talk to it over channels. Memory is not a constraint, latency per request is. Which way would you go and why?

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @borrowck_ben · 4w ago · 3 replies

    With 95% reads and twelve workers I'd start with Arc<RwLock<HashMap>> using the async RwLock only if you hold it across awaits, and the std one if you don't. The channel-to-owner design is lovely for complex invariants but it serialises every read through one task and adds a round trip per lookup, which is the exact thing you said you care about. Measure before you get clever: at 50k entries a read lock plus a hash lookup is sub-microsecond, and if that turns out to be your bottleneck you'll have learned something surprising.

    421
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @borrowck_ben · last mo.

      Then use std::sync::RwLock and keep the critical section to the lookup only. Async mutexes are slower and you don't need what they offer.

      163
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @yaml_yusuf · 4w ago

      Good point on holding across awaits, I don't need to, the lookup returns a small Copy struct.

      87
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @attic_server · 4w ago · 3 replies

    Third option worth pricing: a sharded concurrent map crate. You get lock-free-ish reads without designing anything, and for a rate limiter shared across a dozen workers it's the boring choice that just works. The tradeoff is a dependency and slightly worse behaviour if you ever need a consistent snapshot across keys, which for rate limits you don't.

    267
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @rackmount_rina · last mo.

      This is what I'd do. Went from RwLock to a sharded map on a similar workload and the p99 got noticeably tighter, mostly because writes stopped occasionally blocking a burst of readers.

      118
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @yaml_yusuf · last mo.

      That p99 point is the sort of thing I wouldn't have caught in a benchmark of averages. Thanks.

      64
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @grepwitch · last mo. · 2 replies

    Slight disagreement with the crowd: the channel design gets much more attractive if your writes ever need to be conditional on a read, like 'increment unless over the limit'. With a lock you either take a write lock for every check or you race. Rate limiting is exactly the kind of thing that tends to grow that requirement in month three.

    186
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @borrowck_ben · 4w ago

      Fair, though you can also express that as a single write-locked method on a wrapper type rather than a message. Same atomicity, no round trip.

      94
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @flux_and_solder · 4w ago

    Whichever you pick, hide it behind a small struct with two or three methods rather than passing the map around. Then swapping the implementation later is an afternoon rather than a refactor, and you can benchmark both properly.

    148
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @scripting_slowly · last mo.

    50k entries is small enough that you could also just clone the map into each worker and rebuild it periodically, if staleness of a few seconds is acceptable for a rate limit. Not elegant, zero contention.

    79
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report