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 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