Skip to content

Commit

Permalink
Add support for a new flavor of json serialization configuration, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mperham committed Mar 1, 2024
1 parent 7b650ad commit 7b262bb
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 37 deletions.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -10,6 +10,7 @@ gem "activejob", RAILS_VERSION
gem "activerecord", RAILS_VERSION
gem "railties", RAILS_VERSION
gem "redis-client"
gem "benchmark-ips"
# gem "bumbler"
# gem "debug"

Expand Down
18 changes: 12 additions & 6 deletions lib/sidekiq.rb
Expand Up @@ -27,6 +27,7 @@
rescue LoadError
end

require "sidekiq/json"
require "sidekiq/config"
require "sidekiq/logger"
require "sidekiq/client"
Expand All @@ -35,8 +36,6 @@
require "sidekiq/worker_compatibility_alias"
require "sidekiq/redis_client_adapter"

require "json"

module Sidekiq
NAME = "Sidekiq"
LICENSE = "See LICENSE and the LGPL-3.0 for licensing details."
Expand All @@ -49,12 +48,19 @@ def self.server?
defined?(Sidekiq::CLI)
end

def self.load_json(string)
JSON.parse(string)
PARSE_OPTIONS = {}
GENERATE_OPTIONS = {}
def self.parse_json(string, options = PARSE_OPTIONS)
::JSON.parse(string, options)
end

def self.dump_json(object)
JSON.generate(object)
def self.generate_json(object, options = GENERATE_OPTIONS)
::JSON.generate(object, options)
end
# backwards compatibility
class << self
alias_method :load_json, :parse_json
alias_method :dump_json, :generate_json
end

def self.pro?
Expand Down
2 changes: 1 addition & 1 deletion lib/sidekiq/api.rb
Expand Up @@ -355,7 +355,7 @@ def initialize(item, queue_name = nil)
# @api private
def parse(item)
Sidekiq.load_json(item)
rescue JSON::ParserError
rescue ::JSON::ParserError
# If the job payload in Redis is invalid JSON, we'll load
# the item as an empty hash and store the invalid JSON as
# the job 'args' for display in the Web UI.
Expand Down
31 changes: 1 addition & 30 deletions lib/sidekiq/job_util.rb
Expand Up @@ -71,37 +71,8 @@ def normalized_hash(item_class)

private

RECURSIVE_JSON_UNSAFE = {
Integer => ->(val) {},
Float => ->(val) {},
TrueClass => ->(val) {},
FalseClass => ->(val) {},
NilClass => ->(val) {},
String => ->(val) {},
Array => ->(val) {
val.each do |e|
unsafe_item = RECURSIVE_JSON_UNSAFE[e.class].call(e)
return unsafe_item unless unsafe_item.nil?
end
nil
},
Hash => ->(val) {
val.each do |k, v|
return k unless String === k

unsafe_item = RECURSIVE_JSON_UNSAFE[v.class].call(v)
return unsafe_item unless unsafe_item.nil?
end
nil
}
}

RECURSIVE_JSON_UNSAFE.default = ->(val) { val }
RECURSIVE_JSON_UNSAFE.compare_by_identity
private_constant :RECURSIVE_JSON_UNSAFE

def json_unsafe?(item)
RECURSIVE_JSON_UNSAFE[item.class].call(item)
Sidekiq::JSON::RULES[item.class].call(item)
end
end
end
86 changes: 86 additions & 0 deletions lib/sidekiq/json.rb
@@ -0,0 +1,86 @@
# Sidekiq does not add a serialization step to job processing.
# All job serialization is expected to work with `JSON.parse/generate`
# but since the `json` gem does support optional extensions for core
# Ruby types, we can enable those extensions for the user in order
# to make transition from `perform_async(args)` -> `perform(args)`
# a little smoother.
#
# !!!!!!!!!!!!!!!!!! PLEASE NOTE !!!!!!!!!!!!!!!!!!!
#
# Symbols are not legal keys in JSON hashes so there's still
# effectively no way to support Symbols as Hash keys without a much
# more complex serialization step like ActiveJob implements.
#
# Good, supported types:
# perform_async(:foo, [:foo, 123], { "mike" => :foo })
#
# Bad, unsupported:
# perform_async(foo: 1, { :foo => 123 })
#
# Clean, easy serialization of Symbol'd keys remains an unsolved problem.
#

require "json"

module Sidekiq
module JSON
RULES = {
Integer => ->(val) {},
Float => ->(val) {},
TrueClass => ->(val) {},
FalseClass => ->(val) {},
NilClass => ->(val) {},
String => ->(val) {},
Array => ->(val) {
val.each do |e|
unsafe_item = RULES[e.class].call(e)
return unsafe_item unless unsafe_item.nil?
end
nil
},
Hash => ->(val) {
val.each do |k, v|
return k unless String === k

unsafe_item = RULES[v.class].call(v)
return unsafe_item unless unsafe_item.nil?
end
nil
}
}

RULES.default = ->(val) { val }
RULES.compare_by_identity

DEFAULT_VERSION = :v7
CURRENT_VERSION = DEFAULT_VERSION

# Activate the given JSON flavor globally.
def self.flavor!(ver = DEFAULT_VERSION)
return ver if ver == CURRENT_VERSION
raise ArgumentError, "Once set, Sidekiq's JSON flavor cannot be changed" if DEFAULT_VERSION != CURRENT_VERSION
raise ArgumentError, "Unknown JSON flavor `#{ver}`" unless ver == :v7 || ver == :v8

if ver == :v8
# this cannot be reverted; once v8 is activated in a process
# you cannot go back to v7.
require "json/add/core"
require "json/add/complex"
require "json/add/set"
require "json/add/rational"
require "json/add/bigdecimal"
Sidekiq::GENERATE_OPTIONS[:create_additions] = true
Sidekiq::PARSE_OPTIONS[:create_additions] = true
# Mark the core types as safe
[::Date, ::DateTime, ::Exception, ::Range, ::Regexp,
::Struct, ::Symbol, ::Time, ::Complex, ::Set,
::Rational, ::BigDecimal].each do |klass|
RULES[klass] = ->(_) {}
end
end

remove_const(:CURRENT_VERSION)
const_set(:CURRENT_VERSION, ver)
end
end
end
5 changes: 5 additions & 0 deletions test/helper.rb
Expand Up @@ -57,6 +57,11 @@ def capture_logging(cfg, lvl = Logger::INFO)
end
end

def global_change(&block)
pid = fork(&block)
Process.wait(pid) if pid
end

Signal.trap("TTIN") do
Thread.list.each do |thread|
puts "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread.name}"
Expand Down

0 comments on commit 7b262bb

Please sign in to comment.