1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
#!/usr/local/bin/ruby -w
# $Id$
# the class that actually walks our object/state tree, collects the changes,
# and performs them
# there are two directions of walking:
# - first we recurse down the tree and collect changes
# - then we walk back up the tree through 'refresh' after the changes
require 'puppet'
require 'puppet/statechange'
#---------------------------------------------------------------
module Puppet
class Transaction
attr_accessor :toplevel, :component
#---------------------------------------------------------------
# a bit of a gross hack; a global list of objects that have failed to sync,
# so that we can verify during later syncs that our dependencies haven't
# failed
def Transaction.init
@@failures = Hash.new(0)
Puppet::Metric.init
@@changed = []
end
#---------------------------------------------------------------
#---------------------------------------------------------------
# for now, just store the changes for executing linearly
# later, we might execute them as we receive them
def change(change)
@changes.push change
end
#---------------------------------------------------------------
#---------------------------------------------------------------
# okay, here's the deal:
# a given transaction maps directly to a component, and each transaction
# will only ever receive changes from its respective component
# so, when looking for subscribers, we need to first see if the object
# that actually changed had any direct subscribers
# then, we need to pass the event to the object's containing component,
# to see if it or any of its parents have subscriptions on the event
def evaluate
Puppet.debug "executing %s changes " % @changes.length
events = @changes.collect { |change|
if change.is_a?(Puppet::StateChange)
change.transaction = self
events = nil
begin
# use an array, so that changes can return more than one
# event if they want
events = [change.forward].flatten.reject { |e| e.nil? }
#@@changed.push change.state.parent
rescue => detail
Puppet.err("%s failed: %s" % [change,detail])
if Puppet[:debug] and detail.respond_to?(:stack)
puts detail.stack
end
next
# FIXME this should support using onerror to determine
# behaviour; or more likely, the client calling us
# should do so
end
unless events.nil? or (events.is_a?(Array) and events.empty?)
change.changed = true
end
events
elsif change.is_a?(Puppet::Transaction)
raise Puppet::DevError, "Got a sub-transaction"
change.evaluate
else
raise Puppet::DevError,
"Transactions cannot handle objects of type %s" % child.class
end
}.flatten.reject { |event|
event.nil?
}
#@triggerevents = []
events.each { |event|
object = event.source
object.propagate(event)
}
#events += @triggerevents
end
#---------------------------------------------------------------
#---------------------------------------------------------------
# this should only be called by a Puppet::Container object now
# and it should only receive an array
def initialize(objects)
@objects = objects
@toplevel = false
@triggered = Hash.new { |hash, key|
hash[key] = Hash.new(0)
}
# of course, this won't work on the second run
unless defined? @@failures
@toplevel = true
self.class.init
end
# change collection is in-band, and message generation is out-of-band
# of course, exception raising is also out-of-band
@changes = @objects.collect { |child|
# these children are all Puppet::Type instances
# not all of the children will return a change, and Containers
# return transactions
child.evaluate
}.flatten.reject { |child|
child.nil? # remove empties
}
end
#---------------------------------------------------------------
#---------------------------------------------------------------
def rollback
events = @changes.reverse.collect { |change|
if change.is_a?(Puppet::StateChange)
# skip changes that were never actually run
unless change.changed
Puppet.debug "%s was not changed" % change.to_s
next
end
#change.transaction = self
begin
change.backward
#@@changed.push change.state.parent
rescue => detail
Puppet.err("%s rollback failed: %s" % [change,detail])
next
# at this point, we would normally do error handling
# but i haven't decided what to do for that yet
# so just record that a sync failed for a given object
#@@failures[change.state.parent] += 1
# this still could get hairy; what if file contents changed,
# but a chmod failed? how would i handle that error? dern
end
elsif change.is_a?(Puppet::Transaction)
raise Puppet::DevError, "Got a sub-transaction"
# yay, recursion
change.rollback
else
raise Puppe::DevError,
"Transactions cannot handle objects of type %s" % child.class
end
}.flatten.reject { |e| e.nil? }
#@triggerevents = []
events.each { |event|
object = event.source
object.propagate(event)
}
#events += @triggerevents
end
#---------------------------------------------------------------
#---------------------------------------------------------------
def triggered(object, method)
@triggered[object][method] += 1
#@triggerevents << ("%s_%sed" % [object.class.name.to_s, method.to_s]).intern
end
#---------------------------------------------------------------
#---------------------------------------------------------------
def triggered?(object, method)
@triggered[object][method]
end
#---------------------------------------------------------------
end
end
#---------------------------------------------------------------
|