lspace 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,90 +1,100 @@
1
1
  LSpace, named after the Discworld's [L-Space](http://en.wikipedia.org/wiki/L-Space), is an
2
2
  implementation of dynamic scoping for Ruby.
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
+ Dynamic scope
5
+ =============
8
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
+ Variables that are stored inside an LSpace are dynamically scoped, this means that they
8
+ take effect only for the duration of a block:
13
9
 
14
10
  ```ruby
15
- require 'lspace'
16
- class DatabaseConnection
17
- def get_connection
18
- LSpace[:preferred_connection] || any_free_connection
19
- end
11
+ LSpace.with(:user_id => 5) do
12
+ LSpace[:user_id] == 5
13
+ end
14
+ LSpace[:user_id] == nil
15
+ ```
20
16
 
21
- def self.use_master(&block)
22
- LSpace.with(:preferred_connection => master_connection) do
23
- block.call
24
- end
17
+ You can enter a new LSpace as many times as you need, to add as much state as you need:
18
+
19
+ ```ruby
20
+ LSpace.with(:user_id => 5) do
21
+ LSpace.with(:database_shard => 7) do
22
+ LSpace[:user_id] == 5
23
+ LSpace[:database_shard] == 7
25
24
  end
26
25
  end
26
+ ```
27
27
 
28
- DatabaseConnection.use_master do
29
- very_important_transactions!
28
+ Operation safety
29
+ ================
30
+
31
+ LSpace is thread-safe, so entering a new LSpace on one thread won't affect any of the
32
+ other Threads. In addition, LSpace also comes with extensions for
33
+ [eventmachine](https://github.com/eventmachine/eventmachine) and
34
+ [celluloid](http://celluloid.io/) which extends the notion of thread-safety to
35
+ operation-safety.
36
+
37
+ This means that even if you're doing multiple things on one thread, or one thing using
38
+ many threads, the changes you make to LSpace will still be local to that thing.
39
+
40
+ ```ruby
41
+ require 'lspace/eventmachine'
42
+ EM::run
43
+ LSpace.with(:user_id => 5) do
44
+ EM::defer{ LSpace[:user_id] == 5; EM::stop }
45
+ end
30
46
  end
31
47
  ```
32
48
 
33
- Everything that happens in the `very_important_transactions!` block will use
34
- `LSpace[:preferred_connection]`, which is set to be the master database.
49
+ See also [examples/eventmachine.rb](https://github.com/ConradIrwin/lspace/tree/master/examples/eventmachine.rb).
35
50
 
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
51
 
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.
52
+ ```ruby
53
+ require 'lspace/celluloid'
54
+ class Actor
55
+ include Celluloid
56
+ def example
57
+ LSpace[:user_id] == 5
58
+ end
59
+ end
44
60
 
45
- Eventmachine
46
- ============
61
+ LSpace.with(:user_id => 5) do
62
+ Actor.new.example!
63
+ end
64
+ ```
47
65
 
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:
66
+ See also [examples/celluloid.rb](https://github.com/ConradIrwin/lspace/tree/master/examples/celluloid.rb).
51
67
 
52
- ```ruby
53
- require 'lspace/eventmachine'
54
- require 'em-http-request'
68
+ `lspace_reader`
69
+ ===============
55
70
 
56
- class Fetcher
57
- lspace_reader :log_prefix
71
+ Because reading from the current LSpace is the most common thing to do, you can define an
72
+ accessor function that lets you do this:
58
73
 
59
- def log(str)
60
- puts "#{log_prefix}\t#{str}"
61
- end
74
+ ```ruby
75
+ class Task
76
+ lspace_reader :user_id
62
77
 
63
- def fetch(url)
64
- log "Fetching #{url}"
65
- EM::HttpRequest.new(url).get.callback do
66
- log "Fetched #{url}"
67
- end
78
+ def process
79
+ puts "Running #{self} for User##{user_id}"
68
80
  end
69
81
  end
70
82
 
71
- EM::run do
72
- LSpace.with(: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.with(:log_prefix => rand(50000)) do
77
- Fetcher.new.fetch("http://www.microsoft.com")
78
- end
83
+ LSpace.with(:user_id => 7) do
84
+ Task.new.process
79
85
  end
80
86
  ```
81
87
 
82
88
  Around filters
83
89
  ==============
84
90
 
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.
91
+ The ability of LSpace to be operation-local instead of merely thread local also enables
92
+ you to add around filters to your code. Whenever your operation jumps between threads,
93
+ or fires a callback, the around filters are called so that code running in the context of
94
+ your operation is always wrapped.
95
+
96
+ This is useful for maintaining operation-local state in libraries that only support
97
+ thread-local state (like Log4r):
88
98
 
89
99
  ```ruby
90
100
  LSpace.around_filter do |&block|
@@ -97,9 +107,9 @@ LSpace.around_filter do |&block|
97
107
  end
98
108
  end
99
109
  ```
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:
110
+ You can also use this to log any unhandled exceptions that happen while your operation is
111
+ running without hitting the default error handler for your thread-pool or event loop. This
112
+ makes tracking down the causes of unexpected exceptions much easier:
103
113
 
104
114
  ```ruby
105
115
  LSpace.around_filter do |&block|
@@ -111,11 +121,52 @@ LSpace.around_filter do |&block|
111
121
  end
112
122
  ```
113
123
 
124
+ Use cases
125
+ =========
126
+
127
+ LSpace is good for the parts of your application that are not directly relevant to what
128
+ you're actually trying to do, but are important to the manner in which your application is
129
+ written.
130
+
131
+ For example, when showing a user's page, it's normally fine to use a database slave. If
132
+ the user is looking at their own page, then it's important to use a master database in
133
+ case they've just edited their profile. To implement this without LSpace you have to push
134
+ the `use_master_database` flag down through all of your page-rendering logic. With LSpace
135
+ you can make this change in a much less brittle way:
136
+
137
+ ```ruby
138
+ require 'lspace'
139
+ class DatabaseConnection
140
+ def get_connection
141
+ LSpace[:preferred_connection] || any_free_connection
142
+ end
143
+
144
+ def self.use_master(&block)
145
+ LSpace.with(:preferred_connection => master_connection) do
146
+ block.call
147
+ end
148
+ end
149
+ end
150
+
151
+ DatabaseConnection.use_master do
152
+ very_important_transactions!
153
+ end
154
+ ```
155
+
156
+ Another good example is logging. We want to prefix log messages involved with handling one
157
+ particular web request with the same unique string every time, so that we can tie all of
158
+ those message together despite a large number of concurrent requests being handled.
159
+ Without LSpace this would be a nightmare, as we'd have to push the `log_prefix` down into
160
+ all parts of our code, with LSpace it becomes simple.
161
+
162
+ Because the changes to LSpace are only visible within the current operation, or current
163
+ block, it's much safer than global state; though it has many of the same benefits.
164
+
114
165
  Integrating with new libraries
115
166
  ================================
116
167
 
117
168
  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.
169
+ teach it about LSpace in order to get the correct operation-local semantics.
119
170
 
120
171
  There are two kinds of integration. Firstly, when your library accepts blocks from the
121
172
  programmer's code, and proceeds to run them on a different call-stack, you should call
@@ -0,0 +1,45 @@
1
+ require_relative '../lib/lspace/celluloid'
2
+ module Logging
3
+ lspace_reader :log_prefix
4
+ def log(str)
5
+ puts "INFO #{log_prefix}: #{str}"
6
+ end
7
+ end
8
+ class Customer
9
+ include Logging
10
+ include Celluloid
11
+ def initialize(bob)
12
+ @bob = bob
13
+ end
14
+
15
+ def eat_lunch
16
+ consume @bob.make_sandwich
17
+ end
18
+
19
+ def consume(sandwich)
20
+ log "eating a #{sandwich}"
21
+ end
22
+ end
23
+
24
+ class Caterer
25
+ include Logging
26
+ include Celluloid
27
+ def make_sandwich
28
+ choice = ["Bacon", "Lettuce", "Tomato", "Ham", "Cheese", "Pickle", "Nutella"].sample
29
+ log "making a #{choice} sandwich"
30
+ sleep rand
31
+ "#{choice} sandwich"
32
+ end
33
+ end
34
+
35
+ bob = Caterer.new
36
+
37
+ LSpace.with(:log_prefix => "Table 1") do
38
+ Customer.new(bob).eat_lunch!
39
+ Customer.new(bob).eat_lunch!
40
+ end
41
+ LSpace.with(:log_prefix => "Table 2") do
42
+ Customer.new(bob).eat_lunch!
43
+ end
44
+
45
+ sleep 1
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ require 'lspace/eventmachine'
3
+ require 'em-http-request'
4
+
5
+ class Fetcher
6
+ lspace_reader :log_prefix
7
+
8
+ def log(str)
9
+ puts "#{log_prefix}\t#{str}"
10
+ end
11
+
12
+ def fetch(url)
13
+ log "Fetching #{url}"
14
+ EM::HttpRequest.new(url).get.callback do
15
+ log "Fetched #{url}"
16
+ end
17
+ end
18
+ end
19
+
20
+ EM::run do
21
+ LSpace.with(:log_prefix => rand(50000)) do
22
+ Fetcher.new.fetch("http://www.google.com")
23
+ Fetcher.new.fetch("http://www.yahoo.com")
24
+ end
25
+ LSpace.with(:log_prefix => rand(50000)) do
26
+ Fetcher.new.fetch("http://www.microsoft.com")
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ require 'celluloid'
2
+ require 'lspace'
3
+
4
+ module Celluloid
5
+ class Call
6
+ alias_method :initialize_without_lspace, :initialize
7
+
8
+ def initialize(*args, &block)
9
+ initialize_without_lspace(*args, &block)
10
+ @lspace = LSpace.new
11
+ end
12
+ end
13
+
14
+ class SyncCall < Call
15
+ alias_method :dispatch_without_lspace, :dispatch
16
+
17
+ def dispatch(*args, &block)
18
+ LSpace.enter(@lspace) { dispatch_without_lspace(*args, &block) }
19
+ end
20
+ end
21
+
22
+ class AsyncCall < Call
23
+ alias_method :dispatch_without_lspace, :dispatch
24
+
25
+ def dispatch(*args, &block)
26
+ LSpace.enter(@lspace) { dispatch_without_lspace(*args, &block) }
27
+ end
28
+ end
29
+ end
@@ -50,6 +50,7 @@ module EventMachine
50
50
  # @example
51
51
  # module EchoServer
52
52
  # def setup_lspace
53
+ # LSpace[:log_prefix] = rand(100000).to_s(16)
53
54
  # LSpace.around_filter do |&block|
54
55
  # begin
55
56
  # block.call
@@ -76,7 +77,11 @@ module EventMachine
76
77
  # EM uses the arity of unbind to decide which arguments to pass it.
77
78
  # AFAIK the no-argument version is considerably more popular, so we use that here.
78
79
  [:unbind].each do |method|
79
- define_method(method) { LSpace.enter(@lspace) { super() } }
80
+ define_method(method) do |*a, &b|
81
+ LSpace.enter(@lspace) do
82
+ super()
83
+ end
84
+ end
80
85
  end
81
86
  end
82
87
  end
data/lspace.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "lspace"
3
- s.version = "0.2"
3
+ s.version = "0.3"
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.author = "Conrad Irwin"
6
6
  s.email = "conrad.irwin@gmail.com"
@@ -14,4 +14,7 @@ Gem::Specification.new do |s|
14
14
  s.add_development_dependency 'pry-rescue'
15
15
  s.add_development_dependency 'pry-stack_explorer'
16
16
  s.add_development_dependency 'eventmachine'
17
+ s.add_development_dependency 'celluloid'
18
+ s.add_development_dependency 'yard'
19
+ s.add_development_dependency 'redcarpet'
17
20
  end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe LSpace do
4
+ it "should be preserved across sync calls" do
5
+ seen = nil
6
+ actor = Class.new do
7
+ include Celluloid
8
+ define_method(:update_seen) do
9
+ seen = LSpace[:to_see]
10
+ end
11
+ end
12
+
13
+ LSpace.with(:to_see => 5) {
14
+ actor.new.update_seen
15
+ }
16
+ seen.should == 5
17
+ end
18
+
19
+ it "should be preserved across async calls" do
20
+ seen = nil
21
+ actor = Class.new do
22
+ include Celluloid
23
+ define_method(:update_seen) do
24
+ seen = LSpace[:to_see]
25
+ end
26
+ end
27
+
28
+ LSpace.with(:to_see => 7) {
29
+ actor.new.async.update_seen
30
+ }
31
+ sleep 0.1 # TODO, actor.join or equivalent?
32
+ seen.should == 7
33
+ end
34
+
35
+ it "should be preserved across async calls" do
36
+ seen = nil
37
+ actor = Class.new do
38
+ include Celluloid
39
+ define_method(:update_seen) do
40
+ seen = LSpace[:to_see]
41
+ end
42
+ end
43
+
44
+ LSpace.with(:to_see => 7) {
45
+ f = actor.new.future.update_seen
46
+ f.value
47
+ }
48
+ seen.should == 7
49
+ end
50
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative '../lib/lspace'
2
2
  require_relative '../lib/lspace/eventmachine'
3
+ require_relative '../lib/lspace/celluloid'
3
4
  require 'pry-rescue/rspec'
4
5
 
5
6
  RSpec.configure do |c|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lspace
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '0.3'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-27 00:00:00.000000000 Z
12
+ date: 2012-11-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -75,6 +75,54 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: celluloid
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: yard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: redcarpet
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
78
126
  description: Provides the convenience of global variables, without the safety concerns.
79
127
  email: conrad.irwin@gmail.com
80
128
  executables: []
@@ -85,12 +133,16 @@ files:
85
133
  - Gemfile
86
134
  - LICENSE.MIT
87
135
  - README.md
136
+ - examples/celluloid.rb
137
+ - examples/eventmachine.rb
88
138
  - lib/lspace.rb
139
+ - lib/lspace/celluloid.rb
89
140
  - lib/lspace/class_methods.rb
90
141
  - lib/lspace/core_ext.rb
91
142
  - lib/lspace/eventmachine.rb
92
143
  - lib/lspace/thread.rb
93
144
  - lspace.gemspec
145
+ - spec/celluloid_spec.rb
94
146
  - spec/class_method_spec.rb
95
147
  - spec/core_ext_spec.rb
96
148
  - spec/eventmachine_spec.rb