lspace 0.1.pre.1 → 0.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/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
data/README.md
CHANGED
@@ -1,23 +1,159 @@
|
|
1
|
-
|
1
|
+
LSpace, named after the Discworld's [L-Space](http://en.wikipedia.org/wiki/L-Space), is an
|
2
|
+
implementation of dynamic scoping for Ruby.
|
2
3
|
|
3
|
-
|
4
|
+
Dynamic scope is a fancy term for a variable which changes its value depending on the
|
5
|
+
current context that your application is running in. i.e. the same function can see a
|
6
|
+
different value for a dynamically scoped variable depending on the code-path taken to
|
7
|
+
reach that function.
|
4
8
|
|
5
|
-
|
6
|
-
|
9
|
+
This is particularly useful for implementing many utility functions in applications. For
|
10
|
+
example, let's say I want to use the master database connection for some database
|
11
|
+
operations. I don't want to have to pass a reference to the database connection all the
|
12
|
+
way throughout my code, so I just push it into the LSpace:
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
|
14
|
+
```ruby
|
15
|
+
require 'lspace'
|
16
|
+
class DatabaseConnection
|
17
|
+
def get_connection
|
18
|
+
LSpace[:preferred_connection] || any_free_connection
|
19
|
+
end
|
11
20
|
|
12
|
-
|
21
|
+
def self.use_master(&block)
|
22
|
+
LSpace.update(:preferred_connection => master_connection) do
|
23
|
+
block.call
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
13
27
|
|
14
|
-
|
15
|
-
|
28
|
+
DatabaseConnection.use_master do
|
29
|
+
very_important_transactions!
|
30
|
+
end
|
31
|
+
```
|
16
32
|
|
17
|
-
|
18
|
-
|
19
|
-
accidentally leaks.
|
33
|
+
Everything that happens in the `very_important_transactions!` block will use
|
34
|
+
`LSpace[:preferred_connection]`, which is set to be the master database.
|
20
35
|
|
21
|
-
|
22
|
-
|
23
|
-
|
36
|
+
This is useful for a whole host of stuff, we use it to ensure that every line logged by a
|
37
|
+
given Http request is prefixed by a unique value, so we can tie them back together again.
|
38
|
+
We also use it for generating trees of performance metrics.
|
39
|
+
|
40
|
+
All of these concerns have one thing in common: they're not important to what your program
|
41
|
+
is trying to do, but they are important for the way your program is trying to do things.
|
42
|
+
It doesn't make sense to stuff everything into `LSpace`, though early versions of Lisp
|
43
|
+
essentialy did that, because it makes your code harder to understand.
|
44
|
+
|
45
|
+
Eventmachine
|
46
|
+
============
|
47
|
+
|
48
|
+
LSpace also comes with optional eventmachine integration. This adds a few hooks to
|
49
|
+
Eventmachine to ensure that the current LSpace is preserved, even if your code has
|
50
|
+
asynchronous callbacks; or runs things in eventmachine's threadpool:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
require 'lspace/eventmachine'
|
54
|
+
require 'em-http-request'
|
55
|
+
|
56
|
+
class Fetcher
|
57
|
+
lspace_reader :log_prefix
|
58
|
+
|
59
|
+
def log(str)
|
60
|
+
puts "#{log_prefix}\t#{str}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch(url)
|
64
|
+
log "Fetching #{url}"
|
65
|
+
EM::HttpRequest.new(url).get.callback do
|
66
|
+
log "Fetched #{url}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
EM::run do
|
72
|
+
LSpace.update(:log_prefix => rand(50000)) do
|
73
|
+
Fetcher.new.fetch("http://www.google.com")
|
74
|
+
Fetcher.new.fetch("http://www.yahoo.com")
|
75
|
+
end
|
76
|
+
LSpace.update(:log_prefix => rand(50000)) do
|
77
|
+
Fetcher.new.fetch("http://www.microsoft.com")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Around filters
|
83
|
+
==============
|
84
|
+
|
85
|
+
In addition to just storing variables across call-stacks, LSpace allows you to wrap each
|
86
|
+
re-entry to your code with around filters. This lets you do things like maintain
|
87
|
+
thread-local state in libraries like log4r that don't support LSpace.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
LSpace.around_filter do |&block|
|
91
|
+
previous_context = Log4r::MDC.get :context
|
92
|
+
begin
|
93
|
+
Log4r::MDC.put :context, LSpace[:log_context]
|
94
|
+
block.call
|
95
|
+
ensure
|
96
|
+
Log4r::MDC.put :context, previous_context
|
97
|
+
end
|
98
|
+
end
|
99
|
+
```
|
100
|
+
|
101
|
+
You can also use this to log any unhandled exceptions that happen while your job is
|
102
|
+
running without hitting the eventmachine default error handler:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
LSpace.around_filter do |&block|
|
106
|
+
begin
|
107
|
+
block.call
|
108
|
+
rescue => e
|
109
|
+
puts "Got exception running #{LSpace[:job_id]}: #{e}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
Integrating with new libraries
|
115
|
+
================================
|
116
|
+
|
117
|
+
If you are using a Thread-pool, or an actor system, or an event loop, you will need to
|
118
|
+
teach it about LSpace in order to get the full benefit of the system.
|
119
|
+
|
120
|
+
There are two kinds of integration. Firstly, when your library accepts blocks from the
|
121
|
+
programmer's code, and proceeds to run them on a different call-stack, you should call
|
122
|
+
`Proc#in_lspace`:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
def enqueue_task(&block)
|
126
|
+
$todo << block.in_lspace
|
127
|
+
end
|
128
|
+
```
|
129
|
+
|
130
|
+
This will ensure that the user's current LSpace is re-activated when the block is run. You
|
131
|
+
can automate this by using the `in_lspace` wrapper function at the module level:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
class Scheduler
|
135
|
+
def enqueue_task(&block)
|
136
|
+
$todo << block
|
137
|
+
end
|
138
|
+
in_lspace :enqueue_task
|
139
|
+
end
|
140
|
+
```
|
141
|
+
|
142
|
+
Secondly, when your library creates objects that call out to the user's code, it's polite
|
143
|
+
to re-use the same `LSpace` across each call:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
class Job
|
147
|
+
def initialize
|
148
|
+
@lspace = LSpace.new
|
149
|
+
end
|
150
|
+
|
151
|
+
def run_internal
|
152
|
+
LSpace.enter(@lspace) { run }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
A new `LSpace` will by default inherit everything from its parent, so it's better to store
|
158
|
+
`LSpace.new` than `LSpace.current`, so that if the user mutates their LSpace in a
|
159
|
+
callback, the change does not propagate upwards.
|
data/lib/lspace.rb
CHANGED
@@ -1,214 +1,144 @@
|
|
1
1
|
require File.expand_path('../lspace/core_ext', __FILE__)
|
2
|
+
require File.expand_path('../lspace/class_methods', __FILE__)
|
2
3
|
|
3
|
-
|
4
|
+
# An LSpace is an implicit namespace for storing state that is secondary to your
|
5
|
+
# application's purpose, but still necessary.
|
6
|
+
#
|
7
|
+
# In many ways they are the successor to the Thread-local namespace, but they are designed
|
8
|
+
# to be active during a logical segment of code no-matter how you slice that code amoungst
|
9
|
+
# different Threads or Fibers.
|
10
|
+
#
|
11
|
+
# The API for LSpace encourages creating a new sub-LSpace whenever you want to mutate the
|
12
|
+
# value of an LSpace-variable. This ensures that local changes take effect only for code
|
13
|
+
# that is logically contained within a block, avoiding many of the problems of mutable
|
14
|
+
# global state.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# require 'lspace/thread'
|
18
|
+
# LSpace.update(:job_id => 1) do
|
19
|
+
# Thread.new do
|
20
|
+
# puts "processing #{LSpace[:job_id]}"
|
21
|
+
# end.join
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
class LSpace
|
4
25
|
|
5
|
-
|
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
|
26
|
+
attr_accessor :hash, :parent, :around_filters
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
28
|
+
# Create a new LSpace.
|
29
|
+
#
|
30
|
+
# By default the new LSpace will exactly mirror the currently active LSpace,
|
31
|
+
# though any variables you pass in will take precedence over those defined in the
|
32
|
+
# parent.
|
33
|
+
#
|
34
|
+
# @param [Hash] hash New values for LSpace variables in this LSpace
|
35
|
+
# @param [LSpace] parent The parent LSpace that lookup should default to.
|
36
|
+
# @param [Proc] block Will be called in the new lspace if present.
|
37
|
+
def initialize(hash={}, parent=LSpace.current, &block)
|
38
|
+
@hash = hash
|
39
|
+
@parent = parent
|
40
|
+
@around_filters = []
|
41
|
+
enter(&block) if block_given?
|
42
|
+
end
|
49
43
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
44
|
+
# Get the most specific value for the key.
|
45
|
+
#
|
46
|
+
# If the key is not present in the hash of this LSpace, lookup proceeds up the chain
|
47
|
+
# of parent LSpaces. If the key is not found anywhere, nil is returned.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# LSpace.update :user_id => 5 do
|
51
|
+
# LSpace.update :user_id => 6 do
|
52
|
+
# LSpace[:user_id] == 6
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
# @param [Object] key
|
56
|
+
# @return [Object]
|
57
|
+
def [](key)
|
58
|
+
hierarchy.each do |lspace|
|
59
|
+
return lspace.hash[key] if lspace.hash.has_key?(key)
|
84
60
|
end
|
85
61
|
|
86
|
-
|
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
|
62
|
+
nil
|
63
|
+
end
|
108
64
|
|
109
|
-
|
110
|
-
|
65
|
+
# Update the LSpace-variable with the given name.
|
66
|
+
#
|
67
|
+
# Bear in mind that any code using this LSpace will see this change, and consider
|
68
|
+
# using {LSpace.update} instead to localize your changes.
|
69
|
+
#
|
70
|
+
# This method is mostly useful for setting up a new LSpace before any code is
|
71
|
+
# using it, and has no effect on parent LSpaces.
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# lspace = LSpace.new
|
75
|
+
# lspace[:user_id] = 6
|
76
|
+
# LSpace.enter(lspace) do
|
77
|
+
# LSpace[:user_id] == 6
|
78
|
+
# end
|
79
|
+
# @param [Object] key
|
80
|
+
# @param [Object] value
|
81
|
+
def []=(key, value)
|
82
|
+
hash[key] = value
|
83
|
+
end
|
111
84
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
85
|
+
# Add an around_filter to this LSpace.
|
86
|
+
#
|
87
|
+
# Around filters are blocks that take a block-parameter. They are called whenever
|
88
|
+
# the LSpace is re-entered, so they are suitable for implementing integrations between
|
89
|
+
# LSpace and libraries that rely on Thread-local state (like Log4r) or for adding
|
90
|
+
# fallback exception handlers to your logical segment of code (to prevent exceptions
|
91
|
+
# from killing your Thread-pool or event loop).
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# lspace = LSpace.new
|
95
|
+
# lspace.around_filter do |&block|
|
96
|
+
# begin
|
97
|
+
# block.call
|
98
|
+
# rescue => e
|
99
|
+
# puts "Job #{LSpace[:job_id]} failed with: #{e}"
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# LSpace.enter(lspace) do
|
104
|
+
# Thread.new{ raise "foo" }.join
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
def around_filter(&filter)
|
108
|
+
around_filters.unshift filter
|
109
|
+
end
|
118
110
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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
|
111
|
+
# Enter this LSpace for the duration of the block
|
112
|
+
#
|
113
|
+
# @see LSpace.enter
|
114
|
+
# @param [Proc] block The block to run
|
115
|
+
def enter(&block)
|
116
|
+
LSpace.enter(self, &block)
|
117
|
+
end
|
134
118
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
119
|
+
# Ensure that the Proc runs in this LSpace
|
120
|
+
#
|
121
|
+
# @see Proc#in_lspace
|
122
|
+
# @see LSpace.preserve
|
123
|
+
def wrap(&original)
|
124
|
+
# Store self so that it works if the block is instance_eval'd
|
125
|
+
shelf = self
|
141
126
|
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
127
|
+
proc do |*args, &block|
|
128
|
+
shelf.enter do
|
129
|
+
original.call(*args, &block)
|
180
130
|
end
|
181
131
|
end
|
132
|
+
end
|
182
133
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
134
|
+
# Get the list of Lspaces up to the root, most specific first
|
135
|
+
#
|
136
|
+
# @return [Array<LSpace>]
|
137
|
+
def hierarchy
|
138
|
+
if parent
|
139
|
+
[self] + parent.hierarchy
|
140
|
+
else
|
141
|
+
[self]
|
212
142
|
end
|
213
143
|
end
|
214
144
|
end
|