sidekiq-delay_extensions 7.0.0 → 7.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95dda38ae3c61ad156898807a993f2e4ed94a1e9fa84e0acefc55e9eda8c563f
4
- data.tar.gz: c01d2df0317cb993a90ea42edf54c8cd8555a47abc6e9fc5aeca30cd3c609970
3
+ metadata.gz: '09822a56faaa3bddfd6accbbff7d36e709cd62d41123b3b42c54523ef74460a7'
4
+ data.tar.gz: f4c9f5b96cd11aff2f7790b5384b698f52c331a99260651f16f106632a96d15b
5
5
  SHA512:
6
- metadata.gz: c8ff4ae1215e5f1feb5b69c776711e9bdbd788d485d67f539e6b04aa47712149eff81f3a8148ad18fde812254107d1954754e67559847821cfd22cb414d64321
7
- data.tar.gz: 1c829791c9f5c30a59fd236864f07d9e3ae635bd928bc4df8d25bce692b1323f0a8f851692d49cafac4da446b8c87b4f84d8d33fcfe8e8151201f5de7ade08a4
6
+ metadata.gz: 1ec8ced317007db2bd65436b7c575c01d741efc86273f2d8407637612864636f4757bf8efe5c28f021435fa6f88d20b86f33915bc0bf81b22bdebeb5d4aa145f
7
+ data.tar.gz: 9f43ee40a9aad89506d1a2aa51ed041b97ce499e8fb8d156e67c4b6e3c21e308e3337eb46ac0f8b1d156d39fe0e61ceaee88533cc840f5dcf20fd99bcd2875bd
data/Changes.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  [See Sidekiq for its changes](https://github.com/mperham/sidekiq/blob/main/Changes.md)
4
4
 
5
+ Unreleased
6
+ ---------
7
+
8
+ 7.1.0
9
+ ---------
10
+
11
+ - New `Sidekiq::DelayExtensions::GenericJob` superclass for DelayedMailer, DelayedModel, DelayedClass
12
+ - it has a `_perform` method which accepts the unmarshalled and processed
13
+ `(target, method_name, *args, **kwargs)` and can be overridden or extended as needed.
14
+ - New (opt-in) `Sidekiq::DelayExtensions::GenericProxy` which can parse JSON or YAML delayed arguments
15
+ into a `target`, `method_name`, `args`, and `kwargs` before.
16
+ - New (opt-in) setting `Sidekiq::DelayExtensions.use_generic_proxy` (defaults to false).
17
+ - When false, there is no delayed proxy changes; the original `Sidekiq::DelayExtensions::Proxy` is used.
18
+ - When true, the new `Sidekiq::DelayExtensions::GenericProxy` is used, which handles both `*args` and `**kwargs` more naturally.
19
+ Be sure to test this works for you as expected when turning this on.
20
+ - Chore: Load YAML consistently via `::Sidekiq::DelayExtensions::YAML`
21
+
5
22
  7.0.0
6
23
  ---------
7
24
 
data/README.md CHANGED
@@ -32,6 +32,8 @@ In your initializers, include the line:
32
32
  Upgrading (IMPORTANT): Also add
33
33
 
34
34
  # To handle any existing delayed jobs at time of upgrade.
35
+ module Sidekiq::Extensions
36
+ end
35
37
  Sidekiq::Extensions::DelayedClass = Sidekiq::DelayExtensions::DelayedClass
36
38
  Sidekiq::Extensions::DelayedModel = Sidekiq::DelayExtensions::DelayedModel
37
39
  Sidekiq::Extensions::DelayedMailer = Sidekiq::DelayExtensions::DelayedMailer
@@ -12,12 +12,14 @@ module Sidekiq
12
12
  # UserMailer.delay.send_welcome_email(new_user)
13
13
  # UserMailer.delay_for(5.days).send_welcome_email(new_user)
14
14
  # UserMailer.delay_until(5.days.from_now).send_welcome_email(new_user)
15
- class DelayedMailer
16
- include Sidekiq::Job
17
-
18
- def perform(yml)
19
- (target, method_name, args) = YAML.load(yml)
20
- msg = target.public_send(method_name, *args)
15
+ class DelayedMailer < GenericJob
16
+ def _perform(target, method_name, *args, **kwargs)
17
+ msg =
18
+ if kwargs.empty?
19
+ target.public_send(method_name, *args)
20
+ else
21
+ target.public_send(method_name, *args, **kwargs)
22
+ end
21
23
  # The email method can return nil, which causes ActionMailer to return
22
24
  # an undeliverable empty message.
23
25
  if msg
@@ -29,16 +31,24 @@ module Sidekiq
29
31
  end
30
32
 
31
33
  module ActionMailer
34
+ def sidekiq_delay_proxy
35
+ if Sidekiq::DelayExtensions.use_generic_proxy
36
+ GenericProxy
37
+ else
38
+ Proxy
39
+ end
40
+ end
41
+
32
42
  def sidekiq_delay(options = {})
33
- Proxy.new(DelayedMailer, self, options)
43
+ sidekiq_delay_proxy.new(DelayedMailer, self, options)
34
44
  end
35
45
 
36
46
  def sidekiq_delay_for(interval, options = {})
37
- Proxy.new(DelayedMailer, self, options.merge("at" => Time.now.to_f + interval.to_f))
47
+ sidekiq_delay_proxy.new(DelayedMailer, self, options.merge("at" => Time.now.to_f + interval.to_f))
38
48
  end
39
49
 
40
50
  def sidekiq_delay_until(timestamp, options = {})
41
- Proxy.new(DelayedMailer, self, options.merge("at" => timestamp.to_f))
51
+ sidekiq_delay_proxy.new(DelayedMailer, self, options.merge("at" => timestamp.to_f))
42
52
  end
43
53
  alias_method :delay, :sidekiq_delay
44
54
  alias_method :delay_for, :sidekiq_delay_for
@@ -14,26 +14,28 @@ module Sidekiq
14
14
  # Please note, this is not recommended as this will serialize the entire
15
15
  # object to Redis. Your Sidekiq jobs should pass IDs, not entire instances.
16
16
  # This is here for backwards compatibility with Delayed::Job only.
17
- class DelayedModel
18
- include Sidekiq::Job
19
-
20
- def perform(yml)
21
- (target, method_name, args) = YAML.load(yml)
22
- target.__send__(method_name, *args)
23
- end
17
+ class DelayedModel < GenericJob
24
18
  end
25
19
 
26
20
  module ActiveRecord
21
+ def sidekiq_delay_proxy
22
+ if Sidekiq::DelayExtensions.use_generic_proxy
23
+ GenericProxy
24
+ else
25
+ Proxy
26
+ end
27
+ end
28
+
27
29
  def sidekiq_delay(options = {})
28
- Proxy.new(DelayedModel, self, options)
30
+ sidekiq_delay_proxy.new(DelayedModel, self, options)
29
31
  end
30
32
 
31
33
  def sidekiq_delay_for(interval, options = {})
32
- Proxy.new(DelayedModel, self, options.merge("at" => Time.now.to_f + interval.to_f))
34
+ sidekiq_delay_proxy.new(DelayedModel, self, options.merge("at" => Time.now.to_f + interval.to_f))
33
35
  end
34
36
 
35
37
  def sidekiq_delay_until(timestamp, options = {})
36
- Proxy.new(DelayedModel, self, options.merge("at" => timestamp.to_f))
38
+ sidekiq_delay_proxy.new(DelayedModel, self, options.merge("at" => timestamp.to_f))
37
39
  end
38
40
  alias_method :delay, :sidekiq_delay
39
41
  alias_method :delay_for, :sidekiq_delay_for
@@ -41,12 +41,13 @@ module Sidekiq
41
41
  # vs.
42
42
  # https://github.com/mperham/sidekiq/blob/v7.0.1/lib/sidekiq/api.rb#L374-L411
43
43
  def safe_load(content, default)
44
- yield(*YAML.load(content))
44
+ yield(*Sidekiq::DelayExtensions::YAML.unsafe_load(content))
45
45
  rescue => ex
46
46
  # #1761 in dev mode, it's possible to have jobs enqueued which haven't been loaded into
47
47
  # memory yet so the YAML can't be loaded.
48
48
  # TODO is this still necessary? Zeitwerk reloader should handle?
49
- Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless Sidekiq.options[:environment] == "development"
49
+ sidekiq_env = ::Sidekiq.default_configuration[:environment] || ENV["APP_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"]
50
+ ::Sidekiq.logger.warn "Unable to load YAML: #{ex.message}" unless sidekiq_env == "development"
50
51
  default
51
52
  end
52
53
  end
@@ -12,26 +12,28 @@ module Sidekiq
12
12
  # User.delay.delete_inactive
13
13
  # Wikipedia.delay.download_changes_for(Date.today)
14
14
  #
15
- class DelayedClass
16
- include Sidekiq::Job
17
-
18
- def perform(yml)
19
- (target, method_name, args) = YAML.load(yml)
20
- target.__send__(method_name, *args)
21
- end
15
+ class DelayedClass < GenericJob
22
16
  end
23
17
 
24
18
  module Klass
19
+ def sidekiq_delay_proxy
20
+ if Sidekiq::DelayExtensions.use_generic_proxy
21
+ GenericProxy
22
+ else
23
+ Proxy
24
+ end
25
+ end
26
+
25
27
  def sidekiq_delay(options = {})
26
- Proxy.new(DelayedClass, self, options)
28
+ sidekiq_delay_proxy.new(DelayedClass, self, options)
27
29
  end
28
30
 
29
31
  def sidekiq_delay_for(interval, options = {})
30
- Proxy.new(DelayedClass, self, options.merge("at" => Time.now.to_f + interval.to_f))
32
+ sidekiq_delay_proxy.new(DelayedClass, self, options.merge("at" => Time.now.to_f + interval.to_f))
31
33
  end
32
34
 
33
35
  def sidekiq_delay_until(timestamp, options = {})
34
- Proxy.new(DelayedClass, self, options.merge("at" => timestamp.to_f))
36
+ sidekiq_delay_proxy.new(DelayedClass, self, options.merge("at" => timestamp.to_f))
35
37
  end
36
38
  alias_method :delay, :sidekiq_delay
37
39
  alias_method :delay_for, :sidekiq_delay_for
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module DelayExtensions
5
+ class GenericJob
6
+ include Sidekiq::Job
7
+
8
+ def perform(yml)
9
+ if !Sidekiq::DelayExtensions.use_generic_proxy
10
+ (target, method_name, args) = ::Sidekiq::DelayExtensions::YAML.unsafe_load(yml)
11
+ return _perform(target, method_name, *args)
12
+ end
13
+ (target, method_name, args, kwargs) = ::Sidekiq::DelayExtensions::YAML.unsafe_load(yml)
14
+ if target.is_a?(String)
15
+ target_klass = target.safe_constantize
16
+ if target_klass
17
+ target = target_klass
18
+ else
19
+ fail NameError, "uninitialized constant #{target}. Peforming: #{yml.inspect}"
20
+ end
21
+ end
22
+ has_no_kwargs = kwargs.nil? || kwargs.empty? # rubocop:disable Rails/Blank
23
+ if has_no_kwargs
24
+ if args.is_a?(Array) && args.last.is_a?(Hash) # && args.last.keys.any? { |key| key.is_a?(Symbol) }
25
+ # rehydrate keys
26
+ kwargs = args.pop.symbolize_keys
27
+ has_no_kwargs = kwargs.empty?
28
+ elsif args.is_a?(Hash)
29
+ # rehydrate keys
30
+ kwargs = args.symbolize_keys
31
+ args = []
32
+ has_no_kwargs = kwargs.empty?
33
+ end
34
+ end
35
+ if has_no_kwargs
36
+ _perform(target, method_name, *args)
37
+ else
38
+ _perform(target, method_name, *args, **kwargs)
39
+ end
40
+ end
41
+
42
+ def _perform(target, method_name, *args, **kwargs)
43
+ if kwargs.empty?
44
+ target.__send__(method_name, *args)
45
+ else
46
+ target.__send__(method_name, *args, **kwargs)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -6,6 +6,9 @@ module Sidekiq
6
6
  module DelayExtensions
7
7
  SIZE_LIMIT = 8_192
8
8
 
9
+ singleton_class.attr_accessor :use_generic_proxy
10
+ self.use_generic_proxy = false
11
+
9
12
  class Proxy < BasicObject
10
13
  def initialize(performable, target, options = {})
11
14
  @performable = performable
@@ -29,5 +32,59 @@ module Sidekiq
29
32
  "display_class" => "#{@target}.#{name}"}.merge(@opts))
30
33
  end
31
34
  end
35
+
36
+ class GenericProxy < BasicObject
37
+ def initialize(performable, target, options = {})
38
+ @performable = performable
39
+ @target = target
40
+ @opts = options
41
+ end
42
+
43
+ def method_missing(name, *args, **kwargs) # rubocop:disable Style/MissingRespondToMissing
44
+ begin
45
+ has_no_kwargs = kwargs.nil? || kwargs.empty?
46
+ valid_json_args =
47
+ if has_no_kwargs
48
+ # if args.last.is_a?(Hash) && args.last.keys.any? { |key| key.is_a?(Symbol) }
49
+ # kwargs = args.pop.symbolize_keys
50
+ [*args]
51
+ else
52
+ [*args, ::JSON.parse(::JSON.dump(kwargs))]
53
+ end
54
+ obj = [@target.name, name&.to_s, valid_json_args]
55
+ marshalled = ::JSON.dump(obj)
56
+ rescue ::JSON::ParserError
57
+ obj = if kwargs&.any?
58
+ [@target.name, name&.to_s, *args, **kwargs]
59
+ else
60
+ [@target.name, name&.to_s, *args]
61
+ end
62
+ ::Sidekiq.logger.warn {
63
+ "Non-JSON args passed to Sidekiq delayed job. obj=#{obj.inspect}"
64
+ }
65
+ marshalled = ::YAML.dump(obj)
66
+ end
67
+ # Debug notes:
68
+ # marshalled = ::YAML.dump(valid_json_args)
69
+ # marshalled = ::YAML.to_json(valid_json_args)
70
+ # value = {
71
+ # target_name: @target.name,
72
+ # name: name,
73
+ # args: args,
74
+ # kwargs: kwargs,
75
+ # valid_json_args: valid_json_args,
76
+ # obj: obj,
77
+ # marshalled: marshalled,
78
+ # }
79
+ # ::STDOUT.puts value.inspect
80
+ if marshalled.size > SIZE_LIMIT
81
+ ::Sidekiq.logger.warn { "#{@target}.#{name} job argument is #{marshalled.bytesize} bytes, you should refactor it to reduce the size" }
82
+ end
83
+ valid_opts = @opts.stringify_keys
84
+ @performable.client_push({"class" => @performable,
85
+ "args" => [marshalled],
86
+ "display_class" => "#{@target}.#{name}"}.merge(valid_opts))
87
+ end
88
+ end
32
89
  end
33
90
  end
@@ -14,7 +14,7 @@ module Sidekiq
14
14
  jobs.select do |job|
15
15
  marshalled = job["args"][0]
16
16
  next unless marshalled.index(klass.to_s)
17
- YAML.load(marshalled)
17
+ ::Sidekiq::DelayExtensions::YAML.unsafe_load(marshalled)
18
18
  end
19
19
  end
20
20
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sidekiq
4
4
  module DelayExtensions
5
- VERSION = "7.0.0"
5
+ VERSION = "7.1.0"
6
6
  end
7
7
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ module DelayExtensions
5
+ module YAML
6
+ STDLIB_YAML = ::YAML
7
+
8
+ if STDLIB_YAML.respond_to?(:unsafe_load)
9
+ # https://github.com/ruby/psych/blob/v4.0.3/lib/psych.rb#L271-L323
10
+ # def self.unsafe_load(yaml, filename: nil, fallback: false, symbolize_names: false, freeze: false)
11
+ def self.unsafe_load(yaml, **kwargs)
12
+ STDLIB_YAML.unsafe_load(yaml, **kwargs)
13
+ end
14
+
15
+ # def self.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false, freeze: false)
16
+ def self.safe_load(yaml, **kwargs)
17
+ STDLIB_YAML.safe_load(yaml, **kwargs)
18
+ end
19
+
20
+ # def self.unsafe_load_file(filename, **kwargs)
21
+ def self.unsafe_load_file(filename, **kwargs)
22
+ STDLIB_YAML.unsafe_load_file(filename, **kwargs)
23
+ end
24
+
25
+ # def self.safe_load_file(filename, **kwargs)
26
+ def self.safe_load_file(filename, **kwargs)
27
+ STDLIB_YAML.safe_load_file(filename, **kwargs)
28
+ end
29
+ else
30
+ # https://github.com/ruby/psych/blob/v3.1.0/lib/psych.rb#L271-L328
31
+ # vs.
32
+ # https://github.com/ruby/psych/blob/v4.0.3/lib/psych.rb#L271-L323
33
+ # def self.load( yaml, filename: nil, fallback: false, symbolize_names: false)
34
+ # vs.
35
+ # def self.unsafe_load(yaml, filename: nil, fallback: false, symbolize_names: false, freeze: false)
36
+ def self.unsafe_load(yaml, **kwargs)
37
+ STDLIB_YAML.load(yaml, **kwargs)
38
+ end
39
+
40
+ # def self.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false)
41
+ # vs.
42
+ # def self.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false, freeze: false)
43
+ def self.safe_load(yaml, **kwargs)
44
+ STDLIB_YAML.safe_load(yaml, **kwargs)
45
+ end
46
+
47
+ # def self.load_file filename, fallback: false
48
+ # vs.
49
+ # def self.unsafe_load_file(filename, **kwargs)
50
+ def self.unsafe_load_file(filename, **kwargs)
51
+ STDLIB_YAML.load_file(filename, **kwargs)
52
+ end
53
+
54
+ # n/a
55
+ # vs.
56
+ # def self.safe_load_file(filename, **kwargs)
57
+ def self.safe_load_file(filename, **kwargs)
58
+ STDLIB_YAML.load_file(filename, **kwargs)
59
+ end
60
+ end
61
+ # NOTE(BF): In case someone calls YAML.load
62
+ # from within the gem.
63
+ singleton_class.alias_method :load, :unsafe_load
64
+ end
65
+ end
66
+ end
@@ -5,6 +5,10 @@ require "sidekiq"
5
5
  module Sidekiq
6
6
  module DelayExtensions
7
7
  def self.enable_delay!
8
+ require "sidekiq/delay_extensions/yaml"
9
+
10
+ require "sidekiq/delay_extensions/generic_job"
11
+
8
12
  if defined?(::ActiveSupport)
9
13
  require "sidekiq/delay_extensions/active_record"
10
14
  require "sidekiq/delay_extensions/action_mailer"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-delay_extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 7.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2024-01-26 00:00:00.000000000 Z
12
+ date: 2024-02-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sidekiq
@@ -42,9 +42,11 @@ files:
42
42
  - lib/sidekiq/delay_extensions/active_record.rb
43
43
  - lib/sidekiq/delay_extensions/api.rb
44
44
  - lib/sidekiq/delay_extensions/class_methods.rb
45
+ - lib/sidekiq/delay_extensions/generic_job.rb
45
46
  - lib/sidekiq/delay_extensions/generic_proxy.rb
46
47
  - lib/sidekiq/delay_extensions/testing.rb
47
48
  - lib/sidekiq/delay_extensions/version.rb
49
+ - lib/sidekiq/delay_extensions/yaml.rb
48
50
  - sidekiq-delay_extensions.gemspec
49
51
  homepage: https://github.com/gemhome/sidekiq-delay_extensions/wiki/Delayed-extensions
50
52
  licenses: