Skip to content

Entry and_compute method for performing an insert, update, or remove operation with single closure #227

@tatsuya6502

Description

@tatsuya6502

The entry API was added to Moka v0.10.0. It has some methods such as entry_by_ref(&key).or_insert_with(|| ...) to atomically insert a value when key does not exist. This issue will add another method and_compute to the entry API for more complex tasks. It is similar to the compute method in Java Caffeine cache. Moka user can use and_compute to insert, update, or remove a value for a key with single closure or function.

A new entry API: and_compute method

use moka::{Entry, ops::{Op, PerformedOp}};

// For moka::sync::Cache's entry API
// `K` and `V` are the key and value type of the cache.
pub fn and_compute<F>(self, f: F) -> (Option<Entry<K, V>>, PerformedOp)
where
    F: FnOnce(Option<&V> -> Op<V>);

// For moka::future::Cache's entry API
pub async fn and_compute<'a, F, Fut>(self, f: F) -> (Option<Entry<K, V>>, PerformedOp)
where
    F: FnOnce(Option<&'a V>) -> Fut,
    Fut: Future<Output = Op<V>> + 'a;

moka::ops::Op and moka::ops::PerformedOp will be defined as:

/// Instructs the `and_compute` method how to modify the cache entry.
pub enum Op<V> {
    /// No-op. Do not modify the cache entry.
    Nop,
    /// Insert or replace the value of the cache entry.
    Put(V),
    /// Invalidate the cache entry.
    Invalidate,
}

/// Will be returned from `and_compute` method to indicate what kind of
/// operation was performed.
pub enum PerformedOp {
    /// The entry did not exist, or already existed but was not modified.
    Nop,
    /// The entry did not exist and inserted.
    Inserted,
    /// The entry already existed and its value was updated.
    Updated,
    /// The entry existed and was invalidated.
    /// Note: If `and_compute` tried to invalidate a not-exiting entry, 
    /// `Nop` will be returned instead of `Invalidated`
    Invalidated,
}

Example

Here is an example of a future cache holding counters. (from a question in #179)

use moka::{future::Cache, ops::{Op, PerformedOp}};

let cache: Cache<String, u64> = Cache::new(100);
let key = "key".to_string();

// maybe_entry: Option<moka::Entry<K, V>>
// op_kind:     moka::op::PerformedOp
let (maybe_entry, performed_op) = cache
    .entry_by_ref(&key)
    .and_compute(|entry| async move {
        match entry {
            // If the entry does not exist, insert a value of 1.
            None => Op::Put(1),
            // If the entry exists, increment the value by 1.
            Some(count) => Op::Put(count.saturating_add(1)),
        }
    })
    .await;

assert_eq!(performed_op, PerformedOp::Inserted);
assert_eq!(maybe_entry.unwrap().into_value(), 1);

Concurrency

The and_compute should have the following attributes about concurrency:

  • It is a single operation that is atomic with respect to other operations. It is not possible to observe an intermediate state where the entry is neither present nor absent.
  • If there are simultaneous and_compute calls on the same key, only one call will proceed at a time. The other calls will wait until the first call completes.
    • This is also true between and_compute and or_insert_with on the same key. Only one call will proceed at a time, and the other calls will wait until the first call completes.
  • Unlike or_insert_with, there is no call coercing between simultaneous and_compute calls on the same key.
    • This is because, in typical use cases, and_compute will not do idempotent operation. (e.g. counting up a value)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions