Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental - Add support for a new flavor of json serialization configuration #6209

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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