tengine_job 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile.lock +78 -48
  3. data/bin/tengine_job +71 -0
  4. data/examples/0004_retry_one_layer.rb +10 -7
  5. data/examples/0027_parallel_ssh_job +9 -0
  6. data/examples/0027_parallel_ssh_jobs.rb +14 -0
  7. data/lib/tengine/job.rb +19 -49
  8. data/lib/tengine/job/dsl.rb +13 -0
  9. data/lib/tengine/job/{dsl_binder.rb → dsl/binder.rb} +4 -4
  10. data/lib/tengine/job/{dsl_evaluator.rb → dsl/evaluator.rb} +2 -2
  11. data/lib/tengine/job/{dsl_loader.rb → dsl/loader.rb} +20 -22
  12. data/lib/tengine/job/runtime.rb +32 -0
  13. data/lib/tengine/job/{drivers → runtime/drivers}/job_control_driver.rb +46 -92
  14. data/lib/tengine/job/{drivers → runtime/drivers}/job_execution_driver.rb +14 -10
  15. data/lib/tengine/job/runtime/drivers/jobnet_control_driver.rb +240 -0
  16. data/lib/tengine/job/{drivers → runtime/drivers}/schedule_driver.rb +4 -4
  17. data/lib/tengine/job/{edge.rb → runtime/edge.rb} +79 -25
  18. data/lib/tengine/job/{executable.rb → runtime/executable.rb} +35 -15
  19. data/lib/tengine/job/{execution.rb → runtime/execution.rb} +19 -11
  20. data/lib/tengine/job/runtime/job_base.rb +5 -0
  21. data/lib/tengine/job/runtime/jobnet.rb +283 -0
  22. data/lib/tengine/job/runtime/junction.rb +44 -0
  23. data/lib/tengine/job/runtime/named_vertex.rb +95 -0
  24. data/lib/tengine/job/runtime/root_jobnet.rb +81 -0
  25. data/lib/tengine/job/{signal.rb → runtime/signal.rb} +99 -13
  26. data/lib/tengine/job/runtime/ssh_job.rb +486 -0
  27. data/lib/tengine/job/{jobnet → runtime}/state_transition.rb +6 -4
  28. data/lib/tengine/job/runtime/stoppable.rb +64 -0
  29. data/lib/tengine/job/runtime/vertex.rb +50 -0
  30. data/lib/tengine/job/structure.rb +20 -0
  31. data/lib/tengine/job/{category.rb → structure/category.rb} +9 -5
  32. data/lib/tengine/job/{jobnet/builder.rb → structure/edge_builder.rb} +11 -7
  33. data/lib/tengine/job/{element_selector_notation.rb → structure/element_selector_notation.rb} +15 -11
  34. data/lib/tengine/job/structure/jobnet_builder.rb +83 -0
  35. data/lib/tengine/job/structure/jobnet_finder.rb +60 -0
  36. data/lib/tengine/job/{name_path.rb → structure/name_path.rb} +2 -2
  37. data/lib/tengine/job/structure/tree.rb +20 -0
  38. data/lib/tengine/job/structure/visitor.rb +67 -0
  39. data/lib/tengine/job/template.rb +24 -0
  40. data/lib/tengine/job/template/edge.rb +37 -0
  41. data/lib/tengine/job/template/expansion.rb +24 -0
  42. data/lib/tengine/job/template/generator.rb +111 -0
  43. data/lib/tengine/job/template/jobnet.rb +83 -0
  44. data/lib/tengine/job/template/junction.rb +14 -0
  45. data/lib/tengine/job/{job.rb → template/named_vertex.rb} +3 -5
  46. data/lib/tengine/job/{root_jobnet_template.rb → template/root_jobnet.rb} +12 -26
  47. data/lib/tengine/job/template/ssh_job.rb +80 -0
  48. data/lib/tengine/job/template/vertex.rb +97 -0
  49. metadata +127 -93
  50. data/lib/tengine/job/connectable.rb +0 -43
  51. data/lib/tengine/job/drivers/jobnet_control_driver.rb +0 -249
  52. data/lib/tengine/job/end.rb +0 -32
  53. data/lib/tengine/job/expansion.rb +0 -37
  54. data/lib/tengine/job/fork.rb +0 -6
  55. data/lib/tengine/job/jobnet.rb +0 -184
  56. data/lib/tengine/job/jobnet/job_state_transition.rb +0 -167
  57. data/lib/tengine/job/jobnet/jobnet_state_transition.rb +0 -110
  58. data/lib/tengine/job/jobnet_actual.rb +0 -84
  59. data/lib/tengine/job/jobnet_template.rb +0 -10
  60. data/lib/tengine/job/join.rb +0 -6
  61. data/lib/tengine/job/junction.rb +0 -29
  62. data/lib/tengine/job/killing.rb +0 -30
  63. data/lib/tengine/job/mm_compatibility.rb +0 -6
  64. data/lib/tengine/job/mm_compatibility/connectable.rb +0 -13
  65. data/lib/tengine/job/root.rb +0 -16
  66. data/lib/tengine/job/root_jobnet_actual.rb +0 -58
  67. data/lib/tengine/job/script_executable.rb +0 -235
  68. data/lib/tengine/job/start.rb +0 -20
  69. data/lib/tengine/job/stoppable.rb +0 -15
  70. data/lib/tengine/job/vertex.rb +0 -181
@@ -1,13 +0,0 @@
1
- module Tengine::Job::MmCompatibility::Connectable
2
- extend ActiveSupport::Concern
3
-
4
- included do
5
- alias_method :vm_instance_name , :server_name
6
- alias_method :vm_instance_name=, :server_name=
7
- alias_method :instance_name , :server_name
8
- alias_method :instance_name=, :server_name=
9
- alias_method :user_group_credential_name , :credential_name
10
- alias_method :user_group_credential_name=, :credential_name=
11
- end
12
-
13
- end
@@ -1,16 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'tengine/job'
3
-
4
- # ルートジョブネットとして必要な情報に関するモジュール
5
- module Tengine::Job::Root
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- include Tengine::Core::OptimisticLock
10
- set_locking_field :version
11
-
12
- belongs_to :category, :inverse_of => :root_jobnet_templates, :index => true, :class_name => "Tengine::Job::Category"
13
-
14
- field :version, :type => Integer, :default => 0 # ジョブネット全体を更新する際の楽観的ロックのためのバージョン。更新するたびにインクリメントされます。
15
- end
16
- end
@@ -1,58 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'tengine/job'
3
-
4
- # 実行時のルートジョブネットを表すVertex
5
- class Tengine::Job::RootJobnetActual < Tengine::Job::JobnetActual
6
- include Tengine::Job::Root
7
-
8
- has_many :executions, :inverse_of => :root_jobnet, :class_name => "Tengine::Job::Execution"
9
-
10
-
11
- def rerun(*args)
12
- options = args.extract_options!
13
- sender = options.delete(:sender) || Tengine::Event.default_sender
14
- options = options.merge({
15
- :retry => true,
16
- :root_jobnet_id => self.id,
17
- })
18
- result = Tengine::Job::Execution.new(options)
19
- result.target_actual_ids ||= []
20
- result.target_actual_ids += args.flatten
21
- result.with(safe: safemode(Tengine::Job::Execution.collection)).save!
22
- sender.wait_for_connection do
23
- sender.fire(:'start.execution.job.tengine', :properties => {
24
- :execution_id => result.id.to_s
25
- })
26
- end
27
- result
28
- end
29
-
30
- def update_with_lock(*args)
31
- super(*args) do
32
- Tengine::Job.test_harness_hook("before yield in update_with_lock")
33
- yield if block_given?
34
- Tengine::Job.test_harness_hook("after yield in update_with_lock")
35
- end
36
- Tengine::Job.test_harness_hook("after update_with_lock")
37
- end
38
-
39
- def fire_stop_event(options = Hash.new)
40
- root_jobnet_id = self.id.to_s
41
- result = Tengine::Job::Execution.create!(
42
- options.merge(:root_jobnet_id => root_jobnet_id))
43
-
44
- EM.run do
45
- Tengine::Event.fire(:"stop.jobnet.job.tengine",
46
- :source_name => name_as_resource,
47
- :properties => {
48
- :execution_id => result.id.to_s,
49
- :root_jobnet_id => root_jobnet_id,
50
- :target_jobnet_id => root_jobnet_id.to_s,
51
- :stop_reason => "user_stop",
52
- })
53
- end
54
-
55
- return result
56
- end
57
-
58
- end
@@ -1,235 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'tengine/job'
3
- require 'tengine/resource/net_ssh'
4
-
5
- # ジョブとして実際にスクリプトを実行する処理をまとめるモジュール。
6
- # Tengine::Job::JobnetActualと、Tengine::Job::ScriptActualがincludeします
7
- module Tengine::Job::ScriptExecutable
8
- extend ActiveSupport::Concern
9
-
10
- class Error < StandardError
11
- end
12
-
13
- included do
14
- include Tengine::Core::CollectionAccessible
15
-
16
- field :executing_pid, :type => String # 実行しているプロセスのPID
17
- field :exit_status , :type => String # 終了したプロセスが返した終了ステータス
18
- field :error_messages, :type => Array # エラーになった場合のメッセージを保持する配列。再実行時に追加される場合は末尾に追加されます。
19
- array_text_accessor :error_messages, :delimeter => "\n"
20
- end
21
-
22
- def run(execution)
23
- return ack(@acked_pid) if @acked_pid
24
- cmd = build_command(execution)
25
- # puts "cmd:\n" << cmd
26
- execute(cmd) do |ch, data|
27
- if signal = execution.signal
28
- # signal.data = {:executing_pid => data.strip}
29
- # ack(signal)
30
- pid = data.strip
31
- signal.callback = lambda do
32
- signal.data = {:executing_pid => pid}
33
-
34
- # このブロック内の処理はupdate_with_lockによって複数回実行されることがあります。
35
- # 1回目と同じリロードされていないオブジェクトを2回目以降に使用すると、1回目の変更が残っているので
36
- # そのオブジェクトに対して処理を行うのはNGです。
37
- # self.ack(signal) # これはNG
38
-
39
- # このブロックが実行されるたびに、rootからselfと同じidのオブジェクトを新たに取得する必要があります。
40
- job = root.vertex(self.id)
41
- job.ack(signal)
42
- end
43
- end
44
- end
45
- end
46
-
47
- def execute(cmd)
48
- raise "actual_server not found for #{self.name_path.inspect}" unless actual_server
49
- Tengine.logger.info("connecting to #{actual_server.hostname_or_ipv4}")
50
- port = actual_server.properties["ssh_port"] || 22
51
- keys_only = actual_credential.auth_type_cd == :ssh_public_key
52
- Net::SSH.start(actual_server.hostname_or_ipv4, actual_credential, :port => port, :logger => Tengine.logger, :keys_only => keys_only) do |ssh|
53
- # see http://net-ssh.github.com/ssh/v2/api/classes/Net/SSH/Connection/Channel.html
54
- ssh.open_channel do |channel|
55
- Tengine.logger.info("now exec on ssh: " << cmd)
56
- channel.exec(cmd.force_encoding("binary")) do |ch, success|
57
- raise Error, "could not execute command" unless success
58
-
59
- channel.on_close do |ch|
60
- # puts "channel is closing!"
61
- end
62
-
63
- channel.on_data do |ch, data|
64
- Tengine.logger.debug("got stdout: #{data}")
65
- yield(ch, data) if block_given?
66
- end
67
-
68
- channel.on_extended_data do |ch, type, data|
69
- self.error_messages ||= []
70
- self.error_messages += [data]
71
- raise Error, "Failure to execute #{self.name_path} via SSH: #{data}"
72
- end
73
- end
74
- end
75
-
76
- end
77
- rescue Tengine::Job::ScriptExecutable::Error
78
- raise
79
- rescue Mongoid::Errors::DocumentNotFound, SocketError, Net::SSH::AuthenticationFailed => src
80
- error = Error.new("[#{src.class.name}] #{src.message}")
81
- error.set_backtrace(src.backtrace)
82
- raise error
83
- rescue Exception
84
- # puts "[#{$!.class.name}] #{$!.message}"
85
- raise
86
- end
87
-
88
- def kill(execution)
89
- lines = source_profiles
90
-
91
- if self.executing_pid.blank?
92
- Tengine.logger.warn("PID is blank when kill!!\n#{self.inspect}\n " << caller.join("\n "))
93
- end
94
-
95
- cmd = executable_command("tengine_job_agent_kill %s %d %s" % [
96
- self.executing_pid,
97
- self.actual_killing_signal_interval,
98
- self.actual_killing_signals.join(","),
99
- ])
100
- lines << cmd
101
- cmd = lines.join(' && ')
102
- execute(cmd)
103
- end
104
-
105
- # def ack(pid)
106
- # @acked_pid = pid
107
- # self.executing_pid = pid
108
- # self.phase_key = :running
109
- # self.previous_edges.each{|edge| edge.status_key = :transmitted}
110
- # end
111
-
112
- def build_command(execution)
113
- result = source_profiles
114
- mm_env = build_mm_env(execution).map{|k,v| "#{k}=#{v}"}.join(" ")
115
- # Hadoopジョブの場合は環境変数をセットする
116
- if is_a?(Tengine::Job::Jobnet) && (jobnet_type_key == :hadoop_job_run)
117
- mm_env << ' ' << hadoop_job_env
118
- end
119
- result << "export #{mm_env}"
120
- template_root = root_or_expansion.template
121
- if template_root
122
- template_job = template_root.vertex_by_name_path(self.name_path_until_expansion)
123
- unless template_job
124
- raise "job not found #{self.name_path_until_expansion.inspect} in #{template_root.inspect}"
125
- end
126
- key = Tengine::Job::DslLoader.template_block_store_key(template_job, :preparation)
127
- preparation_block = Tengine::Job::DslLoader.template_block_store[key]
128
- if preparation_block
129
- preparation = instance_eval(&preparation_block)
130
- unless preparation.blank?
131
- result << preparation
132
- end
133
- end
134
- end
135
- unless execution.preparation_command.blank?
136
- result << execution.preparation_command
137
- end
138
- # cmdはユーザーが設定したスクリプトを組み立てたもので、
139
- # プロセスの監視/強制停止のためにtengine_job_agent/bin/tengine_job_agent_run
140
- # からこれらを実行させるためにはcmdを編集します。
141
- # tengine_job_agent_runは、標準出力に監視対象となる起動したプロセスのPIDを出力します。
142
- runner_path = ENV["MM_RUNNER_PATH"] || executable_command("tengine_job_agent_run")
143
- runner_option = ""
144
- # 実装するべきか要検討
145
- # runner_option << " --stdout" if execution.keeping_stdout
146
- # runner_option << " --stderr" if execution.keeping_stderr
147
- # script = "#{runner_path}#{runner_option} -- #{self.script}" # runnerのオプションを指定する際は -- の前に設定してください
148
- script = "#{runner_path}#{runner_option} #{self.script}" # runnerのオプションを指定する際は -- の前に設定してください
149
- result << script
150
- result.join(" && ")
151
- end
152
-
153
- def source_profiles
154
- # RubyのNet::SSHでは設定ファイルが読み込まれないので、ロードするようにします。
155
- # ~/.bash_profile, ~/.bashrc などは非対応。
156
- # ファイルが存在していたらsourceで読み込むようにしたいのですが、一旦保留します。
157
- # http://www.syns.net/10/
158
- ["/etc/profile", "/etc/bashrc", "$HOME/.bashrc", "$HOME/.bash_profile"].map do |path|
159
- "if [ -f #{path} ]; then source #{path}; fi"
160
- end
161
- end
162
-
163
- def executable_command(command)
164
- if prefix = ENV["MM_CMD_PREFIX"]
165
- "#{prefix} #{command}"
166
- else
167
- command
168
- end
169
- end
170
-
171
- # MMから実行されるシェルスクリプトに渡す環境変数のHashを返します。
172
- # MM_ACTUAL_JOB_ID : 実行される末端のジョブのMM上でのID
173
- # MM_ACTUAL_JOB_ANCESTOR_IDS : 実行される末端のジョブの祖先のMM上でのIDをセミコロンで繋げた文字列 (テンプレートジョブ単位)
174
- # MM_FULL_ACTUAL_JOB_ANCESTOR_IDS : 実行される末端のジョブの祖先のMM上でのIDをセミコロンで繋げた文字列 (expansionから展開した単位)
175
- # MM_ACTUAL_JOB_NAME_PATH : 実行される末端のジョブのname_path
176
- # MM_ACTUAL_JOB_SECURITY_TOKEN : 公開API呼び出しのためのセキュリティ用のワンタイムトークン
177
- # MM_TEMPLATE_JOB_ID : テンプレートジョブ(=実行される末端のジョブの元となったジョブ)のID
178
- # MM_TEMPLATE_JOB_ANCESTOR_IDS : テンプレートジョブの祖先のMM上でのIDをセミコロンで繋げたもの
179
- # MM_SCHEDULE_ID : 実行スケジュールのID
180
- # MM_SCHEDULE_ESTIMATED_TIME : 実行スケジュールの見積り時間。単位は分。
181
- # MM_SCHEDULE_ESTIMATED_END : 実行スケジュールの見積り終了時刻をYYYYMMDDHHMMSS式で。(できればISO 8601など、タイムゾーンも表現できる標準的な形式の方が良い?)
182
- # MM_MASTER_SCHEDULE_ID : マスタースケジュールがあればそのID。マスタースケジュールがない場合は環境変数は指定されません。
183
- #
184
- # 未実装
185
- # MM_FAILED_JOB_ID : ジョブが失敗した場合にrecoverやfinally内のジョブを実行時に設定される、失敗したジョブのMM上でのID。
186
- # MM_FAILED_JOB_ANCESTOR_IDS : ジョブが失敗した場合にrecoverやfinally内のジョブを実行時に設定される、失敗したジョブの祖先のMM上でのIDをセミコロンで繋げた文字列。
187
- def build_mm_env(execution)
188
- result = {
189
- "MM_SERVER_NAME" => actual_server_name, # [Tengineの仕様として追加] ジョブの実行サーバ名を設定
190
- "MM_ROOT_JOBNET_ID" => root.id.to_s,
191
- "MM_TARGET_JOBNET_ID" => parent.id.to_s,
192
- "MM_ACTUAL_JOB_ID" => id.to_s,
193
- "MM_ACTUAL_JOB_ANCESTOR_IDS" => '"%s"' % ancestors_until_expansion.map(&:id).map(&:to_s).join(';'),
194
- "MM_FULL_ACTUAL_JOB_ANCESTOR_IDS" => '"%s"' % ancestors.map(&:id).map(&:to_s).join(';'),
195
- "MM_ACTUAL_JOB_NAME_PATH" => name_path.dump,
196
- "MM_ACTUAL_JOB_SECURITY_TOKEN" => "", # TODO トークンの生成
197
- "MM_SCHEDULE_ID" => execution.id.to_s,
198
- "MM_SCHEDULE_ESTIMATED_TIME" => execution.estimated_time,
199
- }
200
- if estimated_end = execution.actual_estimated_end
201
- result["MM_SCHEDULE_ESTIMATED_END"] = estimated_end.strftime("%Y%m%d%H%M%S")
202
- end
203
- if rjt = root.template
204
- t = rjt.find_descendant_by_name_path(self.name_path)
205
- unless t
206
- template_name_parts = self.name_path_until_expansion.split(Tengine::Job::NamePath::SEPARATOR).select{|s| !s.empty?}
207
- root_jobnet_name = template_name_parts.first
208
- if rjt = Tengine::Job::RootJobnetTemplate.find_by_name(root_jobnet_name, :version => rjt.dsl_version)
209
- t = rjt.find_descendant_by_name_path(self.name_path_until_expansion)
210
- raise "template job #{path.inspect} not found in #{rjt.inspect}" unless t
211
- else
212
- raise "Tengine::Job::RootJobnetTemplate not found #{self.name_path_until_expansion.inspect}"
213
- end
214
- end
215
- result.update({
216
- "MM_TEMPLATE_JOB_ID" => t.id.to_s,
217
- "MM_TEMPLATE_JOB_ANCESTOR_IDS" => '"%s"' % t.ancestors.map(&:id).map(&:to_s).join(';'),
218
- })
219
- end
220
- # if ms = execution.master_schedule
221
- # result.update({
222
- # "MM_MASTER_SCHEDULE_ID" => ms.id.to_s,
223
- # })
224
- # end
225
- result
226
- end
227
-
228
- def hadoop_job_env
229
- s = children.select{|c| c.is_a?(Tengine::Job::Jobnet) && (c.jobnet_type_key == :hadoop_job)}.
230
- map{|c| "#{c.name}\\t#{c.id.to_s}\\n"}.join
231
- "MM_HADOOP_JOBS=\"#{s}\""
232
- end
233
-
234
-
235
- end
@@ -1,20 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'tengine/job'
3
-
4
- # ジョブネットの始端を表すVertex。特に状態は持たない。
5
- class Tengine::Job::Start < Tengine::Job::Vertex
6
-
7
- # https://cacoo.com/diagrams/hdLgrzYsTBBpV3Wj#D26C1
8
- def transmit(signal)
9
- activate(signal)
10
- end
11
-
12
- def activate(signal)
13
- signal.leave(self)
14
- end
15
-
16
- def reset(signal)
17
- signal.leave(self, :reset)
18
- end
19
-
20
- end
@@ -1,15 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'tengine/job'
3
- require 'selectable_attr'
4
-
5
- # ジョブ/ジョブネットを実行する際の情報に関するモジュール
6
- # Tengine::Job::JobnetActual, Tengine::Job::JobnetTemplateがこのモジュールをincludeします
7
- module Tengine::Job::Stoppable
8
- extend ActiveSupport::Concern
9
-
10
- included do
11
- field :stopped_at , :type => DateTime # 停止時刻。停止を開始した時刻です。
12
- field :stop_reason, :type => String # 停止理由。手動以外での停止ならば停止した理由が設定されます。
13
- end
14
-
15
- end
@@ -1,181 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'tengine/job'
3
-
4
- # Edgeとともにジョブネットを構成するグラフの「頂点」を表すモデル
5
- # 自身がツリー構造を
6
- class Tengine::Job::Vertex
7
- include Mongoid::Document
8
- include Mongoid::Timestamps
9
- include Tengine::Job::Signal::Transmittable
10
- include Tengine::Job::NamePath
11
-
12
- self.cyclic = true
13
- with_options(:class_name => self.name, :cyclic => true) do |c|
14
- c.embedded_in :parent , :inverse_of => :children
15
- c.embeds_many :children, :inverse_of => :parent , :validate => false
16
- end
17
-
18
- before_validation do |r|
19
- r.children.each do |child|
20
- child.valid?
21
- child.errors.each do |f, error|
22
- r.errors.add(:base, error)
23
- end
24
- end
25
- end
26
-
27
- # def short_inspect
28
- # "#<%%%-30s id: %s>" % [self.class.name, self.id.to_s]
29
- # end
30
- # alias_method :long_inspect, :inspect
31
- # alias_method :inspect, :short_inspect
32
-
33
- class VertexValidations < Mongoid::Errors::Validations
34
- def translate(key, options)
35
- ::I18n.translate(
36
- "#{Mongoid::Errors::MongoidError::BASE_KEY}.validations",
37
- {:errors => Tengine::Job::Vertex.flatten_errors(document).to_a.join(', ')})
38
- end
39
- end
40
-
41
-
42
- class << self
43
- def flatten_errors(vertex, dest = nil)
44
- dest ||= []
45
- children_errors = vertex.errors.messages.delete(:children)
46
- edges_errors = vertex.errors.messages.delete(:edges)
47
- vertex.errors.full_messages.each{|msg| dest << "#{vertex.name_path} #{msg}"}
48
- vertex.children.each{|child| flatten_errors(child, dest)}
49
- if vertex.respond_to?(:edges)
50
- vertex.edges.each do|edge|
51
- edge.errors.full_messages.each{|msg| dest << "#{edge.name_for_message} #{msg}"}
52
- end
53
- end
54
- dest
55
- end
56
-
57
- def raise_flatten_errors
58
- yield if block_given?
59
- rescue Mongoid::Errors::Validations => e
60
- raise VertexValidations, e.document
61
- end
62
-
63
- def create!(*args, &block)
64
- raise_flatten_errors{ super(*args, &block) }
65
- end
66
- end
67
-
68
- def save!(*args)
69
- self.class.raise_flatten_errors{ super(*args) }
70
- end
71
- def update_attributes!(*args)
72
- self.class.raise_flatten_errors{ super(*args) }
73
- end
74
-
75
-
76
-
77
- def previous_edges
78
- return nil unless parent
79
- parent.edges.select{|edge| edge.destination_id == self.id}
80
- end
81
- alias_method :prev_edges, :previous_edges
82
-
83
- def next_edges
84
- return nil unless parent
85
- parent.edges.select{|edge| edge.origin_id == self.id}
86
- end
87
-
88
- def root
89
- (parent = self.parent) ? parent.root : self
90
- end
91
-
92
- def ancestors
93
- if parent = self.parent
94
- parent.ancestors + [parent]
95
- else
96
- []
97
- end
98
- end
99
-
100
- def ancestors_until_expansion
101
- if (parent = self.parent) && !self.was_expansion?
102
- parent.ancestors_until_expansion + [parent]
103
- else
104
- []
105
- end
106
- end
107
-
108
- IGNORED_FIELD_NAMES = ["_type", "_id"].freeze
109
-
110
- def actual_class; self.class; end
111
- def generating_attrs
112
- field_names = self.class.fields.keys - IGNORED_FIELD_NAMES
113
- field_names.inject({}){|d, name| d[name] = send(name); d }
114
- end
115
- def generating_children; self.children; end
116
- def generating_edges; respond_to?(:edges) ? self.edges : []; end
117
-
118
- def generate(klass = actual_class)
119
- result = klass.new(generating_attrs)
120
- src_to_generated = {}
121
- generating_children.each do |child|
122
- generated = child.generate
123
- src_to_generated[child.id] = generated.id
124
- result.children << generated
125
- end
126
- generating_edges.each do |edge|
127
- generated = edge.class.new
128
- generated.origin_id = src_to_generated[edge.origin_id]
129
- generated.destination_id = src_to_generated[edge.destination_id]
130
- result.edges << generated
131
- end
132
- result
133
- end
134
-
135
- def accept_visitor(visitor)
136
- visitor.visit(self)
137
- end
138
-
139
- class AnyVisitor
140
- def initialize(&block)
141
- @block = block
142
- end
143
- def visit(vertex)
144
- if result = @block.call(vertex)
145
- return result
146
- end
147
- vertex.children.each do |child|
148
- if result = child.accept_visitor(self)
149
- return result
150
- end
151
- end
152
- return nil
153
- end
154
- end
155
-
156
- class AllVisitor
157
- def initialize(&block)
158
- @block = block
159
- end
160
-
161
- def visit(vertex)
162
- @block.call(vertex)
163
- vertex.children.each do |child|
164
- child.accept_visitor(self)
165
- end
166
- end
167
- end
168
-
169
- class AllVisitorWithEdge < AllVisitor
170
- def visit(obj)
171
- if obj.respond_to?(:children)
172
- super(obj)
173
- else
174
- @block.call(obj)
175
- end
176
- return unless obj.respond_to?(:edges)
177
- obj.edges.each{|edge| edge.accept_visitor(self)}
178
- end
179
- end
180
-
181
- end