lspace 0.1.pre.1 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +152 -16
- data/lib/lspace.rb +126 -196
- data/lib/lspace/class_methods.rb +129 -0
- data/lib/lspace/core_ext.rb +53 -12
- data/lib/lspace/eventmachine.rb +56 -16
- data/lspace.gemspec +5 -1
- data/spec/class_method_spec.rb +116 -0
- data/spec/core_ext_spec.rb +75 -0
- data/spec/eventmachine_spec.rb +131 -0
- data/spec/lspace_spec.rb +121 -60
- data/spec/spec_helper.rb +4 -1
- metadata +58 -6
@@ -0,0 +1,129 @@
|
|
1
|
+
class LSpace
|
2
|
+
|
3
|
+
# Create a new clean LSpace.
|
4
|
+
#
|
5
|
+
# This LSpace does not inherit any LSpace variables in the currently active LSpace.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# RSpec.configure do |c|
|
9
|
+
# c.around(:each) do |example|
|
10
|
+
# LSpace.clean do
|
11
|
+
# example.run
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @param [Proc] block The logical block that will be run in the clean LSpace
|
17
|
+
# @see LSpace.enter
|
18
|
+
def self.clean(&block)
|
19
|
+
enter new({}, nil), &block
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a new LSpace with the given keys set to the given values.
|
23
|
+
#
|
24
|
+
# The LSpace will inherit any unspecified keys from the currently active LSpace.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# LSpace.update :user_id => 6 do
|
28
|
+
# LSpace.update :job_id => 7 do
|
29
|
+
# LSpace[:user_id] == 6
|
30
|
+
# LSpace[:job_id] == 7
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @param [Hash] hash The keys to update
|
35
|
+
# @param [Proc] block The logical block to run with the updated LSpace
|
36
|
+
# @see LSpace.enter
|
37
|
+
def self.update(hash={}, &block)
|
38
|
+
enter new(hash, current), &block
|
39
|
+
end
|
40
|
+
|
41
|
+
# Enter an LSpace for the logical duration of the block.
|
42
|
+
#
|
43
|
+
# The LSpace will be active at least for the duration of the block's callstack,
|
44
|
+
# but if the block creates any closures (using LSpace.preserve directly, or in
|
45
|
+
# library form) then the logical duration will also encompass code run in those
|
46
|
+
# closures.
|
47
|
+
#
|
48
|
+
# Entering an LSpace will also cause any around_filters defined on it and its parents to
|
49
|
+
# be run.
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# class Job
|
53
|
+
# def initialize
|
54
|
+
# @lspace = LSpace.new(:job_id => self.id)
|
55
|
+
# LSpace.enter(@lspace){ setup_lspace }
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# def run!
|
59
|
+
# LSpace.enter(@lspace) { run }
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# @param [LSpace] lspace The LSpace to enter
|
64
|
+
# @param [Proc] block The logical block to run with the given LSpace
|
65
|
+
def self.enter(lspace, &block)
|
66
|
+
previous = current
|
67
|
+
self.current = lspace
|
68
|
+
|
69
|
+
filters = lspace.hierarchy.take_while{ |lspace| lspace != previous }.map(&:around_filters).flatten
|
70
|
+
|
71
|
+
filters.inject(block) do |blk, filter|
|
72
|
+
lambda{ filter.call(&blk) }
|
73
|
+
end.call
|
74
|
+
ensure
|
75
|
+
self.current = previous
|
76
|
+
end
|
77
|
+
|
78
|
+
# Create a closure that will re-enter the current LSpace when the block is called.
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# class TaskQueue
|
82
|
+
# def queue(&block)
|
83
|
+
# @queue << LSpace.preserve(&block)
|
84
|
+
# end
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# @see Proc#in_lspace
|
88
|
+
# @param [Proc] block The logical block to wrap
|
89
|
+
def self.preserve(&block)
|
90
|
+
current.wrap(&block)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Get the value for the key in the current LSpace or its parents
|
94
|
+
#
|
95
|
+
# @see LSpace#[]
|
96
|
+
def self.[](key)
|
97
|
+
current[key]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Set the value for the key in the current LSpace
|
101
|
+
#
|
102
|
+
# @see LSpace#[]=
|
103
|
+
def self.[]=(key, value)
|
104
|
+
current[key] = value
|
105
|
+
end
|
106
|
+
|
107
|
+
# Add an around filter to the current LSpace
|
108
|
+
#
|
109
|
+
# @see LSpace#around_filter
|
110
|
+
def self.around_filter(&filter)
|
111
|
+
current.around_filter(&filter)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get the current LSpace
|
115
|
+
#
|
116
|
+
# @return [LSpace]
|
117
|
+
def self.current
|
118
|
+
Thread.current[:lspace] ||= LSpace.new({}, nil)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Update the current LSpace
|
124
|
+
#
|
125
|
+
# @see LSpace.enter
|
126
|
+
def self.current=(lspace)
|
127
|
+
Thread.current[:lspace] = lspace
|
128
|
+
end
|
129
|
+
end
|
data/lib/lspace/core_ext.rb
CHANGED
@@ -1,4 +1,48 @@
|
|
1
1
|
class Module
|
2
|
+
# Create getter methods for LSpace
|
3
|
+
#
|
4
|
+
# Assumes that your LSpace keys are Symbols.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class Job
|
8
|
+
# lspace_reader :user_id
|
9
|
+
#
|
10
|
+
# def user
|
11
|
+
# User.find(user_id)
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# LSpace[:user_id] = 6
|
16
|
+
# Job.new.user == #<User:6>
|
17
|
+
#
|
18
|
+
# @param [Symbol] *attrs The accessors to create
|
19
|
+
def lspace_reader(*attrs)
|
20
|
+
attrs.each do |attr|
|
21
|
+
define_method(attr) do
|
22
|
+
LSpace[attr]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Preserve lspace of all blocks passed to this function.
|
28
|
+
#
|
29
|
+
# This wraps both the &block parameter, and also any Procs
|
30
|
+
# that are passed into the function directly.
|
31
|
+
#
|
32
|
+
# If you need more complicated logic (i.e. wrapping Procs
|
33
|
+
# that are passed to a function in a dictionary) you're
|
34
|
+
# on your own.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# class << Thread
|
38
|
+
# in_lspace :new, :start, :fork
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# LSpace.new :user_id => 2 do
|
42
|
+
# Thread.new{ LSpace[:user_id] == 2 }
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @param [Symbol] *methods The methods to wrap
|
2
46
|
def in_lspace(*methods)
|
3
47
|
methods.each do |method|
|
4
48
|
method_without_lspace = "#{method}_without_lspace"
|
@@ -16,21 +60,18 @@ class Module
|
|
16
60
|
protected method if protected_method_defined?(method_without_lspace)
|
17
61
|
end
|
18
62
|
end
|
19
|
-
|
20
|
-
def attr_lspace(*attrs)
|
21
|
-
attrs.each do |attr|
|
22
|
-
define_method(attr) do
|
23
|
-
LSpace[attr]
|
24
|
-
end
|
25
|
-
|
26
|
-
define_method("#{attr}=") do |value|
|
27
|
-
LSpace[attr] = value
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
63
|
end
|
32
64
|
|
33
65
|
class Proc
|
66
|
+
# Preserve LSpace when this Proc is run.
|
67
|
+
#
|
68
|
+
# @example
|
69
|
+
# todo = LSpace.new :user_id => 2 do
|
70
|
+
# proc{ LSpace[:user_id] }.in_lspace
|
71
|
+
# end
|
72
|
+
# todo.call == 2
|
73
|
+
# @see LSpace.preserve
|
74
|
+
# @return [Proc] A version of self that re-enters LSpace before running
|
34
75
|
def in_lspace
|
35
76
|
LSpace.preserve(&self)
|
36
77
|
end
|
data/lib/lspace/eventmachine.rb
CHANGED
@@ -1,39 +1,79 @@
|
|
1
1
|
require 'eventmachine'
|
2
|
+
require 'lspace'
|
2
3
|
module EventMachine
|
4
|
+
|
5
|
+
# Most of the low-level EventMachine stuff goes through singleton methods on the
|
6
|
+
# EventMachine class.
|
7
|
+
#
|
8
|
+
# We use in_lspace to preserve the LSpace of any blocks passed to these methods.
|
9
|
+
class << self
|
10
|
+
in_lspace :add_timer, :next_tick, :error_handler, :defer, :run, :run_block, :schedule, :fork_reactor
|
11
|
+
in_lspace :add_shutdown_hook if method_defined?(:add_shutdown_hook) # only some versions of EM
|
12
|
+
end
|
13
|
+
|
14
|
+
# Many EM APIs (e.g. em-http-request) are based on deferrables. Preserving lspace for
|
15
|
+
# both callbacks and errbacks makes these libraries "just work".
|
3
16
|
module Deferrable
|
4
17
|
in_lspace :callback, :errback
|
5
18
|
end
|
6
19
|
|
7
|
-
class
|
8
|
-
|
20
|
+
class Connection
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# As EM uses a custom implementation of {new}, the only sane way to
|
24
|
+
# set up the LSpace in advance is to override allocate.
|
25
|
+
alias_method :allocate_without_lspace, :allocate
|
26
|
+
end
|
9
27
|
|
10
|
-
|
28
|
+
# Overridden allocate which sets up a new LSpace.
|
29
|
+
#
|
30
|
+
# Each connection object is run in its own LSpace, which can be
|
31
|
+
# configured by implementing the {setup_lspace} method.
|
32
|
+
def self.allocate
|
11
33
|
allocate_without_lspace.instance_eval do
|
12
34
|
extend EventMachine::LSpacePreserver
|
13
|
-
|
14
|
-
|
35
|
+
LSpace.update do
|
36
|
+
setup_lspace
|
37
|
+
@lspace = LSpace.current
|
38
|
+
end
|
15
39
|
self
|
16
40
|
end
|
17
41
|
end
|
18
42
|
|
43
|
+
# Override this method to setup the LSpace in a manner which you require.
|
44
|
+
#
|
45
|
+
# This method is called before initialize() and before post_init().
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# module EchoServer
|
49
|
+
# def setup_lspace
|
50
|
+
# LSpace.around_filter do |&block|
|
51
|
+
# begin
|
52
|
+
# block.call
|
53
|
+
# rescue => e
|
54
|
+
# self.rescue(e)
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# def rescue(e)
|
60
|
+
# puts "An exception occurred!: #{e}"
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
def setup_lspace; end
|
19
64
|
end
|
20
65
|
|
21
|
-
|
22
|
-
|
23
|
-
end
|
24
|
-
|
66
|
+
# A module that's included at the beginning of the method-resolution chain of
|
67
|
+
# connections which restores LSpace whenever a callback is fired by the eventloop.
|
25
68
|
module LSpacePreserver
|
26
69
|
[:initialize, :post_init, :connection_completed, :receive_data, :ssl_verify_peer, :ssl_handshake_completed].each do |method|
|
27
|
-
define_method(method) { |*a, &b| LSpace.enter(@
|
70
|
+
define_method(method) { |*a, &b| LSpace.enter(@lspace) { super(*a, &b) } }
|
28
71
|
end
|
29
72
|
|
73
|
+
# EM uses the arity of unbind to decide which arguments to pass it.
|
74
|
+
# AFAIK the no-argument version is considerably more popular, so we use that here.
|
30
75
|
[:unbind].each do |method|
|
31
|
-
define_method(method) { LSpace.enter(@
|
76
|
+
define_method(method) { LSpace.enter(@lspace) { super() } }
|
32
77
|
end
|
33
78
|
end
|
34
79
|
end
|
35
|
-
|
36
|
-
class << EventMachine
|
37
|
-
in_lspace :add_timer, :next_tick, :error_handler, :defer, :run, :run_block, :schedule, :fork_reactor
|
38
|
-
in_lspace :add_shutdown_hook if method_defined?(:add_shutdown_hook)
|
39
|
-
end
|
data/lspace.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "lspace"
|
3
|
-
s.version = "0.1
|
3
|
+
s.version = "0.1"
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
s.author = "Conrad Irwin"
|
6
6
|
s.email = "conrad.irwin@gmail.com"
|
@@ -9,4 +9,8 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.description = "Provides the convenience of global variables, without the safety concerns."
|
10
10
|
s.files = `git ls-files`.split("\n")
|
11
11
|
s.require_path = "lib"
|
12
|
+
|
13
|
+
s.add_development_dependency 'rspec'
|
14
|
+
s.add_development_dependency 'pry-rescue'
|
15
|
+
s.add_development_dependency 'eventmachine'
|
12
16
|
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe LSpace do
|
3
|
+
before do
|
4
|
+
@lspace = LSpace.new({}, nil)
|
5
|
+
end
|
6
|
+
|
7
|
+
describe ".current" do
|
8
|
+
it "should return the most recently entered LSpace" do
|
9
|
+
LSpace.enter @lspace do
|
10
|
+
LSpace.current.should == @lspace
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should ignore changes on other threads" do
|
15
|
+
LSpace.enter @lspace do
|
16
|
+
Thread.new{ LSpace.current.should_not == @lspace }.join
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe ".update" do
|
22
|
+
it "should enter a new LSpace with the new variables set" do
|
23
|
+
LSpace.update(:foo => 5) do
|
24
|
+
LSpace[:foo].should == 5
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should enter a new LSpace which delegates to the current parent" do
|
29
|
+
LSpace.update(:foo => 5) do
|
30
|
+
LSpace.update(:bar => 4) do
|
31
|
+
LSpace[:foo].should == 5
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe ".clean" do
|
38
|
+
it "should enter a new LSpace with no parent" do
|
39
|
+
LSpace.update(:foo => 5) do
|
40
|
+
LSpace.clean do
|
41
|
+
LSpace[:foo].should == nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe ".enter" do
|
48
|
+
it "should update LSpace.current" do
|
49
|
+
LSpace.enter(@lspace) do
|
50
|
+
LSpace.current.should == @lspace
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should revert that change at the end of the block" do
|
55
|
+
lspace = LSpace.current
|
56
|
+
LSpace.enter(@lspace) do
|
57
|
+
# yada yada
|
58
|
+
end
|
59
|
+
LSpace.current.should == lspace
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should revert that change even if the block raises an exception" do
|
63
|
+
lspace = LSpace.current
|
64
|
+
lambda do
|
65
|
+
LSpace.enter(@lspace) do
|
66
|
+
raise "OOPS"
|
67
|
+
end
|
68
|
+
end.should raise_error /OOPS/
|
69
|
+
LSpace.current.should == lspace
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should not effect other threads" do
|
73
|
+
LSpace.enter(@lspace) do
|
74
|
+
Thread.new{ LSpace.current.should_not == @lspace }.join
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should not effect other fibers" do
|
79
|
+
LSpace.enter(@lspace) do
|
80
|
+
Fiber.new{ LSpace.current.should_not == @lspace }.resume
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe ".preserve" do
|
86
|
+
it "should delegate to LSpace.current" do
|
87
|
+
LSpace.current.should_receive(:wrap).once
|
88
|
+
LSpace.preserve{ 5 }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe ".[]" do
|
93
|
+
it "should delegate to LSpace.current" do
|
94
|
+
LSpace.current.should_receive(:[]).once.with(:foo).and_return(:bar)
|
95
|
+
LSpace[:foo].should == :bar
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe ".[]=" do
|
100
|
+
it "should delegate to LSpace.current" do
|
101
|
+
LSpace.current.should_receive(:[]=).once.with(:foo, :bar)
|
102
|
+
LSpace[:foo] = :bar
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe ".around_filter" do
|
107
|
+
it "should delegate to LSpace.current" do
|
108
|
+
@lspace.should_receive(:around_filter).once
|
109
|
+
LSpace.enter @lspace do
|
110
|
+
LSpace.around_filter do |&block|
|
111
|
+
block.call
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Module do
|
4
|
+
describe "#lspace_reader" do
|
5
|
+
it "should define a reader for LSpace" do
|
6
|
+
klass = Class.new{ lspace_reader :user_id }
|
7
|
+
|
8
|
+
LSpace.update(:user_id => 8) do
|
9
|
+
klass.new.user_id.should == 8
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#in_lspace" do
|
15
|
+
before do
|
16
|
+
@klass = Class.new do
|
17
|
+
def initialize(task=nil, &block)
|
18
|
+
@block = block || task
|
19
|
+
end
|
20
|
+
in_lspace :initialize
|
21
|
+
|
22
|
+
def call
|
23
|
+
@block.call
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def private_test(&block)
|
29
|
+
block
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def protected_test(&block)
|
35
|
+
block
|
36
|
+
end
|
37
|
+
|
38
|
+
in_lspace :private_test, :protected_test
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should automatically preserve LSpace for blocks that are passed in" do
|
43
|
+
@task = LSpace.update :user_id => 6 do
|
44
|
+
@klass.new{ LSpace[:user_id] }
|
45
|
+
end
|
46
|
+
|
47
|
+
@task.call.should == 6
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should automatically preserve LSpace for procs that are passed in" do
|
51
|
+
@task = LSpace.update :user_id => 6 do
|
52
|
+
@klass.new proc{ LSpace[:user_id] }
|
53
|
+
end
|
54
|
+
|
55
|
+
@task.call.should == 6
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should preserve visibility of methods" do
|
59
|
+
@klass.private_method_defined?(:private_test).should == true
|
60
|
+
@klass.protected_method_defined?(:protected_test).should == true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe Proc do
|
66
|
+
describe "#in_lspace" do
|
67
|
+
it "should create a wrapper which preserves the LSpace" do
|
68
|
+
p = LSpace.update(:job_id => 19) do
|
69
|
+
lambda{ LSpace[:job_id] }.in_lspace
|
70
|
+
end
|
71
|
+
|
72
|
+
p.call.should == 19
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|