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

Attempt at making JSONAPI::Consumer connection's Thread-safe #22

Closed
wants to merge 4 commits 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ gemspec

gem 'rake'
gem 'minitest-ci'
gem 'pry'
gem 'pry'
3 changes: 2 additions & 1 deletion lib/jsonapi/consumer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module Helpers
autoload :Dirty, 'jsonapi/consumer/helpers/dirty'
autoload :DynamicAttributes, 'jsonapi/consumer/helpers/dynamic_attributes'
autoload :URI, 'jsonapi/consumer/helpers/uri'
autoload :ThreadsafeAttributes, 'jsonapi/consumer/helpers/threadsafe_attributes'
end

module Linking
Expand Down Expand Up @@ -67,4 +68,4 @@ module Query
autoload :Schema, 'jsonapi/consumer/schema'
autoload :Utils, 'jsonapi/consumer/utils'
end
end
end
4 changes: 2 additions & 2 deletions lib/jsonapi/consumer/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ class Connection

attr_reader :faraday

def initialize(options = {})
site = options.fetch(:site)
def initialize(site, options = {})
site = site
connection_options = options.slice(:proxy, :ssl, :request, :headers, :params)
adapter_options = Array(options.fetch(:adapter, Faraday.default_adapter))
@faraday = Faraday.new(site, connection_options) do |builder|
Expand Down
66 changes: 66 additions & 0 deletions lib/jsonapi/consumer/helpers/threadsafe_attributes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'active_support/core_ext/object/duplicable'

module JSONAPI::Consumer::Helpers
module ThreadsafeAttributes
def self.included(klass)
klass.extend(ClassMethods)
end

module ClassMethods
def threadsafe_attribute(*attrs)
main_thread = Thread.main # remember this, because it could change after forking

attrs.each do |attr|
define_method attr do
get_threadsafe_attribute(attr, main_thread)
end

define_method "#{attr}=" do |value|
set_threadsafe_attribute(attr, value, main_thread)
end

define_method "#{attr}_defined?" do
threadsafe_attribute_defined?(attr, main_thread)
end
end
end
end

private

def get_threadsafe_attribute(name, main_thread)
if threadsafe_attribute_defined_by_thread?(name, Thread.current)
get_threadsafe_attribute_by_thread(name, Thread.current)
elsif threadsafe_attribute_defined_by_thread?(name, main_thread)
value = get_threadsafe_attribute_by_thread(name, main_thread)
value = value.dup if value.duplicable?
set_threadsafe_attribute_by_thread(name, value, Thread.current)
value
end
end

def set_threadsafe_attribute(name, value, main_thread)
set_threadsafe_attribute_by_thread(name, value, Thread.current)
unless threadsafe_attribute_defined_by_thread?(name, main_thread)
set_threadsafe_attribute_by_thread(name, value, main_thread)
end
end

def threadsafe_attribute_defined?(name, main_thread)
threadsafe_attribute_defined_by_thread?(name, Thread.current) || ((Thread.current != main_thread) && threadsafe_attribute_defined_by_thread?(name, main_thread))
end

def get_threadsafe_attribute_by_thread(name, thread)
thread.thread_variable_get "active.resource.#{name}.#{self.object_id}"
end

def set_threadsafe_attribute_by_thread(name, value, thread)
thread.thread_variable_set "active.resource.#{name}.#{self.object_id}.defined", true
thread.thread_variable_set "active.resource.#{name}.#{self.object_id}", value
end

def threadsafe_attribute_defined_by_thread?(name, thread)
thread.thread_variable_get "active.resource.#{name}.#{self.object_id}.defined"
end
end
end
2 changes: 1 addition & 1 deletion lib/jsonapi/consumer/query/requestor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def resource_path(parameters)
end

def request(type, path, params)
klass.parser.parse(klass, connection.run(type, path, params, klass.custom_headers))
klass.parser.parse(klass, connection.run(type, path, params, klass.headers))
end

end
Expand Down
129 changes: 94 additions & 35 deletions lib/jsonapi/consumer/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@ class Resource
attr_accessor :last_result_set,
:links,
:relationships
class_attribute :site,
:primary_key,
class_attribute :primary_key,
:parser,
:paginator,
:connection_class,
:connection_object,
:connection_options,
:query_builder,
:linker,
:relationship_linker,
Expand All @@ -32,8 +28,6 @@ class Resource
self.primary_key = :id
self.parser = Parsers::Parser
self.paginator = Paginating::Paginator
self.connection_class = Connection
self.connection_options = {}
self.query_builder = Query::Builder
self.linker = Linking::Links
self.relationship_linker = Relationships::Relations
Expand All @@ -53,6 +47,8 @@ class Resource

class << self
extend Forwardable
include Helpers::ThreadsafeAttributes
threadsafe_attribute :_connection, :_connection_class, :_site, :_connection_options, :_headers
def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last

# The table name for this resource. i.e. Article -> articles, Person -> people
Expand All @@ -62,6 +58,71 @@ def table_name
route_formatter.format(resource_name.pluralize)
end

def connection_class
if _connection_class_defined?
_connection_class
elsif superclass != Object && superclass.connection_class
superclass.connection_class
else
Connection
end
end

def connection_class=(connection_class)
self._connection = nil

if connection_class.nil?
self._connection_class = nil
else
self._connection_class = connection_class
end
end

# Returns the connection options to be used in Faraday
#
# @return [Hash,NilClass]
def connection_options
if _connection_options_defined?
_connection_options
elsif superclass != Object && superclass.connection_options
superclass.connection_options.dup.freeze
end
end

# Sets the connection options directly in Faraday
#
def connection_options=(connection_options)
self._connection = nil

if connection_options.nil?
self._connection_options = nil
else
self._connection_options = connection_options
end
end

# Sets the URI.
# The site variable is required for JSONAPI::Consumer to work.
def site
if _site_defined?
_site
elsif superclass != Object && superclass.site
superclass.site.dup.freeze
end
end

# Sets the URI.
# The site variable is required for JSONAPI::Consumer to work.
def site=(site)
self._connection = nil

if site.nil?
self._site = nil
else
self._site = site
end
end

# The name of a single resource. i.e. Article -> article, Person -> person
#
# @return [String]
Expand Down Expand Up @@ -98,9 +159,15 @@ def load(params)
# Return/build a connection object
#
# @return [Connection] The connection to the json api server
def connection(rebuild = false, &block)
_build_connection(rebuild, &block)
connection_object
def connection(rebuild = false)
if _connection_defined? || superclass == Object
self._connection = connection_class.new(site, connection_options.to_h).tap do |conn|
yield(conn) if block_given?
end if rebuild || _connection.nil?
_connection
else
superclass.connection
end
end

# Param names that will be considered path params. They will be used
Expand Down Expand Up @@ -142,21 +209,28 @@ def create(attributes = {})
# @param headers [Hash] The headers to send along
# @param block [Block] The block where headers will be set for
def with_headers(headers)
self._custom_headers = headers
self.headers = headers
yield
ensure
self._custom_headers = {}
self.headers = {}
end

def headers=(h)
self._headers = headers.merge(h)
end

# The current custom headers to send with any request made by this
# resource class. This supports inheritance so it only needs to be
# set on the base class.
#
# @return [Hash] Headers
def custom_headers
return _header_store.to_h if superclass == Object

superclass.custom_headers.merge(_header_store.to_h)
def headers
self._headers ||= {}
if superclass != Object && superclass.headers
self._headers = superclass.headers.merge(_headers)
else
_headers
end
end

# Run a command wrapped in an Authorization header
Expand All @@ -169,9 +243,9 @@ def authorize_with(jwt, &block)
#
def authorize_with=(jwt)
if jwt.nil?
self._custom_headers = {authorization: nil}
self.headers = {authorization: nil}
else
self._custom_headers = {authorization: %(Bearer #{jwt})}
self.headers = {authorization: %(Bearer #{jwt})}
end
end

Expand All @@ -183,14 +257,14 @@ def clear_authorization!

# @return [String] The Authorization header
def authorized_as
custom_headers[:authorization]
headers[:authorization]
end

# Returns based on the presence of an Authorization header
#
# @return [Boolean]
def authorized?
!custom_headers[:authorization].nil?
!headers[:authorization].nil?
end

# Returns the requestor for this resource class
Expand Down Expand Up @@ -315,21 +389,6 @@ def _set_prefix_path(attrs)
def _new_scope
query_builder.new(self)
end

def _custom_headers=(headers)
_header_store.replace(headers)
end

def _header_store
Thread.current["json_api_client-#{resource_name}"] ||= {}
end

def _build_connection(rebuild = false)
return connection_object unless connection_object.nil? || rebuild
self.connection_object = connection_class.new(connection_options.merge(site: site)).tap do |conn|
yield(conn) if block_given?
end
end
end

# Instantiate a new resource object
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "bundler/setup"
require "pry"
require "jsonapi/consumer"

require 'minitest/autorun'
Expand Down
Loading