lspace 0.1.pre.1
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/LICENSE.MIT +19 -0
- data/README.md +23 -0
- data/lib/lspace/core_ext.rb +37 -0
- data/lib/lspace/eventmachine.rb +39 -0
- data/lib/lspace/thread.rb +7 -0
- data/lib/lspace.rb +214 -0
- data/lspace.gemspec +12 -0
- data/spec/lspace_spec.rb +89 -0
- data/spec/spec_helper.rb +8 -0
- metadata +54 -0
data/LICENSE.MIT
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2012 Conrad Irwin <conrad.irwin@gmail.com>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
*in progress*
|
2
|
+
|
3
|
+
LSpace — Safe operation-local storage.
|
4
|
+
|
5
|
+
Global variables are awesome. Unfortunately they become a bit useless when you have
|
6
|
+
multiple threads because you often want different "global" state per-thread...
|
7
|
+
|
8
|
+
Thread-local variables are even more awesome! Unfortunately they become a bit useless when
|
9
|
+
you are doing multiple things on the same thread because you often want different
|
10
|
+
"thread-local" state per operation...
|
11
|
+
|
12
|
+
Operation-local variables are most awesome!
|
13
|
+
|
14
|
+
`LSpace`, named after the Discworld's [L-Space](http://en.wikipedia.org/wiki/Other_dimensions_of_the_Discworld#L-space)
|
15
|
+
gives you effective, safe operation-local variables.
|
16
|
+
|
17
|
+
It does this by following your operation as it jumps between thread-pools, or fires
|
18
|
+
callbacks on your event-loop; and makes sure to clean up after itself so that no state
|
19
|
+
accidentally leaks.
|
20
|
+
|
21
|
+
If you're using this on EventMachine, you should be ready to rock by requiring 'lspace/eventmachine'.
|
22
|
+
If you've got your own thread-pool, or are doing something fancy, you'll need to do some
|
23
|
+
manual work.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Module
|
2
|
+
def in_lspace(*methods)
|
3
|
+
methods.each do |method|
|
4
|
+
method_without_lspace = "#{method}_without_lspace"
|
5
|
+
next if method_defined?(method_without_lspace) || private_method_defined?(method_without_lspace)
|
6
|
+
|
7
|
+
alias_method method_without_lspace, method
|
8
|
+
|
9
|
+
define_method(method) do |*args, &block|
|
10
|
+
args.map!{ |a| Proc === a ? a.in_lspace : a }
|
11
|
+
block = block && block.in_lspace
|
12
|
+
__send__(method_without_lspace, *args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
private method if private_method_defined?(method_without_lspace)
|
16
|
+
protected method if protected_method_defined?(method_without_lspace)
|
17
|
+
end
|
18
|
+
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
|
+
end
|
32
|
+
|
33
|
+
class Proc
|
34
|
+
def in_lspace
|
35
|
+
LSpace.preserve(&self)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
module EventMachine
|
3
|
+
module Deferrable
|
4
|
+
in_lspace :callback, :errback
|
5
|
+
end
|
6
|
+
|
7
|
+
class << Connection
|
8
|
+
alias_method :allocate_without_lspace, :allocate
|
9
|
+
|
10
|
+
def allocate
|
11
|
+
allocate_without_lspace.instance_eval do
|
12
|
+
extend EventMachine::LSpacePreserver
|
13
|
+
@lspace_context = LSpace.new
|
14
|
+
lspace_setup
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class Connection
|
22
|
+
def lspace_setup; end
|
23
|
+
end
|
24
|
+
|
25
|
+
module LSpacePreserver
|
26
|
+
[:initialize, :post_init, :connection_completed, :receive_data, :ssl_verify_peer, :ssl_handshake_completed].each do |method|
|
27
|
+
define_method(method) { |*a, &b| LSpace.enter(@lspace_context) { super(*a, &b) } }
|
28
|
+
end
|
29
|
+
|
30
|
+
[:unbind].each do |method|
|
31
|
+
define_method(method) { LSpace.enter(@lspace_context) { super() } }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
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/lib/lspace.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
require File.expand_path('../lspace/core_ext', __FILE__)
|
2
|
+
|
3
|
+
module LSpace
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# Get the most specific value for the key.
|
7
|
+
#
|
8
|
+
# If nested LSpaces are active, returns the value set in the innermost scope.
|
9
|
+
# If this key is not present in any of the nested LSpaces, nil is returned.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# LSpace.new :user_id => 5 do
|
13
|
+
# LSpace.new :user_id => 6 do
|
14
|
+
# LSpace[:user_id] == 6
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# @param [Object] key
|
18
|
+
# @return [Object]
|
19
|
+
def [](key)
|
20
|
+
active.each do |c|
|
21
|
+
return c[key] if c.has_key?(key)
|
22
|
+
end
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sets the value for the key in the currently active LSpace.
|
27
|
+
#
|
28
|
+
# This does not have any effect on outer LSpaces.
|
29
|
+
#
|
30
|
+
# If your LSpace is shared between threads, you should think very hard before
|
31
|
+
# changing a value, it's often better to create a new LSpace if you want to
|
32
|
+
# override a value temporarily.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# LSpace.new :user_id => 5 do
|
36
|
+
# LSpace.new do
|
37
|
+
# LSpace[:user_id] = 6
|
38
|
+
# LSpace[:user_id] == 6
|
39
|
+
# end
|
40
|
+
# LSpace[:user_id] == 5
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# @param [Object] key
|
44
|
+
# @param [Object] value
|
45
|
+
# @return [Object] value
|
46
|
+
def []=(key, value)
|
47
|
+
current[key] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create a new LSpace.
|
51
|
+
#
|
52
|
+
# If a block is passed, then the block is called in the new LSpace,
|
53
|
+
# otherwise you can manually pass the LSpace to {LSpace.enter} later.
|
54
|
+
#
|
55
|
+
# The returned LSpace will inherit from the currently active LSpace:
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# LSpace.new :job_id => 7 do
|
59
|
+
# LSpace[:job_id] == 7
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# class Job
|
64
|
+
# def initialize
|
65
|
+
# @lspace = LSpace.new
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# def run!
|
69
|
+
# LSpace.enter(@lspace){ run }
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# @param [Hash] new Values to set in the new LSpace
|
74
|
+
# @param [Proc] block The block to run
|
75
|
+
# @return [Hash] The new LSpace (unless a block is given)
|
76
|
+
# @return [Object] The return value of the block (if a block is given)
|
77
|
+
def new(new={}, &block)
|
78
|
+
new[:outer_lspace] = current
|
79
|
+
if block_given?
|
80
|
+
enter(new, &block)
|
81
|
+
else
|
82
|
+
new
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Enter an LSpace
|
87
|
+
#
|
88
|
+
# This sets a new LSpace to be current for the duration of the block,
|
89
|
+
# it also runs any around filters for the new space. (Around filters that
|
90
|
+
# were present in the previous space are not run again).
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# class Job
|
94
|
+
# def initialize
|
95
|
+
# @lspace = LSpace.new
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# def run!
|
99
|
+
# LSpace.enter(@lspace){ run }
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# @param [Hash] new The LSpace to enter
|
104
|
+
# @param [Proc] block The block to run
|
105
|
+
def enter(new, &block)
|
106
|
+
previous = current
|
107
|
+
self.current = new
|
108
|
+
|
109
|
+
new_frames = active.take_while{ |space| space != previous }
|
110
|
+
filters = new_frames.map{ |space| space[:around_filter] }.compact
|
111
|
+
|
112
|
+
filters.inject(block) do |block, filter|
|
113
|
+
lambda{ filter.call(&block) }
|
114
|
+
end.call
|
115
|
+
ensure
|
116
|
+
self.current = previous
|
117
|
+
end
|
118
|
+
|
119
|
+
# Preserve the current LSpace when this block is called
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# LSpace.new :user_id => 1 do
|
123
|
+
# $todo = LSpace.preserve do |args|
|
124
|
+
# LSpace[:user_id]
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
# $todo.call == 1
|
128
|
+
#
|
129
|
+
# @see [Proc#in_lspace]
|
130
|
+
# @param [Proc] original The block to wrap
|
131
|
+
# @return [Proc] A modified block that will be executed in the current LSpace.
|
132
|
+
def preserve(&original)
|
133
|
+
current = self.current
|
134
|
+
|
135
|
+
proc do |*args, &block|
|
136
|
+
LSpace.enter(current) do
|
137
|
+
original.call(*args, &block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Add an around filter for the current LSpace
|
143
|
+
#
|
144
|
+
# The filter will be called every time this LSpace is entered on a new call stack, which
|
145
|
+
# makes it suitable for maintaining state in libraries that are not LSpace aware (like
|
146
|
+
# log4r) or implementing unified fallback error handling.
|
147
|
+
#
|
148
|
+
# Bear in mind that when you add an around_filter to the current LSpace it will not be
|
149
|
+
# running. For this reason, you should try and set up around filters before using the
|
150
|
+
# LSpace properly.
|
151
|
+
#
|
152
|
+
# @example
|
153
|
+
# class Job
|
154
|
+
# def initialize
|
155
|
+
# LSpace.new do
|
156
|
+
#
|
157
|
+
# LSpace.around_filter do |&block|
|
158
|
+
# begin
|
159
|
+
# block.call
|
160
|
+
# rescue => e
|
161
|
+
# puts "Job #{LSpace[:job_id]} failed with: #{e}"
|
162
|
+
# end
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
# @lspace = LSpace.current
|
166
|
+
# end
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# def run!
|
170
|
+
# LSpace.enter(@lspace){ run }
|
171
|
+
# end
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# @param [Proc] new_filter A Proc that takes a &block argument.
|
175
|
+
def around_filter(&new_filter)
|
176
|
+
if old_filter = current[:around_filter]
|
177
|
+
current[:around_filter] = lambda{ |&block| old_filter.call{ new_filter.call(&block) } }
|
178
|
+
else
|
179
|
+
current[:around_filter] = new_filter
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Get the currently active LSpace
|
184
|
+
#
|
185
|
+
# @see LSpace.enter
|
186
|
+
# @param [Hash] new The new LSpace
|
187
|
+
def current
|
188
|
+
Thread.current[:lspace] ||= {}
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
# Set the current LSpace
|
194
|
+
#
|
195
|
+
# @see LSpace.enter
|
196
|
+
# @param [Hash] new The new LSpace
|
197
|
+
def current=(new)
|
198
|
+
Thread.current[:lspace] = new
|
199
|
+
end
|
200
|
+
|
201
|
+
# All active LSpaces from most-specific to most-generic
|
202
|
+
#
|
203
|
+
# @return [Array<Hash>]
|
204
|
+
def active
|
205
|
+
c = self.current
|
206
|
+
a = []
|
207
|
+
while c
|
208
|
+
a << c
|
209
|
+
c = c[:outer_lspace]
|
210
|
+
end
|
211
|
+
a
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
data/lspace.gemspec
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "lspace"
|
3
|
+
s.version = "0.1.pre.1"
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.author = "Conrad Irwin"
|
6
|
+
s.email = "conrad.irwin@gmail.com"
|
7
|
+
s.homepage = "http://github.com/ConradIrwin/lspace"
|
8
|
+
s.summary = "Provides local global storage"
|
9
|
+
s.description = "Provides the convenience of global variables, without the safety concerns."
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.require_path = "lib"
|
12
|
+
end
|
data/spec/lspace_spec.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LSpace do
|
4
|
+
it "should act like a hash" do
|
5
|
+
LSpace[:foo] = 1
|
6
|
+
LSpace[:foo].should == 1
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should isolate changes to nested spaces" do
|
10
|
+
LSpace[:foo] = 2
|
11
|
+
|
12
|
+
LSpace.new do
|
13
|
+
LSpace[:foo] = 1
|
14
|
+
LSpace[:foo].should == 1
|
15
|
+
end
|
16
|
+
|
17
|
+
LSpace[:foo].should == 2
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should fallback to outer spaces" do
|
21
|
+
LSpace[:bar] = 1
|
22
|
+
LSpace.new do
|
23
|
+
LSpace[:bar].should == 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should be isolated between different threads" do
|
28
|
+
LSpace[:foo] = 1
|
29
|
+
Thread.new{ LSpace[:foo].should == nil }.join
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should be isolated between different fibers" do
|
33
|
+
LSpace[:foo] = 1
|
34
|
+
Fiber.new{ LSpace[:foo].should == nil }.resume
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should allow preserving spaces" do
|
38
|
+
p = LSpace.new(:foo => 1){ proc{ LSpace[:foo] }.in_lspace }
|
39
|
+
p.call.should == 1
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should allow resuming spaces in different threads" do
|
43
|
+
p = LSpace.new(:foo => 1){ proc{ LSpace[:foo] }.in_lspace }
|
44
|
+
Thread.new{ p.call.should == 1 }.join
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should allow resuming spaces in different fibers" do
|
48
|
+
p = LSpace.new(:foo => 1){ LSpace.preserve{ LSpace[:foo] } }
|
49
|
+
Fiber.new{ p.call.should == 1 }.resume
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should clean up lspace after resuming" do
|
53
|
+
p = LSpace.new(:foo => 1){ proc{ LSpace[:foo] }.in_lspace }
|
54
|
+
p.call.should == 1
|
55
|
+
LSpace[:foo].should == nil
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should resume the entire nested lspace" do
|
59
|
+
p = LSpace.new(:foo => 1) {
|
60
|
+
LSpace.new(:bar => 2) {
|
61
|
+
LSpace.new(:baz => 3) {
|
62
|
+
lambda &LSpace.preserve{ LSpace[:foo] + LSpace[:bar] + LSpace[:baz] }
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
p.call.should == 6
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should return to enclosing lspace after re-entering new lspace" do
|
71
|
+
LSpace.new(:baz => 1) do
|
72
|
+
p = LSpace.new(:baz => 2){ proc{ LSpace[:baz] }.in_lspace }
|
73
|
+
p.call.should == 2
|
74
|
+
LSpace[:baz].should == 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should clean up lspaces properly even if an exception is raised" do
|
79
|
+
LSpace.new(:baz => 1) do
|
80
|
+
begin
|
81
|
+
LSpace.new(:baz => 1) do
|
82
|
+
raise "OOPS"
|
83
|
+
end
|
84
|
+
rescue => e
|
85
|
+
LSpace[:baz].should == 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lspace
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.pre.1
|
5
|
+
prerelease: 4
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Conrad Irwin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-22 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Provides the convenience of global variables, without the safety concerns.
|
15
|
+
email: conrad.irwin@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- LICENSE.MIT
|
21
|
+
- README.md
|
22
|
+
- lib/lspace.rb
|
23
|
+
- lib/lspace/core_ext.rb
|
24
|
+
- lib/lspace/eventmachine.rb
|
25
|
+
- lib/lspace/thread.rb
|
26
|
+
- lspace.gemspec
|
27
|
+
- spec/lspace_spec.rb
|
28
|
+
- spec/spec_helper.rb
|
29
|
+
homepage: http://github.com/ConradIrwin/lspace
|
30
|
+
licenses: []
|
31
|
+
post_install_message:
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>'
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.3.1
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.8.24
|
50
|
+
signing_key:
|
51
|
+
specification_version: 3
|
52
|
+
summary: Provides local global storage
|
53
|
+
test_files: []
|
54
|
+
has_rdoc:
|