lspace 0.2 → 0.3
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 +112 -61
- data/examples/celluloid.rb +45 -0
- data/examples/eventmachine.rb +28 -0
- data/lib/lspace/celluloid.rb +29 -0
- data/lib/lspace/eventmachine.rb +6 -1
- data/lspace.gemspec +4 -1
- data/spec/celluloid_spec.rb +50 -0
- data/spec/spec_helper.rb +1 -0
- metadata +54 -2
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
|
5
|
-
|
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
|
-
|
10
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
11
|
+
LSpace.with(:user_id => 5) do
|
12
|
+
LSpace[:user_id] == 5
|
13
|
+
end
|
14
|
+
LSpace[:user_id] == nil
|
15
|
+
```
|
20
16
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
46
|
-
|
61
|
+
LSpace.with(:user_id => 5) do
|
62
|
+
Actor.new.example!
|
63
|
+
end
|
64
|
+
```
|
47
65
|
|
48
|
-
|
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
|
-
|
53
|
-
|
54
|
-
require 'em-http-request'
|
68
|
+
`lspace_reader`
|
69
|
+
===============
|
55
70
|
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
74
|
+
```ruby
|
75
|
+
class Task
|
76
|
+
lspace_reader :user_id
|
62
77
|
|
63
|
-
def
|
64
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
102
|
-
|
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
|
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
|
data/lib/lspace/eventmachine.rb
CHANGED
@@ -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)
|
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.
|
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
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.
|
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-
|
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
|