Skip to content

feat: add cachelon (name pending) crates with multi-level caching abstractions#240

Open
schgoo wants to merge 26 commits intomainfrom
cache
Open

feat: add cachelon (name pending) crates with multi-level caching abstractions#240
schgoo wants to merge 26 commits intomainfrom
cache

Conversation

@schgoo
Copy link
Collaborator

@schgoo schgoo commented Jan 29, 2026

Cachelon = Cache + Echelon

image

TODO more description

@github-actions
Copy link

github-actions bot commented Jan 29, 2026

⚠️ Breaking Changes Detected

error: failed to retrieve local crate data from git revision

Caused by:
    0: failed to retrieve manifest file from git revision source
    1: possibly due to errors: [
         failed to parse /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-origin_main/2627e94555b3f837af373a6e9930fa063b1d8e87/Cargo.toml: no `package` table,
         failed when reading /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-origin_main/2627e94555b3f837af373a6e9930fa063b1d8e87/scripts/crate-template/Cargo.toml: TOML parse error at line 9, column 26
         |
       9 | keywords = ["oxidizer", {{CRATE_KEYWORDS}}]
         |                          ^
       missing key for inline table element, expected key
       : TOML parse error at line 9, column 26
         |
       9 | keywords = ["oxidizer", {{CRATE_KEYWORDS}}]
         |                          ^
       missing key for inline table element, expected key
       ,
       ]
    2: package `cachelon` not found in /home/runner/work/oxidizer/oxidizer/target/semver-checks/git-origin_main/2627e94555b3f837af373a6e9930fa063b1d8e87

Stack backtrace:
   0: anyhow::error::<impl anyhow::Error>::msg
   1: cargo_semver_checks::rustdoc_gen::RustdocFromProjectRoot::get_crate_source
   2: cargo_semver_checks::rustdoc_gen::StatefulRustdocGenerator<cargo_semver_checks::rustdoc_gen::CoupledState>::prepare_generator
   3: cargo_semver_checks::Check::check_release::{{closure}}
   4: cargo_semver_checks::Check::check_release
   5: cargo_semver_checks::exit_on_error
   6: cargo_semver_checks::main
   7: std::sys::backtrace::__rust_begin_short_backtrace
   8: main
   9: <unknown>
  10: __libc_start_main
  11: _start

If the breaking changes are intentional then everything is fine - this message is merely informative.

Remember to apply a version number bump with the correct severity when publishing a version with breaking changes (1.x.x -> 2.x.x or 0.1.x -> 0.2.x).

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CacheEntry<V> {
value: V,
cached_at: Option<SystemTime>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if we merge cached_at and ttl into cached_until?

Copy link
Collaborator Author

@schgoo schgoo Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said above, cached_at does get set by the cache. But I guess it might be confusing if you can set it here... The reason for being configurable here is in case they want manual control, or they want to be able to test it. In other words, cached_at is never useful without ttl, but ttl is always useful without cached_at. Also, I don't really think exposing cached_at is even useful to consumers - it's really more for internal testing of the expiration functionality. Still, I think what I'll do is have these methods

pub fn new(value: V) -> Self

pub fn expires_after(value: V, ttl: Duration) -> Self

pub fn expires_at(value: V, ttl: Duration, cached_at: SystemTime) -> Self

pub(crate) fn ensure_cached_at(cached_at: SystemTime) // checks for pre-existing cached_at value

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CacheEntry<V> {
value: V,
cached_at: Option<SystemTime>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SystemTime is not very reliable, how about Instant?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instant is limited to in-process manipulations, while this abstraction needs to work with timestamps that are externally stored. For example, when redis entry was stored.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case we should provide a choice to the user because uses cases might be different. There are plenty of cases when cache is used only for process internal data

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It creates API split though. I would argue, that for local data one uses ttl anyway. Btw, would it make more sense to have expires_at field? What capabilities does redis expose for this?

Copy link
Collaborator Author

@schgoo schgoo Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Redis you set TTL as a duration in seconds. They don't expose a cached timestamp of any kind. I don't really have any opinion on Instant vs SystemTime, other than that I don't like that SystemTime::duration_since() is fallible.

/// Extension trait for timing async operations.
pub trait ClockExt {
/// Times an async operation and returns both the result and elapsed duration.
fn timed_async<F, R>(&self, f: F) -> impl Future<Output = TimedResult<R>>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho, this should stay internal to the crate. If these APIs are useful enough, we can just add them to tick.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it should have been internal, my mistake. I could see this being useful for the new telemetry crate (@Vaiz), but I'll leave them here until someone else has a use case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW this was doubling the sizes of the futures, so I added a small struct that implements Future to do the timing, it's much more efficient now, and might be more useful this way.

S: CacheTier<K, V> + Send + Sync,
{
async fn get(&self, key: &K) -> Result<Option<CacheEntry<V>>, Error> {
let timed = self.clock.timed_async(self.inner.get(key)).await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho, what's wrong with just having let stopwatch = clock.stopwatch(); and tracking the duration like that? Is extra layer around timed_async necessary here?

Copy link
Collaborator Author

@schgoo schgoo Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with it is I didn't think of it first. I do like timed_async because otherwise I have to write the stopwatch boilerplate in many places; this feels cleaner from the callsite. I do think timed_async might be better using stopwatch in the implementation, though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants