Stale cached value returned after external file change in Memoized subroutine
0 reputation · 27 Mar 2024, 02:39 UTC
0 reputation · 27 Mar 2024, 02:39 UTC
Goal: guarantee that a memoized subroutine returns up‑to‑date data when the underlying external resource (e.g., a file or database) changes, including after a process fork where the child should see its own environment.
Constraints: the Memoize module stores results keyed only by argument values and never expires entries automatically; after a fork the child process inherits the parent’s %Memoize::CACHE, so stale values persist until the cache is manually flushed. No standard hook exists to trigger invalidation on fork or on external events.
Uncertainty: the documentation notes an unresolved design decision about providing an optional callback for automatic cache invalidation after a fork, but no consensus on its interface or impact on threaded builds.
Should Memoize offer an optional flush‑on‑fork hook, and if so, what function signature would be appropriate? How would such a hook interact with existing Cache or LOCK options in threaded Perl builds? What fallback behavior should be used when the hook is not defined?
32275 reputation · 27 Mar 2024, 07:55 UTC
Memoize can (and should) provide an optional flush_on_fork callback that is executed automatically after a fork. The callback receives the memoized subroutine reference and the arguments that were supplied to the memoized call. It must return a boolean: returning 1 clears the cache for that sub, while 0 leaves the cache untouched.
Signature example:
Memoize::set_cache(
sub { ... },
Cache => $cache_obj,
LOCK => 'global',
flush_on_fork => sub { my ($sub, @args) = @_; return 1; },
);
In a threaded Perl build, Cache can be a thread‑local object (e.g., Thread::Queue). The flush_on_fork callback operates on the cache instance that lives in the child thread, so only the child’s view is cleared. The LOCK option remains unaffected because it governs inter‑thread locking, not cache invalidation.
Fallback behavior: If no flush_on_fork is supplied, the default is to inherit the parent’s cache unchanged. The child will continue to return stale values until the user explicitly calls Memoize::flush or installs a custom cache that supports automatic invalidation.
Providing a flush‑on‑fork hook would give developers precise control over when a cache should be invalidated after a fork, avoiding the need to manually flush or re‑initialize the cache in each child process. It would also preserve existing behavior for non‑threaded builds where the cache is shared by default.
Memoize module accepts a flush_on_fork option that is called with the memoized sub and its arguments.flush_on_fork option when memoizing the sub that reads the external resource.Cache option points to a thread‑local storage object; the flush_on_fork will automatically clear only the child’s cache.clear method and use the callback to call that method.Do you plan to use a thread‑local cache implementation (e.g., Thread::Queue) in your threaded build, or will you rely on the default shared cache?
Adding a flush_on_fork hook gives Memoize the flexibility to handle stale data after forks while preserving existing cache and locking semantics. The signature is simple, the interaction with Cache and LOCK is straightforward, and the fallback is clear: inherit the parent’s cache unchanged.
Use comments to ask for clarification. Post a solution as an answer.
32,275 reputation · 27 Mar 2024, 09:28 UTC
While a flush_on_fork callback is a clean API addition, another practical approach for handling stale data after a fork is implementing a PID check at call time. Since Memoize stores results in a hash, a wrapper could store the process ID ($$) alongside the cached value.
If the current PID differs from the PID stored with the cached entry, the wrapper can trigger Memoize::flush or simply treat the entry as expired. This avoids the need for a global fork hook and integrates naturally with the existing Memoize architecture:
LOCK options in threaded builds, as it operates on the child's local copy of the cache.One caveat: this approach assumes the developer does not intentionally want the child to inherit the parent's pre-computed state to save startup time.