effect icon indicating copy to clipboard operation
effect copied to clipboard

RcMap: Support dynamic idleTimeToLive values per key

Open schickling opened this issue 4 weeks ago • 0 comments

Problem

Currently, RcMap.make accepts a static idleTimeToLive option that applies uniformly to all entries in the map. This value is stored once in the RcMapImpl constructor and used for all entries throughout the lifetime of the map.

However, there are use cases where different keys require different idle TTL values. For example:

  • Resource pools with varying cost: Expensive resources (e.g., database connections to a primary) should have longer TTLs, while cheap resources (e.g., read replicas) can have shorter TTLs
  • Priority-based caching: High-priority or frequently accessed resources should be kept longer than low-priority ones
  • Dynamic workload adaptation: During high load, reduce TTLs to free resources faster; during idle periods, extend TTLs to avoid unnecessary re-acquisition

Proposed Solution

Allow idleTimeToLive to be a function that receives the key and returns a duration:

const map = yield* RcMap.make({
  lookup: (key: string) => acquireResource(key),
  idleTimeToLive: (key: string) => {
    if (key.startsWith("premium:")) return Duration.minutes(10)
    if (key.startsWith("cache:")) return Duration.seconds(30)
    return Duration.minutes(1)  // default
  }
})

API Design Options

Option A: Function signature

idleTimeToLive?: Duration.DurationInput | ((key: K) => Duration.DurationInput) | undefined

Option B: Effectful function (more flexible)

idleTimeToLive?: Duration.DurationInput | ((key: K) => Effect.Effect<Duration.DurationInput>) | undefined

Option B would allow the TTL to depend on async context (e.g., checking a config service), though Option A is simpler and covers most use cases.

Implementation Considerations

  1. Entry-level TTL storage: Currently RcMapImpl stores idleTimeToLive at the class level. With this change, each entry would need to store its computed TTL (or we compute it on each release)

  2. touch function behavior: The touch function currently uses self.idleTimeToLive to extend the expiration. With dynamic TTLs, it should use the entry's configured TTL. This could be stored on the Entry type:

    interface Entry<A, E> {
      // ... existing fields
      readonly idleTimeToLive: Duration.Duration | undefined  // computed at acquisition time
    }
    
  3. Backwards compatibility: The change is backwards compatible since static durations would continue to work as before

Alternatives Considered

  • Multiple RcMaps: Create separate RcMaps for different TTL requirements. This works but adds complexity when the key space is unified and resources need to interact.
  • Custom wrapper: Implement TTL logic externally by calling invalidate on a schedule. This is error-prone and doesn't integrate well with the reference counting semantics.

🤖 Generated with Claude Code

schickling avatar Dec 03 '25 14:12 schickling