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