Skip to content

Commit

Permalink
Version0.1.7 (#8)
Browse files Browse the repository at this point in the history
* Improved clear and added a delete method

* Add constants to a separate file

* Add spec and update version and changelog

* Chore + spec

* Delete multi instead of iterating. update private method name

* Remove MessagePack as it had no value
  • Loading branch information
jlurena authored Jan 23, 2025
1 parent af7f88a commit 92a023d
Show file tree
Hide file tree
Showing 17 changed files with 165 additions and 115 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## [0.1.7] - 2024-01-23
- Improved `clear_cache` method for `active_support_cache` strategy to no longer use `delete_matched`.
- Introduced `.delete_from_cache` method to delete single cached resources.

## [0.1.6] - 2024-01-15
- Renamed `ActiveResource::Collection#refresh` to `#reload` to match Rails ORM naming convention.
- Added a `ActiveResource::Collection.none` class method similar to Rails `ActiveRecord::QueryMethods.none`
Expand Down
1 change: 0 additions & 1 deletion active_cached_resource.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ Gem::Specification.new do |spec|
spec.add_dependency "activemodel-serializers-xml", "~> 1.0"
spec.add_dependency "activemodel", ">= 6.0"
spec.add_dependency "activesupport", ">= 6.0"
spec.add_dependency "msgpack", "~> 1.7", ">= 1.7.5"
spec.add_dependency "ostruct", "~> 0.6.1"
end
1 change: 1 addition & 0 deletions lib/active_cached_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "activeresource/lib/activeresource"

require_relative "active_cached_resource/constants"
require_relative "active_cached_resource/model"
require_relative "active_cached_resource/version"

Expand Down
28 changes: 15 additions & 13 deletions lib/active_cached_resource/caching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

module ActiveCachedResource
module Caching
GLOBAL_PREFIX = "acr"
RELOAD_PARAM = :_acr_reload

extend ActiveSupport::Concern

included do
Expand Down Expand Up @@ -128,7 +125,7 @@ def find_with_cache(*orig_args)
# Hacky but this way ActiveCachedResource::Collection#request_resources! can access it
if should_reload && args.first == :all
options[:params] = {} if options[:params].blank?
options[:params][RELOAD_PARAM] = should_reload
options[:params][Constants::RELOAD_PARAM] = should_reload
args << options
end

Expand All @@ -140,13 +137,19 @@ def find_with_cache(*orig_args)
should_reload ? find_via_reload(*args) : find_via_cache(*args)
end

# Clears the cache for the specified pattern.
# Deletes a resource from the cache.
#
# @param id [Object] the identifier of the resource to be deleted from the cache.
def delete_from_cache(id)
cached_resource.cache.delete(cache_key(id))
end

# Clears the entire cache for the specified model that matches current prefix.
#
# @param pattern [String, nil] The pattern to match cache keys against.
# If nil, all cache keys with this models prefix will be cleared.
# @return [void]
def clear_cache(pattern = nil)
cached_resource.cache.clear("#{cache_key_prefix}/#{pattern}")
def clear_cache
cached_resource.logger.debug("Clearing cache for #{name} cache with prefix: #{cache_key_prefix}")
cached_resource.cache.clear(cache_key_prefix)
end

private
Expand Down Expand Up @@ -216,8 +219,7 @@ def cache_key(*args)
end

def name_key
# `cache_key_prefix` is separated from key parts with a dash to easily distinguish the prefix
"#{cache_key_prefix}-" + name.parameterize.tr("-", "/")
"#{cache_key_prefix}#{Constants::PREFIX_SEPARATOR}#{name.parameterize.tr("-", "/")}"
end

def cache_key_prefix
Expand All @@ -228,9 +230,9 @@ def cache_key_prefix
if !result.is_a?(String) || result.empty?
raise ArgumentError, "cache_key_prefix must return a non-empty String"
end
"#{GLOBAL_PREFIX}/#{result}"
"#{Constants::GLOBAL_PREFIX}/#{result}"
else
"#{GLOBAL_PREFIX}/#{prefix}"
"#{Constants::GLOBAL_PREFIX}/#{prefix}"
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,35 @@ def read_raw(key)
end

def write_raw(key, compressed_value, options)
@cache_store.write(key, compressed_value, options)
successful_write = @cache_store.write(key, compressed_value, options)
update_master_key(key, options) if successful_write

successful_write
end

def delete_raw(key)
@cache_store.delete(key)
end

def clear_raw(pattern)
if @cache_store.respond_to?(:delete_matched)
@cache_store.delete_matched("#{pattern}*")
true
else
false
end
def clear_raw(prefix)
existing_keys = @cache_store.read(prefix)
return if existing_keys.nil?

existing_keys.add(prefix)
@cache_store.delete_multi(existing_keys)
end

private

# Updates the `master` key, which contains keys for a given prefix.
def update_master_key(key, options)
prefix, _ = split_key(key)

existing_keys = @cache_store.read(prefix) || Set.new
existing_keys.add(key)

# Maintain the list of keys for twice the expiration time
@cache_store.write(prefix, existing_keys, expires_in: options[:expires_in])
end
end
end
Expand Down
39 changes: 31 additions & 8 deletions lib/active_cached_resource/caching_strategies/base.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
require "msgpack"
module ActiveCachedResource
module CachingStrategies
class Base
Expand Down Expand Up @@ -29,6 +28,14 @@ def write(key, value, options)
write_raw(hash_key(key), compress(value), options)
end

# Deletes the cached value associated with the given key.
#
# @param key [Object] the key whose associated cached value is to be deleted.
# @return [void]
def delete(key)
delete_raw(hash_key(key))
end

# Clears the cache based on the given pattern.
#
# @param pattern [String] the pattern to match cache keys that need to be cleared.
Expand Down Expand Up @@ -76,6 +83,22 @@ def clear_raw(pattern)
raise NotImplementedError, "#{self.class} must implement `clear_raw`"
end

protected

# Splits the provided key into a prefix and the remaining part
#
# @param key [String] the key to be split
#
# @example Splitting a key
# split_key("prefix-key") #=> "acr/prefix/keyvalue"
#
# @return [Array<String>] an array containing two elements: the part before the first "-", and the rest of the string
def split_key(key)
# Prefix of keys are expected to be the first part of key separated by a dash.
prefix, k = key.split(ActiveCachedResource::Constants::PREFIX_SEPARATOR, 2)
[prefix, k]
end

private

# Generates a hashed key for caching purposes.
Expand All @@ -88,25 +111,25 @@ def clear_raw(pattern)
# @example Hashing a key
# hash_key("prefix-key") #=> "acr/prefix/Digest::SHA256.hexdigest(key)"
#
# @raise [ArgumentError] If the key does not contain a prefix and a key separated by a dash.
#
# @param key [String] the original key to be hashed. It is expected to have a prefix and the key separated by a dash.
# @return [String] the generated hashed key with the global prefix and the prefix from the original key.
def hash_key(key)
# Prefix of keys are expected to be the first part of key separated by a dash.
prefix, k = key.split("-", 2)
prefix, k = split_key(key)
if prefix.nil? || k.nil?
raise ArgumentError, "Key must have a prefix and a key separated by a dash"
end

"#{prefix}/" + Digest::SHA256.hexdigest(k)
"#{prefix}#{ActiveCachedResource::Constants::PREFIX_SEPARATOR}#{Digest::SHA256.hexdigest(k)}"
end

def compress(value)
MessagePack.pack(value)
value.to_json
end

def decompress(value)
MessagePack.unpack(value)
rescue MessagePack::UnpackError
JSON.parse(value)
rescue JSON::ParserError
nil
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/active_cached_resource/caching_strategies/sql_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def initialize(model, options = {})

protected

def delete_raw(key)
@model.where(key: key).delete_all
end

def read_raw(key)
record = @model.where(key: key).where(@model.arel_table[:expires_at].gt(Time.current)).first
record&.value
Expand Down
2 changes: 1 addition & 1 deletion lib/active_cached_resource/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def request_resources!

# Delete the reload param from query params.
# This is drilled down via `params` option to determine if the collection should be reloaded
should_reload = query_params.delete(ActiveCachedResource::Caching::RELOAD_PARAM)
should_reload = query_params.delete(Constants::RELOAD_PARAM)
if !should_reload
from_cache = resource_class.send(:cache_read, from, path_params, query_params, prefix_options)
@elements = from_cache
Expand Down
7 changes: 7 additions & 0 deletions lib/active_cached_resource/constants.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ActiveCachedResource
module Constants
GLOBAL_PREFIX = "acr"
PREFIX_SEPARATOR = ":"
RELOAD_PARAM = :_acr_reload
end
end
2 changes: 1 addition & 1 deletion lib/active_cached_resource/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def inherited(child)
private

def invalidate_cache
self.class.clear_cache(id.to_s)
self.class.delete_from_cache(id.to_s)
end

def save_to_cache
Expand Down
2 changes: 1 addition & 1 deletion lib/active_cached_resource/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module ActiveCachedResource
VERSION = "0.1.6"
VERSION = "0.1.7"
end
70 changes: 50 additions & 20 deletions spec/active_cached_resource/caching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,36 +96,24 @@ def expect_request(path, count)
end
end

describe "#clear" do
describe ".clear_cache" do
before { mock_single_resource }

it "clears the cache" do
TestResource.find(1) # Cache the resource
TestResource.find(2) # Cache the resource
expect_request("/test_resources/1.json", 1)
expect_request("/test_resources/2.json", 1)

TestResource.clear_cache
TestResource.find(1) # Cache cleared, fetch again
TestResource.find(2) # Cache cleared, fetch again
expect_request("/test_resources/1.json", 2)
end

context "with a cache key pattern" do
it "clears the cache for matching keys" do
TestResource.find(1) # Cache the resource
expect_request("/test_resources/1.json", 1)

TestResource.find(2) # Cache the resource
expect_request("/test_resources/2.json", 1)

TestResource.clear_cache("1*")

TestResource.find(1) # Cache cleared, fetch again
expect_request("/test_resources/1.json", 2)
expect_request("/test_resources/2.json", 1)
end
expect_request("/test_resources/2.json", 2)
end
end

describe "#find_with_cache" do
describe ".find_with_cache" do
context "when caching single resources" do
before { mock_single_resource }

Expand Down Expand Up @@ -171,6 +159,23 @@ def expect_request(path, count)
end
end

describe ".delete_from_cache" do
before { mock_single_resource }

it "deletes a resource from the cache" do
TestResource.find(1) # Cache the resource
expect_request("/test_resources/1.json", 1)

TestResource.delete_from_cache(1)
TestResource.find(1) # Cache deleted, fetch again
expect_request("/test_resources/1.json", 2)
end

it "does not raise an error if the resource is not in the cache" do
expect { TestResource.delete_from_cache(999) }.not_to raise_error
end
end

describe "custom caching strategies" do
before do
TestResource.setup_cached_resource!(
Expand Down Expand Up @@ -219,7 +224,7 @@ def expect_request(path, count)

it "uses the callable proc to generate cache key prefix" do
TestResource.find(1) # Cache the resource
hashed_key = custom_cache.send(:hash_key, "acr/dynamic_prefix-testresource/1")
hashed_key = custom_cache.send(:hash_key, "acr/dynamic_prefix#{ActiveCachedResource::Constants::PREFIX_SEPARATOR}testresource/1")
expect(custom_cache.store.key?(hashed_key)).to be true
end

Expand Down Expand Up @@ -271,6 +276,8 @@ def expect_request(path, count)
ttl: 10.minutes,
logger: logger
)

TestResource.clear_cache
end

context "Caching" do
Expand Down Expand Up @@ -304,18 +311,41 @@ def expect_request(path, count)
end
end

context "clear_cache" do
context ".clear_cache" do
before do
mock_single_resource
end

it "clears the cache" do
TestResource.find(1) # Cache the resource
TestResource.find(2) # Cache the resource
expect_request("/test_resources/1.json", 1)
expect_request("/test_resources/2.json", 1)

TestResource.clear_cache
TestResource.find(1) # Cache cleared, fetch again
TestResource.find(2) # Cache cleared, fetch again
expect_request("/test_resources/1.json", 2)
expect_request("/test_resources/2.json", 2)
end
end

context ".delete_from_cache" do
before do
mock_single_resource
end

it "deletes the single resource" do
TestResource.find(1) # Cache the resource
TestResource.find(2) # Cache the resource
expect_request("/test_resources/1.json", 1)
expect_request("/test_resources/2.json", 1)

TestResource.delete_from_cache(1)
TestResource.find(1) # Cache cleared, fetch again
TestResource.find(2) # Cache cleared, fetch again
expect_request("/test_resources/1.json", 2)
expect_request("/test_resources/2.json", 1)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,4 @@
let(:constructor_args) { [ActiveSupport::Cache::MemoryStore.new] }

it_behaves_like "a caching strategy"

context "When the cache does not support the `delete_matched` method" do
let(:cache_store) { double("cache_store") }
let(:cache_instance) { described_class.new(cache_store) }

it "always returns false for clear" do
expect(cache_instance.clear("pattern")).to be false
expect(cache_instance.clear("bar")).to be false
end
end
end
Loading

0 comments on commit 92a023d

Please sign in to comment.