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 CHANGED
@@ -1,23 +1,159 @@
1
- *in progress*
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
- LSpace Safe operation-local storage.
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
- 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...
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
- 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...
14
+ ```ruby
15
+ require 'lspace'
16
+ class DatabaseConnection
17
+ def get_connection
18
+ LSpace[:preferred_connection] || any_free_connection
19
+ end
11
20
 
12
- Operation-local variables are most awesome!
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
- `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.
28
+ DatabaseConnection.use_master do
29
+ very_important_transactions!
30
+ end
31
+ ```
16
32
 
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.
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
- 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.
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.
@@ -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
- module LSpace
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
- 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
26
+ attr_accessor :hash, :parent, :around_filters
25
27
 
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
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
- # 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
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
- # 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
62
+ nil
63
+ end
108
64
 
109
- new_frames = active.take_while{ |space| space != previous }
110
- filters = new_frames.map{ |space| space[:around_filter] }.compact
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
- filters.inject(block) do |block, filter|
113
- lambda{ filter.call(&block) }
114
- end.call
115
- ensure
116
- self.current = previous
117
- end
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
- # 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
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
- proc do |*args, &block|
136
- LSpace.enter(current) do
137
- original.call(*args, &block)
138
- end
139
- end
140
- end
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
- # 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
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
- # 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
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