zeevex_threadsafe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +32 -0
- data/lib/zeevex_threadsafe/aliasing.rb +77 -0
- data/lib/zeevex_threadsafe/rails/request_globals.rb +106 -0
- data/lib/zeevex_threadsafe/rails/thread_pool_release.rb +12 -0
- data/lib/zeevex_threadsafe/rails.rb +4 -0
- data/lib/zeevex_threadsafe/thread_locals.rb +90 -0
- data/lib/zeevex_threadsafe/thread_safer.rb +115 -0
- data/lib/zeevex_threadsafe/version.rb +3 -0
- data/lib/zeevex_threadsafe.rb +3 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/thread_locals_spec.rb +372 -0
- data/zeevex_threadsafe.gemspec +27 -0
- metadata +126 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
namespace :spec do
|
10
|
+
desc "Run on three Rubies"
|
11
|
+
task :platforms do
|
12
|
+
current = %x{rvm-prompt v}
|
13
|
+
|
14
|
+
fail = false
|
15
|
+
%w{1.8.7 1.9.2}.each do |version|
|
16
|
+
puts "Switching to #{version}"
|
17
|
+
Bundler.with_clean_env do
|
18
|
+
system %{bash -c 'source ~/.rvm/scripts/rvm && rvm #{version} && bundle exec rake spec'}
|
19
|
+
end
|
20
|
+
if $?.exitstatus != 0
|
21
|
+
fail = true
|
22
|
+
break
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
system %{rvm #{current}}
|
27
|
+
|
28
|
+
exit (fail ? 1 : 0)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
task :default => 'spec'
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#
|
2
|
+
# this is a copy of ActiveSupport::CoreExtensions::Module
|
3
|
+
#
|
4
|
+
|
5
|
+
module ZeevexThreadsafe
|
6
|
+
module Aliasing
|
7
|
+
# Encapsulates the common pattern of:
|
8
|
+
#
|
9
|
+
# alias_method :foo_without_feature, :foo
|
10
|
+
# alias_method :foo, :foo_with_feature
|
11
|
+
#
|
12
|
+
# With this, you simply do:
|
13
|
+
#
|
14
|
+
# alias_method_chain :foo, :feature
|
15
|
+
#
|
16
|
+
# And both aliases are set up for you.
|
17
|
+
#
|
18
|
+
# Query and bang methods (foo?, foo!) keep the same punctuation:
|
19
|
+
#
|
20
|
+
# alias_method_chain :foo?, :feature
|
21
|
+
#
|
22
|
+
# is equivalent to
|
23
|
+
#
|
24
|
+
# alias_method :foo_without_feature?, :foo?
|
25
|
+
# alias_method :foo?, :foo_with_feature?
|
26
|
+
#
|
27
|
+
# so you can safely chain foo, foo?, and foo! with the same feature.
|
28
|
+
def alias_method_chain(target, feature)
|
29
|
+
# Strip out punctuation on predicates or bang methods since
|
30
|
+
# e.g. target?_without_feature is not a valid method name.
|
31
|
+
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
|
32
|
+
yield(aliased_target, punctuation) if block_given?
|
33
|
+
|
34
|
+
with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
|
35
|
+
|
36
|
+
alias_method without_method, target
|
37
|
+
alias_method target, with_method
|
38
|
+
|
39
|
+
case
|
40
|
+
when public_method_defined?(without_method)
|
41
|
+
public target
|
42
|
+
when protected_method_defined?(without_method)
|
43
|
+
protected target
|
44
|
+
when private_method_defined?(without_method)
|
45
|
+
private target
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Allows you to make aliases for attributes, which includes
|
50
|
+
# getter, setter, and query methods.
|
51
|
+
#
|
52
|
+
# Example:
|
53
|
+
#
|
54
|
+
# class Content < ActiveRecord::Base
|
55
|
+
# # has a title attribute
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# class Email < Content
|
59
|
+
# alias_attribute :subject, :title
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# e = Email.find(1)
|
63
|
+
# e.title # => "Superstars"
|
64
|
+
# e.subject # => "Superstars"
|
65
|
+
# e.subject? # => true
|
66
|
+
# e.subject = "Megastars"
|
67
|
+
# e.title # => "Megastars"
|
68
|
+
def alias_attribute(new_name, old_name)
|
69
|
+
module_eval <<-STR, __FILE__, __LINE__ + 1
|
70
|
+
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
|
71
|
+
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
|
72
|
+
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
|
73
|
+
STR
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require_dependency "weakref"
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
rescue LoadError
|
6
|
+
require 'zeevex_threadsafe/aliasing'
|
7
|
+
end
|
8
|
+
|
9
|
+
module ZeevexThreadsafe
|
10
|
+
module Rails
|
11
|
+
class RequestGlobals
|
12
|
+
class << self
|
13
|
+
|
14
|
+
REQUEST_VARNAME = "_zx_rg_request"
|
15
|
+
INSTANCE_VARNAME = "@_zx_request_globals"
|
16
|
+
|
17
|
+
def request
|
18
|
+
ref = Thread.current[REQUEST_VARNAME]
|
19
|
+
ref && ref.class == WeakRef && ref.weakref_alive? ? ref.__getobj__ : nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def request=(request)
|
23
|
+
Thread.current[REQUEST_VARNAME] = (request ? WeakRef.new(request) : nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](name)
|
27
|
+
hash[name]
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(name, val)
|
31
|
+
hash(true)[name] = val
|
32
|
+
end
|
33
|
+
|
34
|
+
def reset
|
35
|
+
hash && hash.clear
|
36
|
+
Thread.current[REQUEST_VARNAME] = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def define_request_global_accessors(base, name, key = nil)
|
41
|
+
key ||= name
|
42
|
+
base.class_eval do
|
43
|
+
define_method name do
|
44
|
+
ZeevexThreadsafe::Rails::RequestGlobals[key]
|
45
|
+
end
|
46
|
+
|
47
|
+
define_method (name.to_s + "=").to_sym do |value|
|
48
|
+
ZeevexThreadsafe::Rails::RequestGlobals[key] = value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def hash(strict = false)
|
56
|
+
req = request
|
57
|
+
unless req
|
58
|
+
if strict
|
59
|
+
raise "No current request scope for RequestGlobal"
|
60
|
+
else
|
61
|
+
return {}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
hash = req.instance_variable_get(INSTANCE_VARNAME)
|
66
|
+
if !hash
|
67
|
+
hash = {}
|
68
|
+
req.instance_variable_set(INSTANCE_VARNAME, hash)
|
69
|
+
end
|
70
|
+
hash
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
module Controller
|
76
|
+
def self.included(klass)
|
77
|
+
klass.class_eval do
|
78
|
+
alias_method_chain :process, :request_globals
|
79
|
+
protected :process_without_request_globals, :process_with_request_globals
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def process_with_request_globals(request, response, method = :perform_action, *arguments)
|
84
|
+
ZeevexThreadsafe::Rails::RequestGlobals.request = request
|
85
|
+
process_without_request_globals(request, response, method, *arguments)
|
86
|
+
ensure
|
87
|
+
ZeevexThreadsafe::Rails::RequestGlobals.reset
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
module Accessors
|
92
|
+
def self.included(klass)
|
93
|
+
klass.extend ClassMethods
|
94
|
+
end
|
95
|
+
|
96
|
+
module ClassMethods
|
97
|
+
def request_global_accessor(name)
|
98
|
+
ZeevexThreadsafe::Rails::RequestGlobals.define_request_global_accessors(self.class, name)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
#
|
2
|
+
# Usage:
|
3
|
+
# class Foo
|
4
|
+
# include ThreadLocals
|
5
|
+
# thread_local :instance_var_name, :instance_var_name2 # , ...
|
6
|
+
# cthread_local :class_var_name, :class_var_name2 # , ...
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# Foo.class_var_name = "thread specific value on class"
|
10
|
+
# Foo.new.instance_var_name = "thread specific value on instance"
|
11
|
+
#
|
12
|
+
# A hash of options which may be passed in to control the visibility
|
13
|
+
# (public, private, protected) and default value of the variable.
|
14
|
+
#
|
15
|
+
# class Foo
|
16
|
+
# thread_local :example, :visibility => :protected, :default => 42
|
17
|
+
# end
|
18
|
+
# obj = Foo.new
|
19
|
+
# obj.example => 42
|
20
|
+
# obj.example = "non-default value"
|
21
|
+
# obj.example # => "non-default-value"
|
22
|
+
#
|
23
|
+
#
|
24
|
+
module ZeevexThreadsafe::ThreadLocals
|
25
|
+
def self.included(klass)
|
26
|
+
klass.extend ClassMethods
|
27
|
+
klass.send :include, InstanceMethods
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
private
|
32
|
+
|
33
|
+
# remove the thread local maps for threads that are no longer active.
|
34
|
+
# likely to be painful if many threads are running.
|
35
|
+
#
|
36
|
+
# must be called manually; otherwise this object may accumulate lots of garbage
|
37
|
+
# if it is used from many different threads.
|
38
|
+
def _thread_local_clean
|
39
|
+
ids = Thread.list.map &:object_id
|
40
|
+
(@_thread_local_threads.keys - ids).each { |key| @_thread_local_threads.delete(key) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
def thread_local(*args, &block)
|
46
|
+
UtilityMethods.define_thread_local_accessors(self, args, block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def cthread_local(*args, &block)
|
50
|
+
UtilityMethods.define_thread_local_accessors(self.class, args, block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module UtilityMethods
|
55
|
+
|
56
|
+
def self.thread_local_hash(base, autocreate = false)
|
57
|
+
base.instance_eval do
|
58
|
+
if autocreate
|
59
|
+
@_thread_local_threads ||= {}
|
60
|
+
@_thread_local_threads[Thread.current.object_id] ||= {}
|
61
|
+
else
|
62
|
+
@_thread_local_threads ? @_thread_local_threads[Thread.current.object_id] : nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.define_thread_local_accessors(base, args, block = nil)
|
68
|
+
options = args[-1].instance_of?(Hash) ? args.pop : {}
|
69
|
+
args.each do |name|
|
70
|
+
key = name.to_sym
|
71
|
+
base.class_eval do
|
72
|
+
define_method name.to_sym do
|
73
|
+
hash = ZeevexThreadsafe::ThreadLocals::UtilityMethods.thread_local_hash(self)
|
74
|
+
return hash[key] if hash && hash.key?(key)
|
75
|
+
return block.call(self, key) if block
|
76
|
+
return options[:default]
|
77
|
+
end
|
78
|
+
|
79
|
+
define_method (name.to_s + "=").to_sym do |value|
|
80
|
+
ZeevexThreadsafe::ThreadLocals::UtilityMethods.thread_local_hash(self, true)[key] = value
|
81
|
+
end
|
82
|
+
|
83
|
+
if options[:visibility]
|
84
|
+
send options[:visibility], name.to_sym, (name.to_s + "=").to_sym
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
rescue LoadError
|
6
|
+
require 'zeevex_threadsafe/aliasing'
|
7
|
+
end
|
8
|
+
|
9
|
+
module ZeevexThreadsafe
|
10
|
+
module ThreadSafer
|
11
|
+
|
12
|
+
def self.included base
|
13
|
+
base.extend ZeevexThreadsafe::ThreadSafer::ClassMethods
|
14
|
+
base.class_eval do
|
15
|
+
include ZeevexThreadsafe::ThreadSafer::InstanceMethods
|
16
|
+
|
17
|
+
@delayed_thread_safe_methods = []
|
18
|
+
|
19
|
+
if method_defined?(:method_added)
|
20
|
+
class << self
|
21
|
+
alias_method_chain :method_added, :thread_safe
|
22
|
+
end
|
23
|
+
else
|
24
|
+
class << self
|
25
|
+
alias_method :method_added, :method_added_with_thread_safe
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def method_added_with_thread_safe(method)
|
33
|
+
if !method.to_s.match(/_without_mutex$/) && @delayed_thread_safe_methods.include?(method)
|
34
|
+
@delayed_thread_safe_methods.delete method
|
35
|
+
make_thread_safe(method)
|
36
|
+
end
|
37
|
+
method_added_without_thread_safe(method) if method_defined?(:method_added_without_thread_safe)
|
38
|
+
end
|
39
|
+
|
40
|
+
def make_thread_safe *methods
|
41
|
+
methods.each do |method|
|
42
|
+
method = method.to_sym
|
43
|
+
if method_defined?(method)
|
44
|
+
make_thread_safe_now method
|
45
|
+
else
|
46
|
+
@delayed_thread_safe_methods << method
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def make_thread_safe_now *methods
|
52
|
+
methods.each do |method|
|
53
|
+
old_name = method.to_sym
|
54
|
+
new_name = (old_name.to_s + "_without_mutex").to_sym
|
55
|
+
alias_method new_name, old_name
|
56
|
+
myprox = lambda do |*args, &block|
|
57
|
+
_ts_mutex.synchronize do
|
58
|
+
@in_thread_safe_method = old_name
|
59
|
+
res = __send__ new_name, *args, &block
|
60
|
+
@in_thread_safe_method = nil
|
61
|
+
res
|
62
|
+
end
|
63
|
+
end
|
64
|
+
define_method old_name, myprox
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
module InstanceMethods
|
71
|
+
def _ts_mutex
|
72
|
+
@_ts_mutex ||= Mutex.new
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# class Foo
|
81
|
+
# include ZeevexThreadsafe::ThreadSafer
|
82
|
+
#
|
83
|
+
# class << self
|
84
|
+
# include ZeevexThreadsafe::ThreadSafer
|
85
|
+
# end
|
86
|
+
# def foo_unsafe
|
87
|
+
# puts "unsafe: #{@in_thread_safe_method}"
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# def foo_safe
|
91
|
+
# puts "Safe: #{@in_thread_safe_method}"
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# make_thread_safe :foo_safe, :foo_safe_delayed, :hibbity, :hop
|
95
|
+
#
|
96
|
+
# def foo_safe_delayed
|
97
|
+
# puts "safe delayed: #{@in_thread_safe_method}"
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# class << self
|
101
|
+
# def cmethod
|
102
|
+
# puts "in cmethod: #{@in_thread_safe_method}"
|
103
|
+
# end
|
104
|
+
# make_thread_safe :cmethod
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# Foo.new.foo_unsafe
|
110
|
+
# Foo.new.foo_safe
|
111
|
+
# Foo.new.foo_safe_delayed
|
112
|
+
# Foo.cmethod
|
113
|
+
# Foo.class_eval do
|
114
|
+
# puts "remaining delayed methods are #{@delayed_thread_safe_methods.inspect}"
|
115
|
+
# end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,372 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
3
|
+
|
4
|
+
require 'zeevex_threadsafe/thread_locals'
|
5
|
+
|
6
|
+
class ThreadLocalTestClass
|
7
|
+
include ZeevexThreadsafe::ThreadLocals
|
8
|
+
thread_local(:block1, :block2) { |obj,key| "DEFAULT FOR #{obj.class}.#{key.to_s}" }
|
9
|
+
thread_local :plain
|
10
|
+
thread_local :mult1, :mult2
|
11
|
+
thread_local :default_var, :default => 100
|
12
|
+
thread_local :protected_var, :default => 200, :visibility => :protected
|
13
|
+
thread_local :private_var, :default => 200, :visibility => :private
|
14
|
+
thread_local :public_var, :default => 200, :visibility => :public
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
thread_local :contextually_private_var
|
19
|
+
|
20
|
+
public
|
21
|
+
|
22
|
+
thread_local :contextually_public_var
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassDuping
|
26
|
+
def class_name
|
27
|
+
@@class_name || self.class.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def class_name=(name)
|
31
|
+
@@class_name = name
|
32
|
+
end
|
33
|
+
|
34
|
+
def dupe_for_testing(name = self.name)
|
35
|
+
self.dup.tap do |clazz|
|
36
|
+
clazz.class_name = name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class ThreadLocalClassLevel
|
42
|
+
include ZeevexThreadsafe::ThreadLocals
|
43
|
+
extend ClassDuping
|
44
|
+
cthread_local(:block1, :block2) { |obj,key| "DEFAULT FOR #{obj.class_name}.#{key.to_s}" }
|
45
|
+
cthread_local :plain
|
46
|
+
cthread_local :mult1, :mult2
|
47
|
+
cthread_local :default_var, :default => 100
|
48
|
+
cthread_local :protected_var, :default => 200, :visibility => :protected
|
49
|
+
cthread_local :private_var, :default => 200, :visibility => :private
|
50
|
+
cthread_local :public_var, :default => 200, :visibility => :public
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
cthread_local :contextually_private_var
|
55
|
+
|
56
|
+
public
|
57
|
+
|
58
|
+
cthread_local :contextually_public_var
|
59
|
+
end
|
60
|
+
|
61
|
+
class ThreadLocalClassLevel2
|
62
|
+
include ZeevexThreadsafe::ThreadLocals
|
63
|
+
extend ClassDuping
|
64
|
+
|
65
|
+
cthread_local :plain
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
describe ZeevexThreadsafe::ThreadLocals do
|
70
|
+
|
71
|
+
context "class inclusion" do
|
72
|
+
it "should not add class methods to a class unless included" do
|
73
|
+
Object.should_not respond_to(:thread_local)
|
74
|
+
Object.should_not respond_to(:cthread_local)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should add class methods to a class when included" do
|
78
|
+
ThreadLocalTestClass.should respond_to(:thread_local)
|
79
|
+
ThreadLocalTestClass.should respond_to(:cthread_local)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
context "instance thread locals" do
|
85
|
+
let :instance do
|
86
|
+
ThreadLocalTestClass.new
|
87
|
+
end
|
88
|
+
let :instance2 do
|
89
|
+
ThreadLocalTestClass.new
|
90
|
+
end
|
91
|
+
subject { instance }
|
92
|
+
context "method definition" do
|
93
|
+
|
94
|
+
[:block1, :block2, :plain, :mult1, :mult2, :block1, :default_var,
|
95
|
+
:protected_var, :public_var, :contextually_private_var, :contextually_public_var].each do |key|
|
96
|
+
it "should have defined reader for #{key}" do
|
97
|
+
instance.should respond_to(key)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should have defined writer for #{key}" do
|
101
|
+
instance.should respond_to((key.to_s + "=").to_sym)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
[:private_var].each do |key|
|
106
|
+
it "should have defined reader for #{key}" do
|
107
|
+
instance.private_methods.should include(key.to_s)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should have defined writer for #{key}" do
|
111
|
+
instance.private_methods.should include(key.to_s + "=")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "default value" do
|
117
|
+
[:plain, :mult1, :mult2,
|
118
|
+
:contextually_private_var, :contextually_public_var].each do |key|
|
119
|
+
it "should returns nil unless configured otherwise" do
|
120
|
+
instance.send(key).should be_nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should return constant value if :default option provided" do
|
125
|
+
instance.default_var.should == 100
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should return computed value if :block option provided" do
|
129
|
+
instance.block1.should == "DEFAULT FOR ThreadLocalTestClass.block1"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "when setting" do
|
134
|
+
it "should return the newly set value after being set" do
|
135
|
+
instance.block1 = "BLOCK!"
|
136
|
+
instance.block1.should == "BLOCK!"
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should keep values separately for two instances" do
|
140
|
+
instance.block1 = "INSTANCE1 VALUE"
|
141
|
+
instance2.block1 = "INSTANCE2 VALUE"
|
142
|
+
instance.block1.should == "INSTANCE1 VALUE"
|
143
|
+
instance2.block1.should == "INSTANCE2 VALUE"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "visibility" do
|
148
|
+
it "should define methods as public by default" do
|
149
|
+
instance.public_methods.should include("plain", "plain=", "mult1", "mult1=")
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should define methods as protected when :visibility => :protected" do
|
153
|
+
instance.protected_methods.should include("protected_var", "protected_var=")
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should define methods as private when :visibility => :private" do
|
157
|
+
instance.private_methods.should include("private_var", "private_var=")
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should define methods as public when :visibility is not provided, even in a private context" do
|
161
|
+
instance.public_methods.should include("contextually_private_var", "contextually_private_var=")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context "thread locality" do
|
166
|
+
it "should return the same value in the same thread when called consecutively" do
|
167
|
+
instance.mult1 = "barbar"
|
168
|
+
instance.mult1.should == "barbar"
|
169
|
+
instance.mult1.should == "barbar"
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should return the default value in a new thread" do
|
173
|
+
instance.default_var = "FOO"
|
174
|
+
instance.default_var.should == "FOO"
|
175
|
+
Thread.new { @res = instance.default_var }.join
|
176
|
+
@res.should == 100
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should not affect the value in one thread when set in a different thread" do
|
180
|
+
instance.default_var = "FOO"
|
181
|
+
instance.default_var.should == "FOO"
|
182
|
+
Thread.new { instance.default_var = 2500 }.join
|
183
|
+
instance.default_var.should == "FOO"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context "per thread book-keeping" do
|
188
|
+
let :book do
|
189
|
+
instance.instance_variable_get("@_thread_local_threads")
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should have no book-keeping data when first created" do
|
193
|
+
book.should == nil
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should have no book-keeping data for default value access" do
|
197
|
+
instance.block1
|
198
|
+
book.should == nil
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should have one thread's worth of book-keeping data when first variable is set" do
|
202
|
+
instance.mult1 = 3000
|
203
|
+
book.should be_instance_of(Hash)
|
204
|
+
book.should have(1).item
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should have two thread's worth of book-keeping data when variable is set in two threads" do
|
208
|
+
instance.mult1 = 3000
|
209
|
+
Thread.new { instance.mult1 = 4000 }.join
|
210
|
+
book.should have(2).items
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should have one thread's worth of data when var is set in two threads, one thread terminated, and then cleaned" do
|
214
|
+
instance.mult1 = 3000
|
215
|
+
Thread.new { instance.mult1 = 4000 }.join
|
216
|
+
instance.send :_thread_local_clean
|
217
|
+
book.should have(1).item
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
context "class-level thread locals" do
|
225
|
+
|
226
|
+
context "instance thread locals" do
|
227
|
+
let :instance do
|
228
|
+
ThreadLocalClassLevel.dupe_for_testing
|
229
|
+
end
|
230
|
+
|
231
|
+
let :instance2 do
|
232
|
+
ThreadLocalClassLevel2.dupe_for_testing
|
233
|
+
end
|
234
|
+
|
235
|
+
subject { instance }
|
236
|
+
context "method definition" do
|
237
|
+
|
238
|
+
[:block1, :block2, :plain, :mult1, :mult2, :block1, :default_var,
|
239
|
+
:protected_var, :public_var, :contextually_private_var, :contextually_public_var].each do |key|
|
240
|
+
it "should have defined reader for #{key}" do
|
241
|
+
instance.should respond_to(key)
|
242
|
+
end
|
243
|
+
|
244
|
+
it "should have defined writer for #{key}" do
|
245
|
+
instance.should respond_to((key.to_s + "=").to_sym)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
[:private_var].each do |key|
|
250
|
+
it "should have defined reader for #{key}" do
|
251
|
+
instance.private_methods.should include(key.to_s)
|
252
|
+
end
|
253
|
+
|
254
|
+
it "should have defined writer for #{key}" do
|
255
|
+
instance.private_methods.should include(key.to_s + "=")
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
context "default value" do
|
261
|
+
subject { instance }
|
262
|
+
[:plain, :mult1, :mult2,
|
263
|
+
:contextually_private_var, :contextually_public_var].each do |key|
|
264
|
+
it "should return nil for #{key}, being configured without default" do
|
265
|
+
instance.send(key).should be_nil
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
it "should return constant value if :default option provided" do
|
270
|
+
instance.default_var.should == 100
|
271
|
+
end
|
272
|
+
|
273
|
+
it "should return computed value if :block option provided" do
|
274
|
+
instance.block1.should == "DEFAULT FOR ThreadLocalClassLevel.block1"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context "when setting" do
|
279
|
+
it "should return the newly set value after being set" do
|
280
|
+
instance.block1 = "BLOCK!"
|
281
|
+
instance.block1.should == "BLOCK!"
|
282
|
+
end
|
283
|
+
|
284
|
+
|
285
|
+
it "should keep values separately for two instances" do
|
286
|
+
instance.plain = "INSTANCE1 VALUE"
|
287
|
+
instance2.plain = "INSTANCE2 VALUE"
|
288
|
+
instance.plain.should == "INSTANCE1 VALUE"
|
289
|
+
instance2.plain.should == "INSTANCE2 VALUE"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
context "visibility" do
|
294
|
+
it "should define methods as public by default" do
|
295
|
+
instance.public_methods.should include("plain", "plain=", "mult1", "mult1=")
|
296
|
+
end
|
297
|
+
|
298
|
+
it "should define methods as protected when :visibility => :protected" do
|
299
|
+
instance.protected_methods.should include("protected_var", "protected_var=")
|
300
|
+
end
|
301
|
+
|
302
|
+
it "should define methods as private when :visibility => :private" do
|
303
|
+
instance.private_methods.should include("private_var", "private_var=")
|
304
|
+
end
|
305
|
+
|
306
|
+
it "should define methods as public when :visibility is not provided, even in a private context" do
|
307
|
+
instance.public_methods.should include("contextually_private_var", "contextually_private_var=")
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
context "thread locality" do
|
312
|
+
it "should return the same value in the same thread when called consecutively" do
|
313
|
+
instance.mult1 = "barbar"
|
314
|
+
instance.mult1.should == "barbar"
|
315
|
+
instance.mult1.should == "barbar"
|
316
|
+
end
|
317
|
+
|
318
|
+
it "should return the default value in a new thread" do
|
319
|
+
instance.default_var = "FOO"
|
320
|
+
instance.default_var.should == "FOO"
|
321
|
+
Thread.new { @res = instance.default_var }.join
|
322
|
+
@res.should == 100
|
323
|
+
end
|
324
|
+
|
325
|
+
it "should not affect the value in one thread when set in a different thread" do
|
326
|
+
instance.default_var = "FOO"
|
327
|
+
instance.default_var.should == "FOO"
|
328
|
+
Thread.new { instance.default_var = 2500 }.join
|
329
|
+
instance.default_var.should == "FOO"
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
class DualThreadLocalTypes
|
336
|
+
include ZeevexThreadsafe::ThreadLocals
|
337
|
+
thread_local :foo
|
338
|
+
cthread_local :foo
|
339
|
+
end
|
340
|
+
|
341
|
+
context "same named thread locals at class and instance level" do
|
342
|
+
let :clazz do
|
343
|
+
DualThreadLocalTypes
|
344
|
+
end
|
345
|
+
let :instance do
|
346
|
+
DualThreadLocalTypes.new
|
347
|
+
end
|
348
|
+
it "should not set one when setting the other" do
|
349
|
+
clazz.foo = "classvalue"
|
350
|
+
instance.foo = "instancevalue"
|
351
|
+
clazz.foo.should == "classvalue"
|
352
|
+
instance.foo.should == "instancevalue"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
class MetaClassThreadLocals
|
357
|
+
class << self
|
358
|
+
include ZeevexThreadsafe::ThreadLocals
|
359
|
+
thread_local :foo
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
context "defining thread_local on metaclass of a class" do
|
364
|
+
let :instance do
|
365
|
+
MetaClassThreadLocals
|
366
|
+
end
|
367
|
+
subject { instance }
|
368
|
+
it "should have a :foo method defined" do
|
369
|
+
instance.should respond_to(:foo)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "zeevex_threadsafe/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "zeevex_threadsafe"
|
7
|
+
s.version = ZeevexThreadsafe::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Robert Sanders"]
|
10
|
+
s.email = ["robert@zeevex.com"]
|
11
|
+
s.homepage = "http://github.com/zeevex/zeevex_threadsafe"
|
12
|
+
s.summary = %q{Utilities to help in creating thread-safe apps}
|
13
|
+
s.description = %q{Utilities to help in creating thread-safe apps}
|
14
|
+
|
15
|
+
s.rubyforge_project = "zeevex_threadsafe"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency 'zeevex_proxy'
|
23
|
+
s.add_dependency 'zeevex_delayed'
|
24
|
+
|
25
|
+
s.add_development_dependency 'rspec', '~> 2.9.0'
|
26
|
+
s.add_development_dependency 'rake'
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zeevex_threadsafe
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Robert Sanders
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-05-03 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: zeevex_proxy
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: zeevex_delayed
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :runtime
|
43
|
+
version_requirements: *id002
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: rspec
|
46
|
+
prerelease: false
|
47
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ~>
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 2
|
53
|
+
- 9
|
54
|
+
- 0
|
55
|
+
version: 2.9.0
|
56
|
+
type: :development
|
57
|
+
version_requirements: *id003
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rake
|
60
|
+
prerelease: false
|
61
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
type: :development
|
69
|
+
version_requirements: *id004
|
70
|
+
description: Utilities to help in creating thread-safe apps
|
71
|
+
email:
|
72
|
+
- robert@zeevex.com
|
73
|
+
executables: []
|
74
|
+
|
75
|
+
extensions: []
|
76
|
+
|
77
|
+
extra_rdoc_files: []
|
78
|
+
|
79
|
+
files:
|
80
|
+
- .gitignore
|
81
|
+
- Gemfile
|
82
|
+
- Rakefile
|
83
|
+
- lib/zeevex_threadsafe.rb
|
84
|
+
- lib/zeevex_threadsafe/aliasing.rb
|
85
|
+
- lib/zeevex_threadsafe/rails.rb
|
86
|
+
- lib/zeevex_threadsafe/rails/request_globals.rb
|
87
|
+
- lib/zeevex_threadsafe/rails/thread_pool_release.rb
|
88
|
+
- lib/zeevex_threadsafe/thread_locals.rb
|
89
|
+
- lib/zeevex_threadsafe/thread_safer.rb
|
90
|
+
- lib/zeevex_threadsafe/version.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
- spec/thread_locals_spec.rb
|
93
|
+
- zeevex_threadsafe.gemspec
|
94
|
+
has_rdoc: true
|
95
|
+
homepage: http://github.com/zeevex/zeevex_threadsafe
|
96
|
+
licenses: []
|
97
|
+
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
segments:
|
108
|
+
- 0
|
109
|
+
version: "0"
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
requirements: []
|
118
|
+
|
119
|
+
rubyforge_project: zeevex_threadsafe
|
120
|
+
rubygems_version: 1.3.6
|
121
|
+
signing_key:
|
122
|
+
specification_version: 3
|
123
|
+
summary: Utilities to help in creating thread-safe apps
|
124
|
+
test_files:
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
- spec/thread_locals_spec.rb
|