lspace 0.1.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ class << Thread
2
+ in_lspace :new, :start, :fork
3
+ end
4
+
5
+ module Kernel
6
+ in_lspace :fork
7
+ 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
@@ -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
@@ -0,0 +1,8 @@
1
+ require_relative '../lib/lspace'
2
+ RSpec.configure do |c|
3
+ c.around(:each) do |example|
4
+ LSpace.new do
5
+ example.run
6
+ end
7
+ end
8
+ end
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: