graph_mediator 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +4 -0
- data/.gitignore +26 -0
- data/LICENSE +20 -0
- data/README.rdoc +136 -0
- data/Rakefile +32 -0
- data/graph_mediator.gemspec +31 -0
- data/lib/graph_mediator.rb +509 -0
- data/lib/graph_mediator/locking.rb +50 -0
- data/lib/graph_mediator/mediator.rb +260 -0
- data/lib/graph_mediator/version.rb +3 -0
- data/spec/database.rb +12 -0
- data/spec/examples/course_example_spec.rb +91 -0
- data/spec/examples/dingo_pen_example_spec.rb +288 -0
- data/spec/graph_mediator_spec.rb +500 -0
- data/spec/integration/changes_spec.rb +159 -0
- data/spec/integration/locking_tests_spec.rb +214 -0
- data/spec/integration/nesting_spec.rb +113 -0
- data/spec/integration/threads_spec.rb +59 -0
- data/spec/integration/validation_spec.rb +19 -0
- data/spec/investigation/alias_method_chain_spec.rb +170 -0
- data/spec/investigation/insert_subclass_spec.rb +122 -0
- data/spec/investigation/insert_superclass_spec.rb +131 -0
- data/spec/investigation/module_super_spec.rb +88 -0
- data/spec/investigation/self_decorating.rb +55 -0
- data/spec/mediator_spec.rb +201 -0
- data/spec/reservations/lodging.rb +4 -0
- data/spec/reservations/party.rb +4 -0
- data/spec/reservations/party_lodging.rb +4 -0
- data/spec/reservations/reservation.rb +18 -0
- data/spec/reservations/schema.rb +33 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +65 -0
- metadata +173 -0
data/.document
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
.current
|
16
|
+
.vimrc
|
17
|
+
|
18
|
+
## PROJECT::GENERAL
|
19
|
+
coverage
|
20
|
+
rdoc
|
21
|
+
pkg
|
22
|
+
*.gem
|
23
|
+
|
24
|
+
## PROJECT::SPECIFIC
|
25
|
+
*.log
|
26
|
+
thread-test
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Josh Partlow
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
= Graph Mediator
|
2
|
+
|
3
|
+
GraphMediator is used to help coordinate state between a graph of ActiveRecord
|
4
|
+
objects related to a single root node. Its role is assisting in cases where
|
5
|
+
you are representing a complex concept as a graph of related objects with
|
6
|
+
potentially circular interdependencies. Changing attributes in one object
|
7
|
+
might require adding or removing of dependent objects. Adding these objects
|
8
|
+
might necessitate a recalculation of memberships in a join table. Any such
|
9
|
+
changes might require that cached calculations be redone. Touching any object
|
10
|
+
in the graph might require a version bump for the concept of the graph as a
|
11
|
+
whole.
|
12
|
+
|
13
|
+
We want changes to be made once, in a single transaction, with a single overall
|
14
|
+
version change. The version change should be guarded by an optimistic lock
|
15
|
+
check to avoid conflicts between two processes updates to the same graph.
|
16
|
+
|
17
|
+
To make interdependent state changes manageable, GraphMediator wraps an
|
18
|
+
additional layer of callbacks around the ActiveRecord save cycle to ensure
|
19
|
+
that a save occurs within a GraphMediator.mediated_transaction.
|
20
|
+
|
21
|
+
* :before_mediation
|
22
|
+
* * save *
|
23
|
+
* :after_mediation
|
24
|
+
|
25
|
+
The after_mediation callback is itself broken down into three phases:
|
26
|
+
|
27
|
+
* :reconciliation - in this phase, any methods which bring the overall state
|
28
|
+
of the graph into balance should be run to adjust for changes made during the
|
29
|
+
save.
|
30
|
+
* :cacheing - any calculations which rely on the state of a reconciled graph
|
31
|
+
but which do not themselves alter the graph (in that they are reproducible
|
32
|
+
from existing state) should be made in the cacheing phase.
|
33
|
+
* :bumping - if the class has a +lock_column+ set
|
34
|
+
(ActiveRecord::Locking::Optimistic) and has on +updated_at/on+ timestamp then
|
35
|
+
the instance will be touched, bumping the +lock_column+ and checking for stale
|
36
|
+
data.
|
37
|
+
|
38
|
+
During a mediated_transaction, the +lock_column+ will only update during the
|
39
|
+
+bumping+ phase of the after_mediation callback.
|
40
|
+
|
41
|
+
But if there is no +update_at/on+ timestamp, then +lock_column+ cannot be
|
42
|
+
incremented when dependent objects are updated. This is because there is
|
43
|
+
nothing to touch on the root record to trigger the +lock_column+ update.
|
44
|
+
|
45
|
+
GraphMediator ensures that after_mediation is run only once within the context
|
46
|
+
of a mediated transaction. If the block being mediated returns false, the
|
47
|
+
after_mediation is skipped; this allows for validations.
|
48
|
+
|
49
|
+
== Usage
|
50
|
+
|
51
|
+
# * :pen_number
|
52
|
+
# * :dingo_count
|
53
|
+
# * :biscuit_count
|
54
|
+
# * :feed_rate
|
55
|
+
# * :total_biscuit_weight
|
56
|
+
# * :lock_version, :default => 0 # required for versioning
|
57
|
+
# * :updated_at # required for versioning
|
58
|
+
class DingoPen < ActiveRecord::Base
|
59
|
+
|
60
|
+
has_many :dingos
|
61
|
+
has_many :biscuits
|
62
|
+
|
63
|
+
include GraphMediator
|
64
|
+
mediate :purchase_biscuits,
|
65
|
+
:dependencies => [Dingo, Biscuit],
|
66
|
+
:when_reconciling => [:adjust_biscuit_supply, :feed_dingos],
|
67
|
+
:when_cacheing => :calculate_total_biscuit_weight
|
68
|
+
|
69
|
+
or
|
70
|
+
|
71
|
+
mediate :purchase_biscuits,
|
72
|
+
:dependencies => [Dingo, Biscuit], # ensures a mediated_transaction on Dingo#save or Biscuit#save
|
73
|
+
mediate_reconciles :adjust_biscuit_supply, :feed_dingos
|
74
|
+
mediate_caches do |instance|
|
75
|
+
instance.calculate_total_biscuit_weight
|
76
|
+
end
|
77
|
+
|
78
|
+
...
|
79
|
+
|
80
|
+
def purchase_biscuits; ... end
|
81
|
+
def adjust_biscuit_supply; ... end
|
82
|
+
def feed_dingos; ... end
|
83
|
+
def calculate_total_biscuit_weight; ... end
|
84
|
+
end
|
85
|
+
|
86
|
+
See spec/examples for real, dingo-free examples.
|
87
|
+
|
88
|
+
== Caveats
|
89
|
+
|
90
|
+
A lock_column and timestamp are not required, but without both columns in your schema
|
91
|
+
there will be no versioning.
|
92
|
+
|
93
|
+
+A lock_column by itself *without* a timestamp will not increment and will not provide
|
94
|
+
any optimistic locking in a class including GraphMediator!+
|
95
|
+
|
96
|
+
Using a lock_column along with a counter_cache in a dependent child will raise a StaleObject
|
97
|
+
error during a mediated_transaction if you touch the dependent.
|
98
|
+
|
99
|
+
The cache_counters do not play well with optimistic locking because they are updated with
|
100
|
+
a direct SQL call to the database, so ActiveRecord instance remain unaware of the lock_version
|
101
|
+
change and assume it came from another transaction.
|
102
|
+
|
103
|
+
You should not need to declare lock_version for any children that are declared as a dependency
|
104
|
+
of the root node, since updates will also update the root nodes lock_version. So if another
|
105
|
+
transaction updates a child, root.lock_version should increment, and the first transaction
|
106
|
+
should raise a StaleObject error when it too tries to update the child.
|
107
|
+
|
108
|
+
If you override super in the model hierarchy you are mediating, you must pass your
|
109
|
+
override as a block to super or it will occur outside of mediation:
|
110
|
+
|
111
|
+
def save
|
112
|
+
super do
|
113
|
+
my_local_changes
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
You are probably better off hooking to before_save or after_save if they
|
118
|
+
suffice.
|
119
|
+
|
120
|
+
== Threads
|
121
|
+
|
122
|
+
GraphMediator uses thread local variables to keep track of open mediators.
|
123
|
+
It should be thread safe but this needs testing.
|
124
|
+
|
125
|
+
== Advice
|
126
|
+
|
127
|
+
Build a simple system first, rather than building a system to use GraphMediator.
|
128
|
+
|
129
|
+
But if you have a web of observers/callbacks struggling to maintain state,
|
130
|
+
repeated, redundant update calls from observed changes in collection members,
|
131
|
+
or are running into +lock_column+ issues within your own updates, then
|
132
|
+
GraphMediator may help.
|
133
|
+
|
134
|
+
== Copyright
|
135
|
+
|
136
|
+
Copyright (c) 2010 Josh Partlow. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'rake'
|
6
|
+
require 'graph_mediator/version'
|
7
|
+
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
10
|
+
spec.libs << 'lib' << 'spec'
|
11
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
12
|
+
end
|
13
|
+
|
14
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
15
|
+
spec.libs << 'lib' << 'spec'
|
16
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
17
|
+
spec.rcov = true
|
18
|
+
end
|
19
|
+
|
20
|
+
task :default => :spec
|
21
|
+
|
22
|
+
require 'rake/rdoctask'
|
23
|
+
Rake::RDocTask.new do |rdoc|
|
24
|
+
version = GraphMediator::VERSION
|
25
|
+
|
26
|
+
rdoc.rdoc_dir = 'rdoc'
|
27
|
+
rdoc.title = "graph_mediator #{version}"
|
28
|
+
rdoc.main = 'README.rdoc'
|
29
|
+
rdoc.rdoc_files.include('README*')
|
30
|
+
rdoc.rdoc_files.include('LICENSE*')
|
31
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$:.unshift lib unless $:.include?(lib)
|
3
|
+
|
4
|
+
require 'graph_mediator/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{graph_mediator}
|
8
|
+
s.version = GraphMediator::VERSION
|
9
|
+
s.required_rubygems_version = ">= 1.3.6"
|
10
|
+
|
11
|
+
s.authors = ["Josh Partlow"]
|
12
|
+
s.email = %q{jpartlow@glatisant.org}
|
13
|
+
s.summary = %q{Mediates ActiveRecord state changes}
|
14
|
+
s.description = %q{Mediates state changes between a set of interdependent ActiveRecord objects.}
|
15
|
+
s.homepage = %q{http://github.com/jpartlow/graph_mediator}
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE",
|
18
|
+
"README.rdoc"
|
19
|
+
]
|
20
|
+
s.rdoc_options = ["--main=README.rdoc", "--charset=UTF-8"]
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
s.rubygems_version = %q{1.3.6}
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files spec/*`.split("\n")
|
25
|
+
|
26
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
27
|
+
s.add_runtime_dependency(%q<activerecord>, ["= 2.3.5"])
|
28
|
+
s.add_runtime_dependency(%q<activesupport>, ["= 2.3.5"])
|
29
|
+
s.add_runtime_dependency(%q<aasm>, [">= 2.2.0"])
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,509 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'graph_mediator/mediator'
|
3
|
+
require 'graph_mediator/locking'
|
4
|
+
require 'graph_mediator/version'
|
5
|
+
|
6
|
+
# = GraphMediator =
|
7
|
+
#
|
8
|
+
# GraphMediator is used to coordinate changes between a graph of ActiveRecord objects
|
9
|
+
# related to a root node. See README.rdoc for details.
|
10
|
+
#
|
11
|
+
# GraphMediator::Base::DSL - is the simple class macro language used to set up mediation.
|
12
|
+
#
|
13
|
+
# == Versioning and Optimistic Locking
|
14
|
+
#
|
15
|
+
# If you include an integer +lock_version+ column in your class, it will be incremented
|
16
|
+
# only once within a mediated_transaction and will serve as the optimistic locking check
|
17
|
+
# for the entire graph so long as you have declared all your dependent models for mediation.
|
18
|
+
#
|
19
|
+
# Outside of a mediated_transaction, +lock_version+ will increment per update as usual.
|
20
|
+
#
|
21
|
+
# == Convenience Methods for Save Without Mediation
|
22
|
+
#
|
23
|
+
# There are convenience method to perform a save, save!, toggle,
|
24
|
+
# toggle!, update_attribute, update_attributes or update_attributes!
|
25
|
+
# call without mediation. They are of the form <method>_without_mediation<punc>
|
26
|
+
#
|
27
|
+
# For example, save_without_mediation! is equivalent to:
|
28
|
+
#
|
29
|
+
# instance.disable_mediation!
|
30
|
+
# instance.save!
|
31
|
+
# instance.enable_mediation!
|
32
|
+
#
|
33
|
+
# == Overriding
|
34
|
+
#
|
35
|
+
# GraphMediator overrides ActiveRecord's save_without_transaction to slip in mediation
|
36
|
+
# just before the save process is wrapped in a transaction.
|
37
|
+
#
|
38
|
+
# * save_without_transaction
|
39
|
+
# * save_without_transaction_with_mediation
|
40
|
+
# * save_without_transaction_without_mediation
|
41
|
+
#
|
42
|
+
# may all be overridden in your implementation class, but they end up being
|
43
|
+
# defined locally by GraphMediator, so you can override with something like
|
44
|
+
# alias_method_chain, but will need to be in a subclass to use super.
|
45
|
+
#
|
46
|
+
# My original intention was to define aliased overrides in MediatorProxy if the target
|
47
|
+
# was a method in a superclass (like save), so that the implementation class could
|
48
|
+
# make a simple def foo; something; super; end override, but this is prevented by a bug
|
49
|
+
# in ruby 1.8 with aliasing of methods that use super in a module.
|
50
|
+
# http://redmine.ruby-lang.org/issues/show/734
|
51
|
+
#
|
52
|
+
module GraphMediator
|
53
|
+
|
54
|
+
CALLBACKS = [:before_mediation, :mediate_reconciles, :mediate_caches, :mediate_bumps]
|
55
|
+
SAVE_METHODS = [:save_without_transactions, :save_without_transactions!]
|
56
|
+
|
57
|
+
# We want lib/graph_mediator to define GraphMediator constant
|
58
|
+
require 'graph_mediator/mediator'
|
59
|
+
|
60
|
+
class MediatorException < Exception; end
|
61
|
+
|
62
|
+
# Methods used by GraphMediator to setup.
|
63
|
+
class << self
|
64
|
+
def included(base)
|
65
|
+
base.class_eval do
|
66
|
+
extend DSL
|
67
|
+
end
|
68
|
+
initialize_for_mediation(base)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def initialize_for_mediation(base)
|
74
|
+
_include_new_proxy(base)
|
75
|
+
base.class_inheritable_accessor :__graph_mediator_enabled, :instance_writer => false
|
76
|
+
base.__graph_mediator_enabled = true
|
77
|
+
base.__send__(:class_inheritable_array, :graph_mediator_dependencies)
|
78
|
+
base.graph_mediator_dependencies = []
|
79
|
+
base.__send__(:_register_for_mediation, *(SAVE_METHODS.clone << { :track_changes => true }))
|
80
|
+
end
|
81
|
+
|
82
|
+
# Inserts a new #{base}::MediatorProxy module with Proxy included.
|
83
|
+
# All callbacks are defined in here for easy overriding in the Base
|
84
|
+
# class.
|
85
|
+
def _include_new_proxy(base)
|
86
|
+
# XXX How can _include_new_proxy be made cleaner or at least clearer?
|
87
|
+
proxy = Module.new do
|
88
|
+
# include ActiveSupport::Callbacks
|
89
|
+
include Proxy
|
90
|
+
mattr_accessor :_graph_mediator_logger
|
91
|
+
mattr_accessor :_graph_mediator_log_level
|
92
|
+
end
|
93
|
+
base.const_set(:MediatorProxy, proxy)
|
94
|
+
proxy._graph_mediator_logger = GraphMediator::Configuration.logger || base.logger
|
95
|
+
proxy._graph_mediator_log_level = GraphMediator::Configuration.log_level
|
96
|
+
|
97
|
+
base.send(:include, proxy)
|
98
|
+
base.send(:extend, Proxy::ClassMethods)
|
99
|
+
base.send(:include, Locking)
|
100
|
+
|
101
|
+
key = base.to_s.underscore.gsub('/','_').upcase
|
102
|
+
hash_key = "GRAPH_MEDIATOR_#{key}_HASH_KEY"
|
103
|
+
new_array_key = "GRAPH_MEDIATOR_#{key}_NEW_ARRAY_KEY"
|
104
|
+
eigen = base.instance_eval { class << self; self; end }
|
105
|
+
eigen.class_eval do
|
106
|
+
define_method(:mediator_hash_key) { hash_key }
|
107
|
+
define_method(:mediator_new_array_key) { new_array_key }
|
108
|
+
end
|
109
|
+
|
110
|
+
# Relies on ActiveSupport::Callbacks (which is included
|
111
|
+
# into ActiveRecord::Base) for callback handling.
|
112
|
+
base.define_callbacks *CALLBACKS
|
113
|
+
return proxy
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
module Configuration
|
119
|
+
# Enable or disable mediation globally. Default: true
|
120
|
+
# TODO this doesn't effect anything yet
|
121
|
+
mattr_accessor :enable_mediation
|
122
|
+
self.enable_mediation = true
|
123
|
+
|
124
|
+
# Global logger override for GraphMediator. By default each class
|
125
|
+
# including GraphMediator uses the class's ActiveRecord logger. Setting
|
126
|
+
# GraphMediator::Configuration.logger overrides this.
|
127
|
+
mattr_accessor :logger
|
128
|
+
|
129
|
+
# Log level may be adjusted just for GraphMediator globally, or for each class including
|
130
|
+
# GraphMediator. This should be an ActiveSupport::BufferedLogger log level constant
|
131
|
+
# such as ActiveSupport::BufferedLogger::DEBUG
|
132
|
+
mattr_accessor :log_level
|
133
|
+
self.log_level = ActiveSupport::BufferedLogger::INFO
|
134
|
+
end
|
135
|
+
|
136
|
+
module Util
|
137
|
+
# Returns an array of [<method>,<punctuation>] from a given method symbol.
|
138
|
+
#
|
139
|
+
# parse_method_punctuation(:save) => ['save',nil]
|
140
|
+
# parse_method_punctuation(:save!) => ['save','!']
|
141
|
+
def parse_method_punctuation(method)
|
142
|
+
return method.to_s.sub(/([?!=])$/, ''), $1
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# All of the working methods for mediation, plus initial call backs.
|
147
|
+
module Proxy
|
148
|
+
extend Util
|
149
|
+
|
150
|
+
module ClassMethods
|
151
|
+
# Turn on mediation for all instances of this class. (On by default)
|
152
|
+
def enable_all_mediation!
|
153
|
+
self.__graph_mediator_enabled = true
|
154
|
+
end
|
155
|
+
|
156
|
+
# Turn off mediation for all instances of this class. (Off by default)
|
157
|
+
#
|
158
|
+
# This will cause new mediators to start up disabled, but existing
|
159
|
+
# mediators will finish normally.
|
160
|
+
def disable_all_mediation!
|
161
|
+
self.__graph_mediator_enabled = false
|
162
|
+
end
|
163
|
+
|
164
|
+
# True if mediation is enabled at the class level.
|
165
|
+
def mediation_enabled?
|
166
|
+
self.__graph_mediator_enabled
|
167
|
+
end
|
168
|
+
|
169
|
+
# True if we are currently mediating instances of any of the passed ids.
|
170
|
+
def currently_mediating?(ids)
|
171
|
+
Array(ids).detect do |id|
|
172
|
+
mediators[id] || mediators_for_new_records.find { |m| m.mediated_id == id }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Unique key to access a thread local hash of mediators for specific
|
177
|
+
# #{base}::MediatorProxy type.
|
178
|
+
#
|
179
|
+
# (This is overwritten by GraphMediator._include_new_proxy)
|
180
|
+
def mediator_hash_key; end
|
181
|
+
|
182
|
+
# Unique key to access a thread local array of mediators of new records for
|
183
|
+
# specific #{base}::MediatorProxy type.
|
184
|
+
#
|
185
|
+
# (This is overwritten by GraphMediator._include_new_proxy)
|
186
|
+
def mediator_new_array_key; end
|
187
|
+
|
188
|
+
# The hash of Mediator instances active in this Thread for the Proxy's
|
189
|
+
# base class.
|
190
|
+
#
|
191
|
+
# instance.id => Mediator of (instance)
|
192
|
+
#
|
193
|
+
def mediators
|
194
|
+
unless Thread.current[mediator_hash_key]
|
195
|
+
Thread.current[mediator_hash_key] = {}
|
196
|
+
end
|
197
|
+
Thread.current[mediator_hash_key]
|
198
|
+
end
|
199
|
+
|
200
|
+
# An array of Mediator instances mediating new records in this Thread for
|
201
|
+
# the Proxy's base class.
|
202
|
+
def mediators_for_new_records
|
203
|
+
unless Thread.current[mediator_new_array_key]
|
204
|
+
Thread.current[mediator_new_array_key] = []
|
205
|
+
end
|
206
|
+
Thread.current[mediator_new_array_key]
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
# Wraps the given block in a transaction and begins mediation.
|
212
|
+
def mediated_transaction(&block)
|
213
|
+
m_debug("#{self}.mediated_transaction called")
|
214
|
+
mediator = _get_mediator
|
215
|
+
result = mediator.mediate(&block)
|
216
|
+
m_debug("#{self}.mediated_transaction completed successfully")
|
217
|
+
return result
|
218
|
+
ensure
|
219
|
+
if mediator && mediator.idle?
|
220
|
+
mediators.delete(self.id)
|
221
|
+
mediators_for_new_records.delete(mediator)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# True if there is currently a mediated transaction begun for
|
226
|
+
# this instance.
|
227
|
+
def currently_mediating?
|
228
|
+
!current_mediator.nil?
|
229
|
+
end
|
230
|
+
|
231
|
+
# Returns the state of the current_mediator or nil.
|
232
|
+
def current_mediation_phase
|
233
|
+
current_mediator.try(:aasm_current_state)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns the hash of changes to the graph being tracked by the current
|
237
|
+
# mediator or nil if not currently mediating.
|
238
|
+
def mediated_changes
|
239
|
+
current_mediator.try(:changes)
|
240
|
+
end
|
241
|
+
|
242
|
+
# Turn off mediation for this instance. If currently mediating, it
|
243
|
+
# will finish normally, but new mediators will start disabled.
|
244
|
+
def disable_mediation!
|
245
|
+
@graph_mediator_mediation_disabled = true
|
246
|
+
end
|
247
|
+
|
248
|
+
# Turn on mediation for this instance (on by default).
|
249
|
+
def enable_mediation!
|
250
|
+
@graph_mediator_mediation_disabled = false
|
251
|
+
end
|
252
|
+
|
253
|
+
# By default, every instance will be mediated and this will return true.
|
254
|
+
# You can turn mediation on or off on an instance by instance basis with
|
255
|
+
# calls to disable_mediation! or enable_mediation!.
|
256
|
+
#
|
257
|
+
# Mediation may also be disabled at the class level, but enabling or
|
258
|
+
# disabling an instance supercedes this.
|
259
|
+
def mediation_enabled?
|
260
|
+
enabled = @graph_mediator_mediation_disabled.nil? ?
|
261
|
+
self.class.mediation_enabled? :
|
262
|
+
!@graph_mediator_mediation_disabled
|
263
|
+
end
|
264
|
+
|
265
|
+
%w(save save! touch toggle toggle! update_attribute update_attributes update_attributes!).each do |method|
|
266
|
+
base, punctuation = parse_method_punctuation(method)
|
267
|
+
define_method("#{base}_without_mediation#{punctuation}") do |*args,&block|
|
268
|
+
disable_mediation!
|
269
|
+
send(method, *args, &block)
|
270
|
+
enable_mediation!
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
[:debug, :info, :warn, :error, :fatal].each do |level|
|
275
|
+
const = ActiveSupport::BufferedLogger.const_get(level.to_s.upcase)
|
276
|
+
define_method("m_#{level}") do |message|
|
277
|
+
_graph_mediator_logger.send(level, message) if _graph_mediator_log_level <= const
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
protected
|
282
|
+
|
283
|
+
def mediators
|
284
|
+
self.class.mediators
|
285
|
+
end
|
286
|
+
|
287
|
+
def mediators_for_new_records
|
288
|
+
self.class.mediators_for_new_records
|
289
|
+
end
|
290
|
+
|
291
|
+
# Accessor for the mediator associated with this instance's id, or nil if we are
|
292
|
+
# not currently mediating.
|
293
|
+
def current_mediator
|
294
|
+
m_debug("#{self}.current_mediator called")
|
295
|
+
mediator = mediators[self.id]
|
296
|
+
mediator ||= mediators_for_new_records.find { |m| m.mediated_instance.equal?(self) || m.mediated_id == self.id }
|
297
|
+
m_debug("#{self}.current_mediator found #{mediator || 'nothing'}")
|
298
|
+
return mediator
|
299
|
+
end
|
300
|
+
|
301
|
+
private
|
302
|
+
|
303
|
+
# Gets the current mediator or initializes a new one.
|
304
|
+
def _get_mediator
|
305
|
+
m_debug("#{self}._get_mediator called")
|
306
|
+
m_debug("#{self}.get_mediator in a new record") if new_record?
|
307
|
+
unless mediator = current_mediator
|
308
|
+
mediator = GraphMediator::Mediator.new(self)
|
309
|
+
m_debug("#{self}.get_mediator created new mediator")
|
310
|
+
new_record? ?
|
311
|
+
mediators_for_new_records << mediator :
|
312
|
+
mediators[self.id] = mediator
|
313
|
+
end
|
314
|
+
m_debug("#{self}._get_mediator obtained #{mediator}")
|
315
|
+
return mediator
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
module AliasExtension #:nodoc:
|
321
|
+
include Util
|
322
|
+
|
323
|
+
private
|
324
|
+
|
325
|
+
# Wraps each method in a mediated_transaction call.
|
326
|
+
# The original method is aliased as :method_without_mediation so that it can be
|
327
|
+
# overridden separately if needed.
|
328
|
+
#
|
329
|
+
# * options:
|
330
|
+
# * :through => root node accessor that will be the target of the
|
331
|
+
# mediated_transaction. By default self is assumed.
|
332
|
+
# * :track_changes => if true, the mediator will track changes such
|
333
|
+
# that they can be reviewed after_mediation. The after_mediation
|
334
|
+
# callbacks occur after dirty has completed and changes are normally lost.
|
335
|
+
# False by default. Normally only applied to save and destroy methods.
|
336
|
+
def _register_for_mediation(*methods)
|
337
|
+
options = methods.extract_options!
|
338
|
+
root_node_accessor = options[:through]
|
339
|
+
track_changes = options[:track_changes]
|
340
|
+
methods.each do |method|
|
341
|
+
saveing = method.to_s =~ /save/
|
342
|
+
destroying = method.to_s =~ /destroy/
|
343
|
+
_alias_method_chain_ensuring_inheritability(method, :mediation) do |aliased_target,punctuation|
|
344
|
+
__send__(:define_method, "#{aliased_target}_with_mediation#{punctuation}") do |*args, &block|
|
345
|
+
root_node = (root_node_accessor ? send(root_node_accessor) : self)
|
346
|
+
unless root_node.nil?
|
347
|
+
root_node.mediated_transaction do |mediator|
|
348
|
+
mediator.debug("#{root_node} mediating #{aliased_target}#{punctuation} for #{self}")
|
349
|
+
mediator.track_changes_for(self) if track_changes && saveing
|
350
|
+
result = __send__("#{aliased_target}_without_mediation#{punctuation}", *args, &block)
|
351
|
+
mediator.track_changes_for(self) if track_changes && destroying
|
352
|
+
mediator.debug("#{root_node} done mediating #{aliased_target}#{punctuation} for #{self}")
|
353
|
+
result
|
354
|
+
end
|
355
|
+
else
|
356
|
+
__send__("#{aliased_target}_without_mediation#{punctuation}", *args, &block)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def _method_defined(method, anywhere = true)
|
364
|
+
(instance_methods(anywhere) + private_instance_methods(anywhere)).include?(RUBY_VERSION < '1.9' ? method.to_s : method)
|
365
|
+
end
|
366
|
+
|
367
|
+
# This uses Tammo Freese's patch to alias_method_chain.
|
368
|
+
# https://rails.lighthouseapp.com/projects/8994/tickets/285-alias_method_chain-limits-extensibility
|
369
|
+
#
|
370
|
+
# target, target_with_mediation, target_without_mediation should all be
|
371
|
+
# available for decorating (via aliasing) in the base class including the
|
372
|
+
# MediatorProxy, as well as in it's subclasses (via aliasing or direct
|
373
|
+
# overriding). Overrides made higher up the chain should flow through as
|
374
|
+
# well
|
375
|
+
#
|
376
|
+
# If the target has not been defined yet, there's nothing we can do, and we
|
377
|
+
# raise a MediatorException
|
378
|
+
def _alias_method_chain_ensuring_inheritability(target, feature, &block)
|
379
|
+
raise(MediatorException, "Method #{target} has not been defined yet.") unless _method_defined(target)
|
380
|
+
|
381
|
+
# Strip out punctuation on predicates or bang methods since
|
382
|
+
# e.g. target?_without_feature is not a valid method name.
|
383
|
+
aliased_target, punctuation = parse_method_punctuation(target)
|
384
|
+
with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
|
385
|
+
|
386
|
+
method_defined_here = _method_defined(target, false)
|
387
|
+
unless method_defined_here
|
388
|
+
module_eval do
|
389
|
+
define_method(target) do |*args, &block|
|
390
|
+
super
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
__send__(:alias_method, without_method, target)
|
396
|
+
|
397
|
+
if block_given?
|
398
|
+
# create with_method
|
399
|
+
yield(aliased_target, punctuation)
|
400
|
+
end
|
401
|
+
|
402
|
+
target_method_exists = _method_defined(with_method)
|
403
|
+
raise NameError unless target_method_exists
|
404
|
+
|
405
|
+
module_eval do
|
406
|
+
define_method(target) do |*args, &block|
|
407
|
+
__send__(with_method, *args, &block)
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
end
|
413
|
+
|
414
|
+
# DSL for setting up and describing mediation.
|
415
|
+
#
|
416
|
+
# save and save! are automatically wrapped for mediation when GraphMediator
|
417
|
+
# is included into your class. You can mediate other methods with a call to
|
418
|
+
# mediate(), and can setup callbacks for reconcilation, cacheing or version
|
419
|
+
# bumping.
|
420
|
+
#
|
421
|
+
# = Callbacks
|
422
|
+
#
|
423
|
+
# The mediate() method takes options to set callbacks. Or you can set them
|
424
|
+
# directly with a method symbol, array of method symbols or a Proc. They may
|
425
|
+
# be called multiple times and may be added to in subclasses.
|
426
|
+
#
|
427
|
+
# * before_mediation - runs before mediation is begun
|
428
|
+
# * - mediate and save
|
429
|
+
# * mediate_reconciles - after saveing the instance, run any routines to make further
|
430
|
+
# adjustments to the structure of the graph or non-cache attributes
|
431
|
+
# * mediate_caches - routines for updating cache values
|
432
|
+
#
|
433
|
+
# Example:
|
434
|
+
#
|
435
|
+
# mediate_reconciles :bar do |instance|
|
436
|
+
# instance.something_else
|
437
|
+
# end
|
438
|
+
# mediate_reconciles :baz
|
439
|
+
#
|
440
|
+
# will ensure that [:bar, <block>, :baz] are run in
|
441
|
+
# sequence after :foo is done saveing within the context of a mediated
|
442
|
+
# transaction.
|
443
|
+
#
|
444
|
+
module DSL
|
445
|
+
include AliasExtension
|
446
|
+
|
447
|
+
# Establishes callbacks, dependencies and possible methods as entry points
|
448
|
+
# for mediation.
|
449
|
+
#
|
450
|
+
# * :methods => list of methods to mediate (automatically wrap in a
|
451
|
+
# mediated_transaction call)
|
452
|
+
#
|
453
|
+
# ActiveRecord::Base.save is decorated for mediation when GraphMediator
|
454
|
+
# is included into your model. If you have additional methods which
|
455
|
+
# perform bulk operations on members, you probably want to list them
|
456
|
+
# here so that they are mediated as well.
|
457
|
+
#
|
458
|
+
# You should not list methods used for reconcilation, or cacheing.
|
459
|
+
#
|
460
|
+
# This macro takes a number of options:
|
461
|
+
#
|
462
|
+
# * :options => hash of options
|
463
|
+
# * :dependencies => list of dependent member classes whose save methods
|
464
|
+
# should be decorated for mediation as well.
|
465
|
+
# * :when_reconciling => list of methods to execute during the after_mediation
|
466
|
+
# reconcilation phase
|
467
|
+
# * :when_cacheing => list of methods to execute during the after_mediation
|
468
|
+
# cacheing phase
|
469
|
+
#
|
470
|
+
# mediate :update_children,
|
471
|
+
# :dependencies => Child,
|
472
|
+
# :when_reconciling => :reconcile,
|
473
|
+
# :when_caching => :cache
|
474
|
+
#
|
475
|
+
# = Dependent Classes
|
476
|
+
#
|
477
|
+
# Dependent classes have their save methods mediated as well. However, a
|
478
|
+
# dependent class must provide an accessor for the root node, so that a
|
479
|
+
# mediated_transaction can be begun in the root node when a dependent is
|
480
|
+
# changed.
|
481
|
+
#
|
482
|
+
# = Versioning and Optimistic Locking
|
483
|
+
#
|
484
|
+
# GraphMediator uses the class's lock_column (default +lock_version+) and
|
485
|
+
# +updated_at+ or +updated_on+ for versioning and locks checks during
|
486
|
+
# mediation. The lock_column is incremented only once during a mediated_transaction.
|
487
|
+
#
|
488
|
+
# +Unless both these columns are present in the schema, versioning/locking
|
489
|
+
# will not happen.+ A lock_column by itself will not be updated unless
|
490
|
+
# there is an updated_at/on timestamp available to touch.
|
491
|
+
#
|
492
|
+
def mediate(*methods)
|
493
|
+
options = methods.extract_options!
|
494
|
+
self.graph_mediator_dependencies = Array(options[:dependencies] || [])
|
495
|
+
|
496
|
+
_register_for_mediation(*methods)
|
497
|
+
graph_mediator_dependencies.each do |dependent_class|
|
498
|
+
dependent_class.send(:extend, AliasExtension) unless dependent_class.include?(AliasExtension)
|
499
|
+
methods = SAVE_METHODS.clone
|
500
|
+
methods << :destroy
|
501
|
+
methods << { :through => self.class_of_active_record_descendant(self).to_s.demodulize.underscore, :track_changes => true }
|
502
|
+
dependent_class.send(:_register_for_mediation, *methods)
|
503
|
+
end
|
504
|
+
mediate_reconciles(options[:when_reconciling]) if options[:when_reconciling]
|
505
|
+
mediate_caches(options[:when_cacheing]) if options[:when_cacheing]
|
506
|
+
end
|
507
|
+
|
508
|
+
end
|
509
|
+
end
|