sidekiq_smart_cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0bfe51b0884c74e8f916b2e07a756b14fa41810dc3c137225bc978ac6a41ab81
4
+ data.tar.gz: e5c34d6795f47aa5b4ba17e7ca538ada975cdef8406c113a8107b205cd540955
5
+ SHA512:
6
+ metadata.gz: b66ae306d585ac20cf6b0e5d47bd634cb1fb36ba4e51b041c33eb3f77471190405cb4a64face8a53b578425e85f9bf85a345fd869f262ce91516941fab17e168
7
+ data.tar.gz: 2fc77d0ad12e9c317dddb1d6a8358256fb86f74330ed1fde78a71e61fa9914a6dd55ff8406a57770de8b0476615ff0ac7ceba6ddf3dc0f93d999c3007d896249
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Bill Kirtley
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # SidekiqSmartCache
2
+
3
+ Generate and cache objects using sidekiq, with thundering herd prevention and client timeouts.
4
+
5
+ Say you have a resource that's expensive to calculate (a heavy database query) and would like to use it in a web page.
6
+
7
+ But you'd rather the web page show an empty value (or a "check back later" placeholder) than take too long to render.
8
+
9
+ And you'd like to ensure that if there are multiple actions requesting that page at once, that the database only has to fill the query once. (You want to prevent a [thundering herd problem](https://en.wikipedia.org/wiki/Thundering_herd_problem))
10
+
11
+ ## Usage
12
+
13
+ Say your `Widget` class has a method `do_a_thing` that is sometimes quite expensive to calculate, but returns a value you'd like to include in a web page, as long as the value can be made available in five seconds. Once calculated, the value is valid for ten minutes, and all renderings of the page can show that same value.
14
+
15
+ In the controller:
16
+
17
+ ```ruby
18
+ promise = SidekiqSmartCache::Promise.new(klass: Widget, method: :do_a_thing, expires_in: 10 * 60)
19
+ if promise.ready_within?(5.seconds)
20
+ # Render using the generated thing
21
+ @thing = promise.value
22
+ else
23
+ # Render some "try again in a bit" presentation
24
+ end
25
+ ```
26
+
27
+ If no other workers are currently calculating the value, this will queue up a sidekiq job to call `Widget.do_a_thing`. If other workers are currently calculating, it will not start another, preventing the thundering herd.
28
+
29
+ Then it will wait as much as 5 seconds for a value to be returned.
30
+
31
+ If in the end, value is nil, offer a default or "try again" presentation to the user.
32
+
33
+ Also supported: passing arguments, calling an instance method on an object in the database, and explicitly naming your cache tag.
34
+
35
+ ```ruby
36
+ SidekiqSmartCache::Promise.new(
37
+ object: widget, method: :do_an_instance_thing, args: ['fun', 12],
38
+ expires_in: 10.minutes
39
+ )
40
+ ```
41
+
42
+ If your results are (or contain) other than the YAML.safe_load list (TrueClass, FalseClass, NilClass, Numeric, String, Array, Hash) plus Symbol and Time, configure with an initializer:
43
+
44
+ ```
45
+ SidekiqSmartCache.allowed_classes = [MyFavoriteClass]
46
+ ```
47
+
48
+ ## Installation
49
+ Add this line to your application's Gemfile:
50
+
51
+ ```ruby
52
+ gem 'sidekiq_smart_cache'
53
+ ```
54
+
55
+ And then execute:
56
+ ```bash
57
+ $ bundle
58
+ ```
59
+
60
+ Or install it yourself as:
61
+ ```bash
62
+ $ gem install sidekiq_smart_cache
63
+ ```
64
+
65
+ Add an initializer:
66
+ ```ruby
67
+ Rails.configuration.to_prepare do
68
+ SidekiqSmartCache.logger = Rails.logger
69
+ SidekiqSmartCache.redis_pool = Sidekiq.redis_pool
70
+ SidekiqSmartCache.cache_prefix = ENV['RAILS_CACHE_ID']
71
+ end
72
+ ```
73
+
74
+ ## Model mix-in
75
+
76
+ Let's assume your User class has a active_user_count class method that is expensive to calculate
77
+
78
+ Declaring `make_class_action_cacheable :active_user_count` will add:
79
+
80
+ * `User.active_user_count_without_caching` - always performs the full calculation synchronously, doesn't touch the cache.
81
+ * `User.refresh_active_user_count` - always performs the full calculation synchronously, populating the cache with the new value.
82
+ * `User.active_user_count` (the original name) - will now fetch from the cache, only recalculating if the cache is absent or stale.
83
+ * `User.active_user_count_if_available` will now fetch from the cache but not recalculate, returning nil if the cache is absent or stale.
84
+ * `User.active_user_count_cache_tag` - the cache tag used to store calculated results. Probably not useful to clients.
85
+ * `User.active_user_count_promise` - returns a Promise object
86
+
87
+ Call `promise.fetch(5.seconds)` to wait up to five seconds for a new value, returning nil on timeout.
88
+
89
+ Call `promise.fetch!(5.seconds)` to wait up to five seconds for a new value, raising `SidekiqSmartCache::TimeoutError` on timeout.
90
+
91
+ Use <tt>make_instance_action_cacheable</tt> for the equivalent set of instance methods.
92
+ Your models must respond to <tt>to_param</tt> with a unique string suitable for constructing a cache key.
93
+ The class must respond to <tt>find</tt> and return an object that responds to the method.
94
+
95
+ ## Testing
96
+
97
+ # Setup
98
+ ```
99
+ bundle install
100
+ pushd test/dummy
101
+ bundle exec rake db:create db:migrate db:test:prepare
102
+ popd
103
+ bundle exec bin/test
104
+ ```
105
+ # Tidy
106
+
107
+ The test database is using sqlite, so `rm test/dummy/db/*.sqlite3` is a sincere way to reset context.
108
+
109
+ # Debugging
110
+ Test a single file:
111
+ ```
112
+ bin/test test/model_test.rb -n test_cache_tag_generation
113
+ ```
114
+
115
+ Open a SQL shell on a database:
116
+ ```
117
+ sqlite3 test/dummy/db/test.sqlite3
118
+ ```
119
+
120
+ Test with a specific Sidekiq version:
121
+ ```
122
+ BUNDLE_GEMFILE=gemfiles/Gemfile-sidekiq7 bundle exec bin/test
123
+ ```
124
+
125
+ Specify a non-default redis for both sidekiq and the cache:
126
+ ```
127
+ REDIS_PROVIDER=REDIS_URL REDIS_URL=redis://localhost:6380 BUNDLE_GEMFILE=gemfiles/Gemfile-sidekiq7 bundle exec bin/test
128
+ ```
129
+
130
+ ## Contributing
131
+ Contribution directions go here.
132
+
133
+ ## License
134
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'SidekiqSmartCache'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+
33
+ task default: :test
@@ -0,0 +1,27 @@
1
+ module SidekiqSmartCache
2
+ class Interlock
3
+ delegate :redis, to: SidekiqSmartCache
4
+ attr_accessor :cache_tag, :job_interlock_timeout
5
+
6
+ def initialize(cache_tag, job_interlock_timeout=nil)
7
+ @cache_tag = cache_tag
8
+ @job_interlock_timeout = job_interlock_timeout
9
+ end
10
+
11
+ def key
12
+ cache_tag + '/in-progress'
13
+ end
14
+
15
+ def working?
16
+ redis.get(key)
17
+ end
18
+
19
+ def lock_job?
20
+ redis.set(key, 'winner!', nx: true) && redis.expire(key, job_interlock_timeout)
21
+ end
22
+
23
+ def clear
24
+ redis.del(key)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,109 @@
1
+ require 'active_support/concern'
2
+ module SidekiqSmartCache::Model
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Takes a class or instance action, makes it operate lazy-loaded through a cache, and adds a couple of related actions to the class.
7
+ #
8
+ # === Example
9
+ #
10
+ # A User class has a active_user_count class method that is expensive to calculate
11
+ #
12
+ # Declaring <tt>make_class_action_cacheable :active_user_count</tt> will add:
13
+ #
14
+ # * <tt>User.active_user_count_without_caching</tt> - always performs the full calculation synchronously, doesn't touch the cache.
15
+ # * <tt>User.refresh_active_user_count</tt> - always performs the full calculation synchronously, populating the cache with the new value.
16
+ # * <tt>User.active_user_count</tt> (the original name) - will now fetch from the cache, only recalculating if the cache is absent or stale.
17
+ # * <tt>User.active_user_count_if_available</tt> will now fetch from the cache but not recalculate, returning nil if the cache is absent or stale.
18
+ # * <tt>User.active_user_count_cache_tag</tt> - the cache tag used to store calculated results. Probably not useful to clients.
19
+ # * <tt>User.active_user_count_promise</tt> - returns a promise object
20
+ #
21
+ # Call <tt>promise.fetch(5.seconds)</tt> to wait up to five seconds for a new value, returning nil on timeout
22
+ # Call <tt>promise.fetch!(5.seconds)</tt> to wait up to five seconds for a new value, raising SidekiqSmartCache::TimeoutError on timeout
23
+ #
24
+ # Use <tt>make_instance_action_cacheable</tt> for the equivalent set of instance methods.
25
+ # Your models must respond to <tt>to_param</tt> with a unique string suitable for constructing a cache key.
26
+ # The class must respond to <tt>find(param)</tt> and return an object that responds to the method.
27
+ #
28
+ # === Options
29
+ # [:cache_tag]
30
+ # Specifies the cache tag to use.
31
+ # A default cache tag will include the cache_prefix, so will implicitly flush on each release.
32
+ #
33
+ # [:expires_in]
34
+ # Specifies a period after which a cached result will be invalid. Default one hour.
35
+ #
36
+ # [:job_interlock_timeout]
37
+ # When a new value is needed, prevents new refresh jobs from being dispatched.
38
+ #
39
+ # Option examples:
40
+ # make_action_cacheable :active_user_count, expires_in: 12.hours, job_interlock_timeout: 10.minutes
41
+ # make_instance_action_cacheable :median_post_length, expires_in: 1.hour, job_interlock_timeout: 1.minute
42
+ delegate :cache_prefix, to: SidekiqSmartCache
43
+
44
+ def make_action_cacheable(name, options = {})
45
+ cache_tag = options[:cache_tag] || [cache_prefix, self.name, name].join('.')
46
+ cache_options = options.slice(:expires_in, :job_interlock_timeout)
47
+ instance_method = options[:instance_method]
48
+ without_caching_name = "#{name}_without_caching"
49
+ promise_method_name = "#{name}_promise"
50
+
51
+ promise_method = ->(*args) do
52
+ promise_args = cache_options.merge(
53
+ method: without_caching_name,
54
+ args: args
55
+ )
56
+
57
+ if instance_method
58
+ promise_args[:klass] = self.class.name
59
+ promise_args[:object_param] = to_param
60
+ else
61
+ promise_args[:klass] = self.name
62
+ end
63
+ SidekiqSmartCache::Promise.new(**promise_args)
64
+ end
65
+
66
+ if_available_method = ->(*args) do
67
+ send(promise_method_name, *args).existing_value
68
+ end
69
+
70
+ cache_tag_method = ->(*args) do
71
+ send(promise_method_name, *args).cache_tag
72
+ end
73
+
74
+ with_caching_method = ->(*args) do
75
+ send(promise_method_name, *args).fetch(24.hours) # close enough to forever?
76
+ end
77
+
78
+ without_caching_method = ->(*args) do
79
+ method(name).super_method.call(*args)
80
+ end
81
+
82
+ refresh_method = ->(*args) do
83
+ send(promise_method_name, *args).perform_now
84
+ end
85
+
86
+ prefix_module = Module.new
87
+ prefix_module.send(:define_method, "#{name}_if_available", if_available_method)
88
+ prefix_module.send(:define_method, "#{name}_cache_tag", cache_tag_method)
89
+ prefix_module.send(:define_method, name, with_caching_method)
90
+ prefix_module.send(:define_method, without_caching_name, without_caching_method)
91
+ prefix_module.send(:define_method, promise_method_name, promise_method)
92
+ prefix_module.send(:define_method, "refresh_#{name}", refresh_method)
93
+
94
+ if instance_method
95
+ prepend prefix_module
96
+ else
97
+ singleton_class.prepend prefix_module
98
+ end
99
+ end
100
+
101
+ def make_class_action_cacheable(name, options = {})
102
+ make_action_cacheable(name, options)
103
+ end
104
+
105
+ def make_instance_action_cacheable(name, options = {})
106
+ make_action_cacheable(name, options.merge(instance_method: true))
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,120 @@
1
+ module SidekiqSmartCache
2
+ class Promise
3
+ attr_accessor :klass, :object_param, :method, :expires_in, :args, :job_interlock_timeout
4
+ attr_accessor :timed_out
5
+ delegate :redis, :log, to: SidekiqSmartCache
6
+ delegate :working?, to: :interlock
7
+ delegate :value, to: :result
8
+ delegate :created_at, to: :result, prefix: true
9
+
10
+ def initialize(klass: nil, object: nil, object_param: nil, method:, args: nil,
11
+ cache_tag: nil, expires_in: 1.hour, job_interlock_timeout: nil)
12
+ if object
13
+ @klass = object.class.to_s
14
+ @object_param = object.to_param
15
+ elsif klass
16
+ @klass = klass.to_s
17
+ @object_param = object_param
18
+ else
19
+ raise "Must provide either klass or object"
20
+ end
21
+ raise "Must provide method" unless method
22
+ @method = method.to_s
23
+ @expires_in = expires_in.to_i
24
+ @job_interlock_timeout = job_interlock_timeout || @expires_in
25
+ @args = args
26
+ @cache_tag = cache_tag
27
+ end
28
+
29
+ def cache_tag
30
+ @cache_tag ||= begin
31
+ [
32
+ klass,
33
+ (object_param || '.'),
34
+ method,
35
+ (Digest::MD5.hexdigest(args.compact.to_json) if args.present?)
36
+ ].compact * '/'
37
+ end
38
+ end
39
+
40
+ def interlock
41
+ @_interlock ||= Interlock.new(cache_tag, job_interlock_timeout)
42
+ end
43
+
44
+ def perform_now
45
+ Worker.new.perform(klass, object_param, method, args, cache_tag, expires_in)
46
+ end
47
+
48
+ def enqueue_job!
49
+ Worker.perform_async(klass, object_param, method, args, cache_tag, expires_in)
50
+ end
51
+
52
+ def execute_and_wait!(timeout, stale_on_timeout: false)
53
+ execute_and_wait(timeout, raise_on_timeout: true, stale_on_timeout: stale_on_timeout)
54
+ end
55
+
56
+ def result
57
+ Result.load_from(cache_tag)
58
+ end
59
+
60
+ def stale_value_available?
61
+ !!result&.stale?
62
+ end
63
+
64
+ def existing_value(allow_stale: false)
65
+ if((existing = result) && (allow_stale || existing.fresh?))
66
+ existing.value
67
+ end
68
+ end
69
+
70
+ def ready_within?(timeout)
71
+ execute_and_wait(timeout)
72
+ !timed_out
73
+ end
74
+
75
+ def timed_out?
76
+ !!timed_out
77
+ end
78
+
79
+ def start
80
+ # Start a job if no other client has
81
+ if interlock.lock_job?
82
+ log('promise enqueuing calculator job')
83
+ enqueue_job!
84
+ else
85
+ log('promise calculator job already working')
86
+ end
87
+ self # for chaining
88
+ end
89
+
90
+ def execute_and_wait(timeout, raise_on_timeout: false, stale_on_timeout: false)
91
+ previous_result = result
92
+ if previous_result&.fresh?
93
+ # found a previously fresh message
94
+ @timed_out = false
95
+ return previous_result.value
96
+ else
97
+ start
98
+
99
+ # either a job was already running or we started one, now wait for an answer
100
+ if redis.wait_for_done_message(cache_tag, timeout.to_i)
101
+ # ready now, fetch it
102
+ log('promise calculator job finished')
103
+ @timed_out = false
104
+ result.value
105
+ elsif previous_result && stale_on_timeout
106
+ log('promise timed out awaiting calculator job, serving stale')
107
+ previous_result.value
108
+ else
109
+ log('promise timed out awaiting calculator job')
110
+ @timed_out = true
111
+ raise TimeoutError if raise_on_timeout
112
+ end
113
+ end
114
+ end
115
+
116
+ alias_method :fetch!, :execute_and_wait!
117
+ alias_method :fetch, :execute_and_wait
118
+
119
+ end
120
+ end
@@ -0,0 +1,94 @@
1
+ module SidekiqSmartCache
2
+ BLOCKING_COMMANDS = %i[ brpop ].freeze
3
+ COMMANDS = BLOCKING_COMMANDS + %i[ get set lpush expire flushdb del ].freeze
4
+
5
+ ERROR_TO_CATCH = if defined?(::RedisClient)
6
+ ::RedisClient::CommandError
7
+ else
8
+ ::Redis::CommandError
9
+ end
10
+
11
+ class Redis
12
+ def initialize(pool)
13
+ @pool = pool
14
+ end
15
+
16
+ def job_completion_key(key)
17
+ key + '/done'
18
+ end
19
+
20
+ def send_done_message(key)
21
+ lpush(job_completion_key(key), 'done')
22
+ expire(job_completion_key(key), 1)
23
+ end
24
+
25
+ def wait_for_done_message(key, timeout)
26
+ return true if defined?(Sidekiq::Testing) && Sidekiq::Testing.inline?
27
+
28
+ if brpop(job_completion_key(key), timeout: timeout.to_i)
29
+ # log_msg("got done message for #{key}")
30
+ send_done_message(key) # put it back for any other readers
31
+ true
32
+ end
33
+ end
34
+
35
+ def log_msg(msg) # WIP all this
36
+ Rails.logger.info("#{Time.now.iso8601(3)} #{Thread.current[:name]} redis #{msg}")
37
+ end
38
+
39
+ def method_missing(name, ...)
40
+ @pool.with do |r|
41
+ if COMMANDS.include? name
42
+ retryable = true
43
+ begin
44
+ # log_msg("#{name} #{args}")
45
+ if r.respond_to?(name)
46
+ # old redis gem implements methods including `brpop` and `flusdb`
47
+ r.send(name, ...)
48
+ elsif BLOCKING_COMMANDS.include? name
49
+ # support redis-client semantics
50
+ make_blocking_call(r, name, ...)
51
+ else
52
+ r.call(name.to_s.upcase, ...)
53
+ end
54
+ # stolen from sidekiq - Thanks Mike!
55
+ # Quietly consume this one and return nil
56
+ rescue ERROR_TO_CATCH => ex
57
+ # 2550 Failover can cause the server to become a replica, need
58
+ # to disconnect and reopen the socket to get back to the primary.
59
+ if retryable && ex.message =~ /READONLY/
60
+ r.disconnect!
61
+ retryable = false
62
+ retry
63
+ end
64
+ raise
65
+ end
66
+ else
67
+ super
68
+ end
69
+ end
70
+ end
71
+
72
+ def make_blocking_call(r, name, *args)
73
+ # The Redis `brpop` implementation seems to allow timeout to be the last argument
74
+ # or a key in a hash `timeout: 5`, so:
75
+ # `r.brpop('key1', 'key2', 5)` - wait five seconds for either queue
76
+ # `r.brpop('key1', 'key2', timeout: 5)` - same
77
+ # `r.brpop('key1', 'key2')` - wait forever on either queue
78
+ timeout = if args.last.is_a?(Hash)
79
+ options = args.pop
80
+ options[:timeout]
81
+ else
82
+ args.pop
83
+ end
84
+
85
+ # With RedisClient, the doc is a little thin, but it looks like we want to start with the timeout
86
+ # then the verb, then the array of keys
87
+ # and end with ... a 0?
88
+ blocking_call_args = [timeout, name.to_s.upcase] + args + [0]
89
+ r.blocking_call(*blocking_call_args)
90
+ rescue ::RedisClient::TimeoutError
91
+ nil # quietly return nil in this case
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,38 @@
1
+ class Result
2
+ class << self
3
+ delegate :redis, :allowed_classes, to: SidekiqSmartCache
4
+ end
5
+ attr_accessor :value, :valid_until, :created_at, :cache_prefix
6
+
7
+ def self.persist(cache_tag, value, expires_in)
8
+ structure = {
9
+ value: value,
10
+ created_at: Time.now,
11
+ valid_until: Time.now + expires_in,
12
+ cache_prefix: SidekiqSmartCache.cache_prefix
13
+ }
14
+ result_lifetime = 1.month.to_i # ??? maybe a function of expires_in ???
15
+ redis.set(cache_tag, structure.to_yaml)
16
+ redis.expire(cache_tag, result_lifetime)
17
+ end
18
+
19
+ def self.load_from(cache_tag)
20
+ raw = redis.get(cache_tag)
21
+ new(YAML.safe_load(raw, allowed_classes)) if raw
22
+ end
23
+
24
+ def initialize(result)
25
+ @value = result[:value]
26
+ @created_at = result[:created_at]
27
+ @valid_until = result[:valid_until]
28
+ @cache_prefix = result[:cache_prefix]
29
+ end
30
+
31
+ def fresh?
32
+ !stale?
33
+ end
34
+
35
+ def stale?
36
+ (Time.now > valid_until) || (cache_prefix != SidekiqSmartCache.cache_prefix)
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module SidekiqSmartCache
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,26 @@
1
+ require 'sidekiq'
2
+
3
+ module SidekiqSmartCache
4
+ class Worker
5
+ include Sidekiq::Worker
6
+ delegate :redis, to: SidekiqSmartCache
7
+
8
+ def perform(klass, instance_id, method, args, cache_tag, expires_in)
9
+ all_args = [method]
10
+ if args.is_a?(Array)
11
+ all_args += args
12
+ elsif args
13
+ all_args << args
14
+ end
15
+ subject = Object.const_get(klass)
16
+ subject = subject.find(instance_id) if instance_id
17
+ result = subject.send(*all_args)
18
+ Result.persist(cache_tag, result, expires_in)
19
+ redis.send_done_message(cache_tag)
20
+ result
21
+ ensure
22
+ # remove the interlock key
23
+ Interlock.new(cache_tag).clear
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ require "sidekiq_smart_cache/worker"
2
+ require "sidekiq_smart_cache/interlock"
3
+ require "sidekiq_smart_cache/model"
4
+ require "sidekiq_smart_cache/promise"
5
+ require "sidekiq_smart_cache/redis"
6
+ require "sidekiq_smart_cache/result"
7
+
8
+ module SidekiqSmartCache
9
+ class TimeoutError < StandardError; end
10
+
11
+ class << self
12
+ attr_accessor :cache_prefix, :redis_pool, :logger, :log_level
13
+
14
+ def allowed_classes
15
+ (@allowed_classes || []) + [Time, Symbol]
16
+ end
17
+
18
+ def allowed_classes=(extra_classes)
19
+ @allowed_classes = extra_classes
20
+ end
21
+ end
22
+
23
+ def self.log(message)
24
+ logger.send((log_level || :info), message) if logger
25
+ end
26
+
27
+ def self.redis
28
+ @redis ||= Redis.new(redis_pool)
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :sidekiq_smart_cache do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sidekiq_smart_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bill Kirtley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sidekiq
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Cache objects in redis, generated with thundering herd prevention via
42
+ sidekiq.
43
+ email:
44
+ - bill.kirtley@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - Rakefile
52
+ - lib/sidekiq_smart_cache.rb
53
+ - lib/sidekiq_smart_cache/interlock.rb
54
+ - lib/sidekiq_smart_cache/model.rb
55
+ - lib/sidekiq_smart_cache/promise.rb
56
+ - lib/sidekiq_smart_cache/redis.rb
57
+ - lib/sidekiq_smart_cache/result.rb
58
+ - lib/sidekiq_smart_cache/version.rb
59
+ - lib/sidekiq_smart_cache/worker.rb
60
+ - lib/tasks/sidekiq_smart_cache_tasks.rake
61
+ homepage: https://github.com/cluesque/sidekiq_smart_cache
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.3.3
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Cache objects in redis, generated with thundering herd prevention via sidekiq.
84
+ test_files: []