dynflow 0.7.9 → 0.8.0

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.
Files changed (118) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +16 -1
  3. data/Gemfile +13 -1
  4. data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
  5. data/doc/pages/source/_includes/menu.html +1 -0
  6. data/doc/pages/source/_includes/menu_right.html +1 -1
  7. data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
  8. data/doc/pages/source/_sass/_style.scss +4 -0
  9. data/doc/pages/source/blog/index.html +12 -0
  10. data/doc/pages/source/documentation/index.md +330 -5
  11. data/dynflow.gemspec +3 -1
  12. data/examples/example_helper.rb +18 -11
  13. data/examples/orchestrate_evented.rb +2 -1
  14. data/examples/remote_executor.rb +53 -20
  15. data/lib/dynflow.rb +16 -6
  16. data/lib/dynflow/action/suspended.rb +1 -1
  17. data/lib/dynflow/action/with_sub_plans.rb +3 -6
  18. data/lib/dynflow/actor.rb +56 -0
  19. data/lib/dynflow/clock.rb +43 -38
  20. data/lib/dynflow/config.rb +107 -0
  21. data/lib/dynflow/connectors.rb +7 -0
  22. data/lib/dynflow/connectors/abstract.rb +41 -0
  23. data/lib/dynflow/connectors/database.rb +175 -0
  24. data/lib/dynflow/connectors/direct.rb +71 -0
  25. data/lib/dynflow/coordinator.rb +280 -0
  26. data/lib/dynflow/coordinator_adapters.rb +8 -0
  27. data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
  28. data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
  29. data/lib/dynflow/dispatcher.rb +58 -0
  30. data/lib/dynflow/dispatcher/abstract.rb +14 -0
  31. data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
  32. data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
  33. data/lib/dynflow/errors.rb +7 -1
  34. data/lib/dynflow/execution_history.rb +46 -0
  35. data/lib/dynflow/execution_plan.rb +19 -15
  36. data/lib/dynflow/executors.rb +0 -1
  37. data/lib/dynflow/executors/abstract.rb +5 -10
  38. data/lib/dynflow/executors/parallel.rb +16 -13
  39. data/lib/dynflow/executors/parallel/core.rb +76 -78
  40. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
  41. data/lib/dynflow/executors/parallel/pool.rb +22 -52
  42. data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
  43. data/lib/dynflow/executors/parallel/worker.rb +5 -10
  44. data/lib/dynflow/persistence.rb +14 -0
  45. data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
  46. data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
  47. data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
  48. data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
  49. data/lib/dynflow/round_robin.rb +37 -0
  50. data/lib/dynflow/serializable.rb +1 -2
  51. data/lib/dynflow/serializer.rb +46 -0
  52. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  53. data/lib/dynflow/testing/dummy_world.rb +1 -1
  54. data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
  55. data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
  56. data/lib/dynflow/version.rb +1 -1
  57. data/lib/dynflow/web.rb +26 -0
  58. data/lib/dynflow/web/console.rb +108 -0
  59. data/lib/dynflow/web/console_helpers.rb +158 -0
  60. data/lib/dynflow/web/filtering_helpers.rb +85 -0
  61. data/lib/dynflow/web/world_helpers.rb +9 -0
  62. data/lib/dynflow/web_console.rb +3 -310
  63. data/lib/dynflow/world.rb +188 -119
  64. data/test/abnormal_states_recovery_test.rb +152 -0
  65. data/test/action_test.rb +2 -3
  66. data/test/clock_test.rb +1 -5
  67. data/test/coordinator_test.rb +152 -0
  68. data/test/dispatcher_test.rb +146 -0
  69. data/test/execution_plan_test.rb +2 -1
  70. data/test/executor_test.rb +534 -612
  71. data/test/middleware_test.rb +4 -4
  72. data/test/persistence_test.rb +17 -0
  73. data/test/prepare_travis_env.sh +35 -0
  74. data/test/rescue_test.rb +5 -3
  75. data/test/round_robin_test.rb +28 -0
  76. data/test/support/code_workflow_example.rb +0 -73
  77. data/test/support/dummy_example.rb +130 -0
  78. data/test/support/test_execution_log.rb +41 -0
  79. data/test/test_helper.rb +222 -116
  80. data/test/testing_test.rb +10 -10
  81. data/test/web_console_test.rb +3 -3
  82. data/test/world_test.rb +23 -0
  83. data/web/assets/images/logo-square.png +0 -0
  84. data/web/assets/stylesheets/application.css +9 -0
  85. data/web/assets/vendor/bootstrap/config.json +429 -0
  86. data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
  87. data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
  88. data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
  89. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
  90. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  91. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  92. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  93. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  94. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  95. data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
  96. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
  97. data/web/views/execution_history.erb +17 -0
  98. data/web/views/index.erb +4 -6
  99. data/web/views/layout.erb +44 -8
  100. data/web/views/show.erb +4 -5
  101. data/web/views/worlds.erb +26 -0
  102. metadata +116 -23
  103. checksums.yaml +0 -15
  104. data/lib/dynflow/daemon.rb +0 -30
  105. data/lib/dynflow/executors/remote_via_socket.rb +0 -43
  106. data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
  107. data/lib/dynflow/future.rb +0 -173
  108. data/lib/dynflow/listeners.rb +0 -7
  109. data/lib/dynflow/listeners/abstract.rb +0 -17
  110. data/lib/dynflow/listeners/serialization.rb +0 -77
  111. data/lib/dynflow/listeners/socket.rb +0 -117
  112. data/lib/dynflow/micro_actor.rb +0 -102
  113. data/lib/dynflow/simple_world.rb +0 -19
  114. data/test/remote_via_socket_test.rb +0 -170
  115. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
  116. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
  117. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  118. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
@@ -0,0 +1,7 @@
1
+ module Dynflow
2
+ module Connectors
3
+ require 'dynflow/connectors/abstract'
4
+ require 'dynflow/connectors/direct'
5
+ require 'dynflow/connectors/database'
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module Dynflow
2
+ module Connectors
3
+ class Abstract
4
+ include Algebrick::TypeCheck
5
+ include Algebrick::Matching
6
+
7
+ def start_listening(world)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def stop_listening(world)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def terminate
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def send(envelope)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # we need to pass the world, as the connector can be shared
24
+ # between words: we need to know the one to send the message to
25
+ def receive(world, envelope)
26
+ Type! envelope, Dispatcher::Envelope
27
+ match(envelope.message,
28
+ (on Dispatcher::Ping do
29
+ response_envelope = envelope.build_response_envelope(Dispatcher::Pong, world)
30
+ send(response_envelope)
31
+ end),
32
+ (on Dispatcher::Request do
33
+ world.executor_dispatcher.tell([:handle_request, envelope])
34
+ end),
35
+ (on Dispatcher::Response do
36
+ world.client_dispatcher.tell([:dispatch_response, envelope])
37
+ end))
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,175 @@
1
+ module Dynflow
2
+ module Connectors
3
+ class Database < Abstract
4
+
5
+ class PostgresListerner
6
+ def initialize(core, world_id, db)
7
+ @core = core
8
+ @db = db
9
+ @world_id = world_id
10
+ @started = Concurrent::AtomicReference.new
11
+ end
12
+
13
+ def self.notify_supported?(db)
14
+ db.class.name == "Sequel::Postgres::Database"
15
+ end
16
+
17
+ def started?
18
+ @started.get
19
+ end
20
+
21
+ def start
22
+ @started.set true
23
+ @thread = Thread.new do
24
+ @db.listen("world:#{ @world_id }", :loop => true) do
25
+ if started?
26
+ @core << :check_inbox
27
+ else
28
+ break # the listener is stopped: don't continue listening
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def notify(world_id)
35
+ @db.notify("world:#{world_id}")
36
+ end
37
+
38
+ def stop
39
+ @started.set false
40
+ notify(@world_id)
41
+ end
42
+ end
43
+
44
+ class Core < Actor
45
+ attr_reader :polling_interval
46
+
47
+ def initialize(connector, polling_interval)
48
+ @connector = connector
49
+ @world = nil
50
+ @executor_round_robin = RoundRobin.new
51
+ @stopped = false
52
+ @polling_interval = polling_interval
53
+ end
54
+
55
+ def stopped?
56
+ !!@stopped
57
+ end
58
+
59
+ def start_listening(world)
60
+ @world = world
61
+ @stopped = false
62
+ postgres_listen_start
63
+ self << :periodic_check_inbox
64
+ end
65
+
66
+ def stop_receiving_new_work
67
+ @world.coordinator.deactivate_world(@world.registered_world)
68
+ end
69
+
70
+ def stop_listening
71
+ @stopped = true
72
+ postgres_listen_stop
73
+ end
74
+
75
+ def periodic_check_inbox
76
+ self << :check_inbox
77
+ @world.clock.ping(self, polling_interval, :periodic_check_inbox) unless @stopped
78
+ end
79
+
80
+ def check_inbox
81
+ return unless @world
82
+ receive_envelopes
83
+ end
84
+
85
+ def handle_envelope(envelope)
86
+ world_id = find_receiver(envelope)
87
+ if world_id == @world.id
88
+ if @stopped
89
+ log(Logger::ERROR, "Envelope #{envelope} received for stopped world")
90
+ else
91
+ @connector.receive(@world, envelope)
92
+ end
93
+ else
94
+ send_envelope(update_receiver_id(envelope, world_id))
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def postgres_listen_start
101
+ if PostgresListerner.notify_supported?(@world.persistence.adapter.db)
102
+ @postgres_listener ||= PostgresListerner.new(self, @world.id, @world.persistence.adapter.db)
103
+ @postgres_listener.start unless @postgres_listener.started?
104
+ end
105
+ end
106
+
107
+ def postgres_listen_stop
108
+ @postgres_listener.stop if @postgres_listener
109
+ end
110
+
111
+ def receive_envelopes
112
+ @world.persistence.pull_envelopes(@world.id).each do |envelope|
113
+ self.tell([:handle_envelope, envelope])
114
+ end
115
+ rescue => e
116
+ log(Logger::ERROR, "Receiving envelopes failed on #{e}")
117
+ end
118
+
119
+ def send_envelope(envelope)
120
+ @world.persistence.push_envelope(envelope)
121
+ if @postgres_listener
122
+ @postgres_listener.notify(envelope.receiver_id)
123
+ end
124
+ rescue => e
125
+ log(Logger::ERROR, "Sending envelope failed on #{e}")
126
+ end
127
+
128
+ def update_receiver_id(envelope, new_receiver_id)
129
+ Dispatcher::Envelope[envelope.request_id, envelope.sender_id, new_receiver_id, envelope.message]
130
+ end
131
+
132
+ def find_receiver(envelope)
133
+ if Dispatcher::AnyExecutor === envelope.receiver_id
134
+ any_executor.id
135
+ else
136
+ envelope.receiver_id
137
+ end
138
+ end
139
+
140
+ def any_executor
141
+ @executor_round_robin.data = @world.coordinator.find_worlds(true)
142
+ @executor_round_robin.next or raise Dynflow::Error, "No executor available"
143
+ end
144
+ end
145
+
146
+ def initialize(world = nil, polling_interval = nil)
147
+ polling_interval ||= begin
148
+ if world && PostgresListerner.notify_supported?(world.persistence.adapter.db)
149
+ 30 # when the notify is supported, we don't need that much polling
150
+ else
151
+ 1
152
+ end
153
+ end
154
+ @core = Core.spawn('connector-database-core', self, polling_interval)
155
+ start_listening(world) if world
156
+ end
157
+
158
+ def start_listening(world)
159
+ @core.ask([:start_listening, world])
160
+ end
161
+
162
+ def stop_receiving_new_work(_)
163
+ @core.ask(:stop_receiving_new_work).wait
164
+ end
165
+
166
+ def stop_listening(_)
167
+ @core.ask(:stop_listening).then { @core.ask(:terminate!) }.wait
168
+ end
169
+
170
+ def send(envelope)
171
+ @core.ask([:handle_envelope, envelope])
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,71 @@
1
+ module Dynflow
2
+ module Connectors
3
+ class Direct < Abstract
4
+
5
+ class Core < Actor
6
+
7
+ def initialize(connector)
8
+ @connector = connector
9
+ @worlds = {}
10
+ @executor_round_robin = RoundRobin.new
11
+ end
12
+
13
+ def start_listening(world)
14
+ @worlds[world.id] = world
15
+ @executor_round_robin.add(world) if world.executor
16
+ end
17
+
18
+ def stop_receiving_new_work(world)
19
+ @executor_round_robin.delete(world)
20
+ end
21
+
22
+ def stop_listening(world)
23
+ @worlds.delete(world.id)
24
+ @executor_round_robin.delete(world) if world.executor
25
+ reference.tell(:terminate!) if @worlds.empty?
26
+ end
27
+
28
+ def handle_envelope(envelope)
29
+ if world = find_receiver(envelope)
30
+ @connector.receive(world, envelope)
31
+ else
32
+ log(Logger::ERROR, "Receiver for envelope #{ envelope } not found")
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def find_receiver(envelope)
39
+ receiver = if Dispatcher::AnyExecutor === envelope.receiver_id
40
+ @executor_round_robin.next
41
+ else
42
+ @worlds[envelope.receiver_id]
43
+ end
44
+ raise Dynflow::Error, "No executor available" unless receiver
45
+ return receiver
46
+ end
47
+ end
48
+
49
+ def initialize(world = nil)
50
+ @core = Core.spawn('connector-direct-core', self)
51
+ start_listening(world) if world
52
+ end
53
+
54
+ def start_listening(world)
55
+ @core.ask([:start_listening, world])
56
+ end
57
+
58
+ def stop_receiving_new_work(world)
59
+ @core.ask([:stop_receiving_new_work, world]).wait
60
+ end
61
+
62
+ def stop_listening(world)
63
+ @core.ask([:stop_listening, world]).wait
64
+ end
65
+
66
+ def send(envelope)
67
+ @core.ask([:handle_envelope, envelope])
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,280 @@
1
+ require 'dynflow/coordinator_adapters'
2
+
3
+ module Dynflow
4
+ class Coordinator
5
+
6
+ include Algebrick::TypeCheck
7
+
8
+ class DuplicateRecordError < Dynflow::Error
9
+ attr_reader :record
10
+
11
+ def initialize(record)
12
+ @record = record
13
+ super("record #{record} already exists")
14
+ end
15
+ end
16
+
17
+ class LockError < Dynflow::Error
18
+ attr_reader :lock
19
+
20
+ def initialize(lock)
21
+ @lock = lock
22
+ super("Unable to acquire lock #{lock}")
23
+ end
24
+ end
25
+
26
+ class Record < Serializable
27
+ attr_reader :data
28
+
29
+ include Algebrick::TypeCheck
30
+
31
+ def self.new_from_hash(hash)
32
+ self.allocate.tap { |record| record.from_hash(hash) }
33
+ end
34
+
35
+ def self.constantize(name)
36
+ Serializable.constantize(name)
37
+ rescue NameError
38
+ # If we don't find the lock name, return the most generic version
39
+ Record
40
+ end
41
+
42
+ def initialize(*args)
43
+ @data ||= {}
44
+ @data = @data.merge(class: self.class.name).with_indifferent_access
45
+ end
46
+
47
+ def from_hash(hash)
48
+ @data = hash
49
+ @from_hash = true
50
+ end
51
+
52
+ def to_hash
53
+ @data
54
+ end
55
+
56
+ def id
57
+ @data[:id]
58
+ end
59
+
60
+ # @api override
61
+ # check to be performed before we try to acquire the lock
62
+ def validate!
63
+ Type! id, String
64
+ Type! @data, Hash
65
+ end
66
+
67
+ def to_s
68
+ "#{self.class.name}: #{id}"
69
+ end
70
+
71
+ def ==(other_object)
72
+ self.class == other_object.class && self.id == other_object.id
73
+ end
74
+
75
+ def hash
76
+ [self.class, self.id].hash
77
+ end
78
+ end
79
+
80
+ class WorldRecord < Record
81
+ def initialize(world)
82
+ super
83
+ @data[:id] = world.id
84
+ @data[:meta] = world.meta
85
+ end
86
+
87
+ def meta
88
+ @data[:meta]
89
+ end
90
+ end
91
+
92
+ class ExecutorWorld < WorldRecord
93
+ def initialize(world)
94
+ super
95
+ self.active = !world.terminating?
96
+ end
97
+
98
+ def active?
99
+ @data[:active]
100
+ end
101
+
102
+ def active=(value)
103
+ Type! value, Algebrick::Types::Boolean
104
+ @data[:active] = value
105
+ end
106
+ end
107
+
108
+ class ClientWorld < WorldRecord
109
+ end
110
+
111
+ class Lock < Record
112
+ def self.constantize(name)
113
+ Serializable.constantize(name)
114
+ rescue NameError
115
+ # If we don't find the lock name, return the most generic version
116
+ Lock
117
+ end
118
+
119
+ def to_s
120
+ "#{self.class.name}: #{id} by #{owner_id}"
121
+ end
122
+
123
+ def owner_id
124
+ @data[:owner_id]
125
+ end
126
+
127
+ # @api override
128
+ # check to be performed before we try to acquire the lock
129
+ def validate!
130
+ super
131
+ raise "Can't acquire the lock after deserialization" if @from_hash
132
+ Type! owner_id, String
133
+ end
134
+
135
+ def to_s
136
+ "#{self.class.name}: #{id} by #{owner_id}"
137
+ end
138
+ end
139
+
140
+ class LockByWorld < Lock
141
+ def initialize(world)
142
+ super
143
+ @world = world
144
+ @data.merge!(owner_id: "world:#{world.id}", world_id: world.id)
145
+ end
146
+
147
+ def validate!
148
+ super
149
+ raise Errors::InactiveWorldError.new(@world) if @world.terminating?
150
+ end
151
+
152
+ def world_id
153
+ @data[:world_id]
154
+ end
155
+
156
+ end
157
+
158
+ class WorldInvalidationLock < LockByWorld
159
+ def initialize(world, invalidated_world)
160
+ super(world)
161
+ @data[:id] = "world-invalidation:#{invalidated_world.id}"
162
+ end
163
+ end
164
+
165
+ class AutoExecuteLock < LockByWorld
166
+ def initialize(*args)
167
+ super
168
+ @data[:id] = "auto-execute"
169
+ end
170
+ end
171
+
172
+ class ExecutionLock < LockByWorld
173
+ def initialize(world, execution_plan_id, client_world_id, request_id)
174
+ super(world)
175
+ @data.merge!(id: "execution-plan:#{execution_plan_id}",
176
+ execution_plan_id: execution_plan_id,
177
+ client_world_id: client_world_id,
178
+ request_id: request_id)
179
+ end
180
+
181
+ # we need to store the following data in case of
182
+ # invalidation of the lock from outside (after
183
+ # the owner world terminated unexpectedly)
184
+ def execution_plan_id
185
+ @data[:execution_plan_id]
186
+ end
187
+
188
+ def client_world_id
189
+ @data[:client_world_id]
190
+ end
191
+
192
+ def request_id
193
+ @data[:request_id]
194
+ end
195
+ end
196
+
197
+ attr_reader :adapter
198
+
199
+ def initialize(coordinator_adapter)
200
+ @adapter = coordinator_adapter
201
+ end
202
+
203
+ def acquire(lock, &block)
204
+ Type! lock, Lock
205
+ lock.validate!
206
+ adapter.create_record(lock)
207
+ if block
208
+ begin
209
+ block.call
210
+ ensure
211
+ release(lock)
212
+ end
213
+ end
214
+ rescue DuplicateRecordError => e
215
+ raise LockError.new(e.record)
216
+ end
217
+
218
+ def release(lock)
219
+ Type! lock, Lock
220
+ adapter.delete_record(lock)
221
+ end
222
+
223
+ def release_by_owner(owner_id)
224
+ find_locks(owner_id: owner_id).map { |lock| release(lock) }
225
+ end
226
+
227
+ def find_locks(filter_options)
228
+ adapter.find_records(filter_options).map do |lock_data|
229
+ Lock.from_hash(lock_data)
230
+ end
231
+ end
232
+
233
+ def create_record(record)
234
+ Type! record, Record
235
+ adapter.create_record(record)
236
+ end
237
+
238
+ def update_record(record)
239
+ Type! record, Record
240
+ adapter.update_record(record)
241
+ end
242
+
243
+ def delete_record(record)
244
+ Type! record, Record
245
+ adapter.delete_record(record)
246
+ end
247
+
248
+ def find_records(filter)
249
+ adapter.find_records(filter).map do |record_data|
250
+ Record.from_hash(record_data)
251
+ end
252
+ end
253
+
254
+ def find_worlds(active_executor_only = false, filters = {})
255
+ ret = find_records(filters.merge(class: Coordinator::ExecutorWorld.name))
256
+ if active_executor_only
257
+ ret = ret.select(&:active?)
258
+ else
259
+ ret.concat(find_records(filters.merge(class: Coordinator::ClientWorld.name)))
260
+ end
261
+ ret
262
+ end
263
+
264
+ def register_world(world)
265
+ Type! world, Coordinator::ClientWorld, Coordinator::ExecutorWorld
266
+ create_record(world)
267
+ end
268
+
269
+ def delete_world(world)
270
+ Type! world, Coordinator::ClientWorld, Coordinator::ExecutorWorld
271
+ delete_record(world)
272
+ end
273
+
274
+ def deactivate_world(world)
275
+ Type! world, Coordinator::ExecutorWorld
276
+ world.active = false
277
+ update_record(world)
278
+ end
279
+ end
280
+ end