Class: LSpace

Inherits:
Object
  • Object
show all
Defined in:
lib/lspace.rb,
lib/lspace/class_methods.rb

Overview

An LSpace is an implicit namespace for storing state that is secondary to your application’s purpose, but still necessary.

In many ways they are the successor to the Thread-local namespace, but they are designed to be active during a logical segment of code, no matter how you slice that code amongst different Threads or Fibers.

The API for LSpace encourages creating a new sub-LSpace whenever you want to mutate the value of an LSpace-variable. This ensures that local changes take effect only for code that is logically contained within a block, avoiding many of the problems of mutable global state.

Examples:

require 'lspace/thread'
LSpace.with(:job_id => 1) do
  Thread.new do
    puts "processing #{LSpace[:job_id]}"
  end.join
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hash = {}, parent = LSpace.current, &block) ⇒ LSpace

Create a new LSpace.

By default the new LSpace will exactly mirror the currently active LSpace, though any variables you pass in will take precedence over those defined in the parent.

Parameters:

  • hash (Hash) (defaults to: {})

    New values for LSpace variables in this LSpace

  • parent (LSpace) (defaults to: LSpace.current)

    The parent LSpace that lookup should default to.

  • block (Proc)

    Will be called in the new lspace if present.



37
38
39
40
41
42
# File 'lib/lspace.rb', line 37

def initialize(hash={}, parent=LSpace.current, &block)
  @hash = hash
  @parent = parent
  @around_filters = []
  enter(&block) if block_given?
end

Instance Attribute Details

#around_filtersObject

Returns the value of attribute around_filters.



26
27
28
# File 'lib/lspace.rb', line 26

def around_filters
  @around_filters
end

#hashObject

Returns the value of attribute hash.



26
27
28
# File 'lib/lspace.rb', line 26

def hash
  @hash
end

#parentObject

Returns the value of attribute parent.



26
27
28
# File 'lib/lspace.rb', line 26

def parent
  @parent
end

Class Method Details

.[](key) ⇒ Object

Get the value for the key in the current LSpace or its parents

See Also:



113
114
115
# File 'lib/lspace/class_methods.rb', line 113

def self.[](key)
  current[key]
end

.[]=(key, value) ⇒ Object

Set the value for the key in the current LSpace

See Also:



120
121
122
# File 'lib/lspace/class_methods.rb', line 120

def self.[]=(key, value)
  current[key] = value
end

.around_filter(&filter) ⇒ Object

Add an around filter to the current LSpace

See Also:



134
135
136
# File 'lib/lspace/class_methods.rb', line 134

def self.around_filter(&filter)
  current.around_filter(&filter)
end

.clean(&block) ⇒ Object

Create a new clean LSpace.

This LSpace does not inherit any LSpace variables in the currently active LSpace.

Examples:

RSpec.configure do |c|
  c.around(:each) do |example|
    LSpace.clean do
      example.run
    end
  end
end

Parameters:

  • block (Proc)

    The logical block that will be run in the clean LSpace

See Also:



18
19
20
21
22
23
24
# File 'lib/lspace/class_methods.rb', line 18

def self.clean(&block)
  if block_given?
    enter new({}, nil), &block
  else
    new({}, nil)
  end
end

.currentLSpace

Get the current LSpace

Returns:



148
149
150
# File 'lib/lspace/class_methods.rb', line 148

def self.current
  Thread.current[:lspace] ||= LSpace.new({}, nil)
end

.enter(lspace, &block) ⇒ Object

Enter an LSpace for the logical duration of the block.

The LSpace will be active at least for the duration of the block’s callstack, but if the block creates any closures (using LSpace.preserve directly, or in library form) then the logical duration will also encompass code run in those closures.

Entering an LSpace will also cause any around_filters defined on it and its parents to be run.

Examples:

class Job
  def initialize
    @lspace = LSpace.new(:job_id => self.id)
    LSpace.enter(@lspace){ setup_lspace }
  end

  def run!
    LSpace.enter(@lspace) { run }
  end
end

Parameters:

  • lspace (LSpace)

    The LSpace to enter

  • block (Proc)

    The logical block to run with the given LSpace



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/lspace/class_methods.rb', line 70

def self.enter(lspace, &block)
  previous = current
  self.current = lspace

  filters = lspace.hierarchy.take_while{ |lspace| lspace != previous }.flatten.map(&:around_filters).flatten

  filters.inject(block) do |blk, filter|
    lambda{ filter.call(&blk) }
  end.call
ensure
  self.current = previous
end

.forkObject

Replace the current LSpace with a fork of it.

Forking the Lspace means that values changed with LSpace#[]= no longer affect parent LSpaces.

This should be used carefully - it may confuse around_filters, since they will see a different LSpace after the block is called than before.



91
92
93
# File 'lib/lspace/class_methods.rb', line 91

def self.fork
  self.current = LSpace.new({}, self.current)
end

.keysObject

Find all the keys currently in LSpace

See Also:



127
128
129
# File 'lib/lspace/class_methods.rb', line 127

def self.keys
  current.keys
end

.preserve(&block) ⇒ Object

Create a closure that will re-enter the current LSpace when the block is called.

Examples:

class TaskQueue
  def queue(&block)
    @queue << LSpace.preserve(&block)
  end
end

Parameters:

  • block (Proc)

    The logical block to wrap

See Also:



106
107
108
# File 'lib/lspace/class_methods.rb', line 106

def self.preserve(&block)
  current.wrap(&block)
end

.rescue(*exceptions, &handler) ⇒ Object

Add an exception handler

See Also:



141
142
143
# File 'lib/lspace/class_methods.rb', line 141

def self.rescue(*exceptions, &handler)
  current.rescue(*exceptions, &handler)
end

.with(hash = {}, &block) ⇒ Object

Create a new LSpace with the given keys set to the given values, and run the given block in that new LSpace.

The LSpace will inherit any unspecified keys from the currently active LSpace.

Examples:

LSpace.with :user_id => 6 do
  LSpace.with :job_id => 7 do
    LSpace[:user_id] == 6
    LSpace[:job_id] == 7
  end
end

Parameters:

  • hash (Hash) (defaults to: {})

    The keys to update

  • block (Proc)

    The logical block to run with the updated LSpace

See Also:



42
43
44
# File 'lib/lspace/class_methods.rb', line 42

def self.with(hash={}, &block)
  enter new(hash, current), &block
end

Instance Method Details

#[](key) ⇒ Object

Get the most specific value for the key.

If the key is not present in the hash of this LSpace, lookup proceeds up the chain of parent LSpaces. If the key is not found anywhere, nil is returned.

Examples:

LSpace.with :user_id => 5 do
  LSpace.with :user_id => 6 do
    LSpace[:user_id] == 6
  end
end

Parameters:

  • key (Object)

Returns:

  • (Object)


57
58
59
60
61
62
63
# File 'lib/lspace.rb', line 57

def [](key)
  hierarchy.each do |lspace|
    return lspace.hash[key] if lspace.hash.has_key?(key)
  end

  nil
end

#[]=(key, value) ⇒ Object

Update the LSpace-variable with the given name.

Bear in mind that any code using this LSpace will see this change, and consider using with or fork instead to localize your changes.

This method is mostly useful for setting up a new LSpace before any code is using it, and has no effect on parent LSpaces.

Examples:

lspace = LSpace.new
lspace[:user_id] = 6
LSpace.enter(lspace) do
  LSpace[:user_id] == 6
end

Parameters:

  • key (Object)
  • value (Object)


81
82
83
# File 'lib/lspace.rb', line 81

def []=(key, value)
  hash[key] = value
end

#around_filter(&filter) ⇒ Object

Add an around_filter to this LSpace.

Around filters are blocks that take a block-parameter. They are called whenever the LSpace is re-entered, so they are suitable for implementing integrations between LSpace and libraries that rely on Thread-local state (like Log4r) or for adding fallback exception handlers to your logical segment of code (to prevent exceptions from killing your Thread-pool or event loop).

Examples:

lspace = LSpace.new
lspace.around_filter do |&block|
  begin
    block.call
  rescue => e
    puts "Job #{LSpace[:job_id]} failed with: #{e}"
  end
end

LSpace.enter(lspace) do
  Thread.new{ raise "foo" }.join
end


119
120
121
122
# File 'lib/lspace.rb', line 119

def around_filter(&filter)
  around_filters.unshift filter
  self
end

#enter(&block) ⇒ Object

Enter this LSpace for the duration of the block

Parameters:

  • block (Proc)

    The block to run

See Also:



152
153
154
# File 'lib/lspace.rb', line 152

def enter(&block)
  LSpace.enter(self, &block)
end

#hierarchyArray<LSpace>

Get the list of Lspaces up to the root, most specific first

Returns:



174
175
176
177
178
179
180
# File 'lib/lspace.rb', line 174

def hierarchy
  @hierarchy ||= if parent
                   [self] + parent.hierarchy
                 else
                   [self]
                 end
end

#keysArray

Return the list of keys in the current LSpace or its parents.

Examples:

parent = LSpace.new(:user_id => 5)
child  = LSpace.new(:friend_id => 7, parent)
child.keys == [:user_id, :friend_id]

Returns:

  • (Array)


93
94
95
# File 'lib/lspace.rb', line 93

def keys
  hierarchy.flat_map{ |lspace| lspace.hash.keys }.uniq
end

#rescue(*exceptions, &handler) ⇒ Object

Add an error handler to this LSpace.

Examples:

lspace = LSpace.new
lspace.rescue do |e|
  puts "Job #{LSpace[:job_id]} failed with: #{e}
end

LSpace.enter(lspace) do
  Thread.new{ reaise "foo" }.join
end


136
137
138
139
140
141
142
143
144
145
146
# File 'lib/lspace.rb', line 136

def rescue(*exceptions, &handler)
  exceptions << RuntimeError unless exceptions.any?

  around_filter do |&block|
    begin
      block.call
    rescue *exceptions => e
      handler.call e
    end
  end
end

#wrap(&original) ⇒ Object

Wraps a block/proc such that it runs in this LSpace when it is called.



160
161
162
163
164
165
166
167
168
169
# File 'lib/lspace.rb', line 160

def wrap(&original)
  # Store self so that it works if the block is instance_eval'd
  shelf = self

  proc do |*args, &block|
    shelf.enter do
      original.call(*args, &block)
    end
  end
end