graph_mediator 0.2.1
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/.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
|