promiscuous 0.91.0 → 0.92.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/promiscuous.rb +1 -1
- data/lib/promiscuous/amqp.rb +1 -1
- data/lib/promiscuous/amqp/fake.rb +0 -3
- data/lib/promiscuous/amqp/file.rb +81 -0
- data/lib/promiscuous/amqp/null.rb +0 -3
- data/lib/promiscuous/cli.rb +35 -25
- data/lib/promiscuous/config.rb +8 -3
- data/lib/promiscuous/error.rb +1 -2
- data/lib/promiscuous/key.rb +1 -12
- data/lib/promiscuous/mongoid.rb +7 -0
- data/lib/promiscuous/publisher/context/base.rb +4 -4
- data/lib/promiscuous/publisher/context/middleware.rb +2 -23
- data/lib/promiscuous/publisher/model/ephemeral.rb +5 -1
- data/lib/promiscuous/publisher/model/mock.rb +9 -7
- data/lib/promiscuous/publisher/model/mongoid.rb +3 -1
- data/lib/promiscuous/publisher/operation.rb +1 -1
- data/lib/promiscuous/publisher/operation/atomic.rb +44 -32
- data/lib/promiscuous/publisher/operation/base.rb +14 -9
- data/lib/promiscuous/publisher/operation/ephemeral.rb +14 -0
- data/lib/promiscuous/publisher/operation/mongoid.rb +4 -12
- data/lib/promiscuous/subscriber/message_processor/base.rb +17 -1
- data/lib/promiscuous/subscriber/message_processor/regular.rb +94 -48
- data/lib/promiscuous/subscriber/model/active_record.rb +25 -0
- data/lib/promiscuous/subscriber/model/base.rb +17 -13
- data/lib/promiscuous/subscriber/model/mongoid.rb +20 -1
- data/lib/promiscuous/subscriber/model/observer.rb +4 -0
- data/lib/promiscuous/subscriber/operation/base.rb +14 -16
- data/lib/promiscuous/subscriber/operation/bootstrap.rb +7 -1
- data/lib/promiscuous/subscriber/operation/regular.rb +6 -0
- data/lib/promiscuous/subscriber/worker.rb +6 -2
- data/lib/promiscuous/subscriber/worker/eventual_destroyer.rb +85 -0
- data/lib/promiscuous/subscriber/worker/message.rb +9 -15
- data/lib/promiscuous/subscriber/worker/message_synchronizer.rb +24 -78
- data/lib/promiscuous/subscriber/worker/runner.rb +6 -2
- data/lib/promiscuous/subscriber/worker/stats.rb +11 -7
- data/lib/promiscuous/version.rb +1 -1
- metadata +66 -63
- data/lib/promiscuous/error/already_processed.rb +0 -5
@@ -25,7 +25,7 @@ class Promiscuous::Publisher::Operation::Base
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def recovering?
|
28
|
-
!!@
|
28
|
+
!!@recovery_data
|
29
29
|
end
|
30
30
|
|
31
31
|
def current_context
|
@@ -147,11 +147,14 @@ class Promiscuous::Publisher::Operation::Base
|
|
147
147
|
def generate_payload
|
148
148
|
payload = {}
|
149
149
|
payload[:operations] = operation_payloads
|
150
|
-
payload[:context] = current_context.name
|
151
150
|
payload[:app] = Promiscuous::Config.app
|
151
|
+
payload[:context] = current_context.name
|
152
|
+
payload[:current_user_id] = current_context.current_user.id if current_context.current_user
|
152
153
|
payload[:timestamp] = @timestamp
|
154
|
+
payload[:generation] = Promiscuous::Config.generation
|
153
155
|
payload[:host] = Socket.gethostname
|
154
|
-
payload[:
|
156
|
+
payload[:was_during_bootstrap] = true if @was_during_bootstrap
|
157
|
+
payload[:recovered_operation] = true if recovering?
|
155
158
|
payload[:dependencies] = {}
|
156
159
|
payload[:dependencies][:read] = @committed_read_deps if @committed_read_deps.present?
|
157
160
|
payload[:dependencies][:write] = @committed_write_deps
|
@@ -203,10 +206,10 @@ class Promiscuous::Publisher::Operation::Base
|
|
203
206
|
@read_dependencies = read_dependencies
|
204
207
|
@write_dependencies = write_dependencies
|
205
208
|
@op_lock = lock
|
206
|
-
@
|
209
|
+
@recovery_data = recovery_data
|
207
210
|
|
208
211
|
query = Promiscuous::Publisher::Operation::ProxyForQuery.new(self) { recover_db_operation }
|
209
|
-
execute_instrumented(query)
|
212
|
+
self.execute_instrumented(query)
|
210
213
|
query.result
|
211
214
|
end
|
212
215
|
end
|
@@ -256,7 +259,8 @@ class Promiscuous::Publisher::Operation::Base
|
|
256
259
|
# The index of the first write is then used to pass to redis along with the
|
257
260
|
# dependencies. This is done because arguments to redis LUA scripts cannot
|
258
261
|
# accept complex data types.
|
259
|
-
|
262
|
+
first_read_index = deps.index(&:read?) || deps.length
|
263
|
+
argv << first_read_index
|
260
264
|
|
261
265
|
# Each shard have their own recovery payload. The master recovery node
|
262
266
|
# has the full operation recovery, and the others just have their versions.
|
@@ -326,12 +330,13 @@ class Promiscuous::Publisher::Operation::Base
|
|
326
330
|
return { first_read_index-1, versions }
|
327
331
|
SCRIPT
|
328
332
|
|
329
|
-
|
333
|
+
received_first_read_index, versions = @@increment_script.eval(node, :argv => argv, :keys => deps)
|
330
334
|
|
331
335
|
deps.zip(versions).each { |dep, version| dep.version = version }
|
332
336
|
|
333
|
-
@committed_write_deps += deps[0...
|
334
|
-
@committed_read_deps += deps[
|
337
|
+
@committed_write_deps += deps[0...received_first_read_index]
|
338
|
+
@committed_read_deps += deps[received_first_read_index..-1]
|
339
|
+
@was_during_bootstrap = true if first_read_index != received_first_read_index
|
335
340
|
end
|
336
341
|
|
337
342
|
# The instance version must to be the first in the list to allow atomic
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Promiscuous::Publisher::Operation::Ephemeral < Promiscuous::Publisher::Operation::Atomic
|
2
|
+
def execute
|
3
|
+
super {}
|
4
|
+
end
|
5
|
+
|
6
|
+
def yell_about_missing_instance
|
7
|
+
# don't yell :)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.recover_operation(*recovery_payload)
|
11
|
+
# no instance when we recover, it's okay
|
12
|
+
new(:instance => nil)
|
13
|
+
end
|
14
|
+
end
|
@@ -157,16 +157,16 @@ class Moped::PromiscuousQueryWrapper < Moped::Query
|
|
157
157
|
@query.selector = selector
|
158
158
|
end
|
159
159
|
|
160
|
-
def
|
161
|
-
@change['$
|
162
|
-
@change['$
|
160
|
+
def increment_version_in_document
|
161
|
+
@change['$inc'] ||= {}
|
162
|
+
@change['$inc'][Promiscuous::Config.version_field] = 1
|
163
163
|
end
|
164
164
|
|
165
165
|
def execute_instrumented(query)
|
166
166
|
# We are trying to be optimistic for the locking. We are trying to figure
|
167
167
|
# out our dependencies with the selector upfront to avoid an extra read
|
168
168
|
# from reload_instance.
|
169
|
-
@instance ||= get_selector_instance
|
169
|
+
@instance ||= get_selector_instance unless recovering? && operation == :update
|
170
170
|
super
|
171
171
|
end
|
172
172
|
|
@@ -360,14 +360,6 @@ class Mongoid::Validations::UniquenessValidator
|
|
360
360
|
end
|
361
361
|
end
|
362
362
|
|
363
|
-
class Moped::BSON::ObjectId
|
364
|
-
# No {"$oid": "123"}, it's horrible.
|
365
|
-
# TODO Document this shit.
|
366
|
-
def to_json(*args)
|
367
|
-
"\"#{to_s}\""
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
363
|
Moped.__send__(:remove_const, :Collection)
|
372
364
|
Moped.__send__(:const_set, :Collection, Moped::PromiscuousCollectionWrapper)
|
373
365
|
Moped.__send__(:remove_const, :Query)
|
@@ -14,7 +14,7 @@ class Promiscuous::Subscriber::MessageProcessor::Base
|
|
14
14
|
|
15
15
|
begin
|
16
16
|
self.current = new(*args)
|
17
|
-
self.current.
|
17
|
+
self.current.process_message
|
18
18
|
ensure
|
19
19
|
self.current = nil
|
20
20
|
end
|
@@ -28,6 +28,22 @@ class Promiscuous::Subscriber::MessageProcessor::Base
|
|
28
28
|
Thread.current[:promiscuous_message_processor] = value
|
29
29
|
end
|
30
30
|
|
31
|
+
def process_message
|
32
|
+
begin
|
33
|
+
on_message
|
34
|
+
rescue Exception => e
|
35
|
+
@fail_count ||= 0; @fail_count += 1
|
36
|
+
|
37
|
+
if @fail_count <= Promiscuous::Config.max_retries
|
38
|
+
Promiscuous.warn("[receive] #{e.message} #{@fail_count.ordinalize} retry: #{@message}")
|
39
|
+
sleep @fail_count ** 2
|
40
|
+
process_message
|
41
|
+
else
|
42
|
+
raise e
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
31
47
|
def on_message
|
32
48
|
raise "Must be implemented"
|
33
49
|
end
|
@@ -33,32 +33,14 @@ class Promiscuous::Subscriber::MessageProcessor::Regular < Promiscuous::Subscrib
|
|
33
33
|
|
34
34
|
# XXX TODO Code is not tolerant to losing a lock.
|
35
35
|
|
36
|
-
def
|
37
|
-
# Read and write dependencies are not handled the same way:
|
38
|
-
# * Read dependencies are just incremented (which allow parallelization).
|
39
|
-
# * Write dependencies are set to be max(current_version, received_version).
|
40
|
-
# This allow the version bootstrapping process to be non-atomic.
|
41
|
-
# Publishers upgrade their reads dependencies to write dependencies
|
42
|
-
# during bootstrapping to permit the mechanism to function properly.
|
43
|
-
|
44
|
-
# TODO Evaluate the performance hit of this heavy mechanism, and see if it's
|
45
|
-
# worth optimizing it for the non-bootstrap case.
|
46
|
-
|
47
|
-
node = node_with_deps[0]
|
48
|
-
r_deps = node_with_deps[1].select(&:read?)
|
49
|
-
w_deps = node_with_deps[1].select(&:write?)
|
50
|
-
|
51
|
-
if options[:only_write_dependencies]
|
52
|
-
r_deps = []
|
53
|
-
end
|
54
|
-
|
36
|
+
def update_dependencies_non_atomic_bootstrap(node, r_deps, w_deps, options={})
|
55
37
|
argv = []
|
56
38
|
argv << MultiJson.dump([r_deps.map { |dep| dep.key(:sub) },
|
57
39
|
w_deps.map { |dep| dep.key(:sub) },
|
58
40
|
w_deps.map { |dep| dep.version }])
|
59
41
|
argv << recovery_key if options[:with_recovery]
|
60
42
|
|
61
|
-
@@
|
43
|
+
@@update_script_bootstrap ||= Promiscuous::Redis::Script.new <<-SCRIPT
|
62
44
|
local _args = cjson.decode(ARGV[1])
|
63
45
|
local read_deps = _args[1]
|
64
46
|
local write_deps = _args[2]
|
@@ -90,7 +72,56 @@ class Promiscuous::Subscriber::MessageProcessor::Regular < Promiscuous::Subscrib
|
|
90
72
|
end
|
91
73
|
SCRIPT
|
92
74
|
|
93
|
-
@@
|
75
|
+
@@update_script_bootstrap.eval(node, :argv => argv)
|
76
|
+
end
|
77
|
+
|
78
|
+
def update_dependencies_fast(node, r_deps, w_deps, options={})
|
79
|
+
keys = (r_deps + w_deps).map { |dep| dep.key(:sub) }
|
80
|
+
argv = options[:with_recovery] ? [recovery_key] : []
|
81
|
+
|
82
|
+
@@update_script_fast ||= Promiscuous::Redis::Script.new <<-SCRIPT
|
83
|
+
local deps = KEYS
|
84
|
+
local recovery_key = ARGV[1]
|
85
|
+
|
86
|
+
if recovery_key and redis.call('exists', recovery_key) == 1 then
|
87
|
+
return
|
88
|
+
end
|
89
|
+
|
90
|
+
for i, _key in ipairs(deps) do
|
91
|
+
local key = _key .. ':rw'
|
92
|
+
local v = redis.call('incr', key)
|
93
|
+
redis.call('publish', key, v)
|
94
|
+
end
|
95
|
+
|
96
|
+
if recovery_key then
|
97
|
+
redis.call('set', recovery_key, 'done')
|
98
|
+
end
|
99
|
+
SCRIPT
|
100
|
+
|
101
|
+
@@update_script_fast.eval(node, :keys => keys, :argv => argv)
|
102
|
+
end
|
103
|
+
|
104
|
+
def update_dependencies_on_node(node_with_deps, options={})
|
105
|
+
# Read and write dependencies are not handled the same way:
|
106
|
+
# * Read dependencies are just incremented (which allow parallelization).
|
107
|
+
# * Write dependencies are set to be max(current_version, received_version).
|
108
|
+
# This allow the version bootstrapping process to be non-atomic.
|
109
|
+
# Publishers upgrade their reads dependencies to write dependencies
|
110
|
+
# during bootstrapping to permit the mechanism to function properly.
|
111
|
+
|
112
|
+
# TODO Evaluate the performance hit of this heavy mechanism, and see if it's
|
113
|
+
# worth optimizing it for the non-bootstrap case.
|
114
|
+
|
115
|
+
node = node_with_deps[0]
|
116
|
+
r_deps = node_with_deps[1].select(&:read?)
|
117
|
+
w_deps = node_with_deps[1].select(&:write?)
|
118
|
+
|
119
|
+
if message.was_during_bootstrap?
|
120
|
+
raise "Message should not have any read deps" unless r_deps.empty?
|
121
|
+
update_dependencies_non_atomic_bootstrap(node, r_deps, w_deps, options)
|
122
|
+
else
|
123
|
+
update_dependencies_fast(node, r_deps, w_deps, options)
|
124
|
+
end
|
94
125
|
end
|
95
126
|
|
96
127
|
def update_dependencies_master(options={})
|
@@ -124,7 +155,7 @@ class Promiscuous::Subscriber::MessageProcessor::Regular < Promiscuous::Subscrib
|
|
124
155
|
cleanup_dependency_secondaries
|
125
156
|
end
|
126
157
|
|
127
|
-
def
|
158
|
+
def duplicate_message?
|
128
159
|
unless instance_dep.version >= get_current_instance_version + 1
|
129
160
|
# We happen to get a duplicate message, or we are recovering a dead
|
130
161
|
# worker. During regular operations, we just need to cleanup the 2pc (from
|
@@ -142,12 +173,11 @@ class Promiscuous::Subscriber::MessageProcessor::Regular < Promiscuous::Subscrib
|
|
142
173
|
# seldom: either (1) the publisher recovered a payload that didn't need
|
143
174
|
# recovery, or (2) a subscriber worker died after # update_dependencies_master,
|
144
175
|
# but before the message acking).
|
145
|
-
|
146
|
-
update_dependencies(:only_write_dependencies => true)
|
147
|
-
|
148
|
-
message.ack
|
176
|
+
update_dependencies if message.was_during_bootstrap?
|
149
177
|
|
150
|
-
|
178
|
+
true
|
179
|
+
else
|
180
|
+
false
|
151
181
|
end
|
152
182
|
end
|
153
183
|
|
@@ -155,35 +185,51 @@ class Promiscuous::Subscriber::MessageProcessor::Regular < Promiscuous::Subscrib
|
|
155
185
|
:sleep => 0.1, # polling every 100ms.
|
156
186
|
:expire => 1.minute } # after one minute, we are considered dead
|
157
187
|
|
158
|
-
def
|
159
|
-
if
|
160
|
-
|
161
|
-
|
162
|
-
|
188
|
+
def check_duplicate_and_update_dependencies
|
189
|
+
if duplicate_message?
|
190
|
+
Promiscuous.debug "[receive] Skipping message (already processed) #{message}"
|
191
|
+
return
|
192
|
+
end
|
163
193
|
|
164
|
-
|
165
|
-
raise Promiscuous::Error::LockUnavailable.new(mutex.key)
|
166
|
-
end
|
194
|
+
yield
|
167
195
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
196
|
+
update_dependencies
|
197
|
+
end
|
198
|
+
|
199
|
+
def with_instance_locked(&block)
|
200
|
+
return yield unless message.has_dependencies?
|
201
|
+
|
202
|
+
lock_options = LOCK_OPTIONS.merge(:node => master_node)
|
203
|
+
mutex = Promiscuous::Redis::Mutex.new(instance_dep.key(:sub).to_s, lock_options)
|
204
|
+
|
205
|
+
unless mutex.lock
|
206
|
+
raise Promiscuous::Error::LockUnavailable.new(mutex.key)
|
207
|
+
end
|
208
|
+
|
209
|
+
begin
|
210
|
+
yield
|
211
|
+
ensure
|
212
|
+
unless mutex.unlock
|
213
|
+
# TODO Be safe in case we have a duplicate message and lost the lock on it
|
214
|
+
raise "The subscriber lost the lock during its operation. It means that someone else\n"+
|
215
|
+
"received a duplicate message, and we got screwed.\n"
|
179
216
|
end
|
180
217
|
end
|
181
218
|
end
|
182
219
|
|
220
|
+
def execute_operations
|
221
|
+
self.operations.each(&:execute)
|
222
|
+
end
|
223
|
+
|
183
224
|
def on_message
|
184
|
-
|
185
|
-
|
225
|
+
with_instance_locked do
|
226
|
+
if Promiscuous::Config.consistency == :causal && message.has_dependencies?
|
227
|
+
self.check_duplicate_and_update_dependencies { execute_operations }
|
228
|
+
else
|
229
|
+
execute_operations
|
230
|
+
end
|
186
231
|
end
|
232
|
+
message.ack
|
187
233
|
end
|
188
234
|
|
189
235
|
def operation_class
|
@@ -2,9 +2,34 @@ module Promiscuous::Subscriber::Model::ActiveRecord
|
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
include Promiscuous::Subscriber::Model::Base
|
4
4
|
|
5
|
+
included do
|
6
|
+
if Promiscuous::Config.consistency == :eventual && !self.columns.collect(&:name).include?("_v")
|
7
|
+
raise <<-help
|
8
|
+
#{self} must include a _v column. Create the following migration:
|
9
|
+
change_table :#{self.table_name} do |t|
|
10
|
+
t.integer :_v, :limit => 8
|
11
|
+
end
|
12
|
+
help
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
5
16
|
module ClassMethods
|
6
17
|
def __promiscuous_missing_record_exception
|
7
18
|
ActiveRecord::RecordNotFound
|
8
19
|
end
|
20
|
+
|
21
|
+
def __promiscuous_duplicate_key_exception?(e)
|
22
|
+
# TODO Ensure that it's on the pk
|
23
|
+
e.is_a?(ActiveRecord::RecordNotUnique)
|
24
|
+
end
|
25
|
+
|
26
|
+
def __promiscuous_fetch_existing(id)
|
27
|
+
key = subscribe_foreign_key
|
28
|
+
if promiscuous_root_class.respond_to?("find_by_#{key}!")
|
29
|
+
promiscuous_root_class.__send__("find_by_#{key}!", id)
|
30
|
+
elsif respond_to?("find_by")
|
31
|
+
promiscuous_root_class.find_by(key => id)
|
32
|
+
end
|
33
|
+
end
|
9
34
|
end
|
10
35
|
end
|
@@ -1,6 +1,23 @@
|
|
1
1
|
module Promiscuous::Subscriber::Model::Base
|
2
2
|
extend ActiveSupport::Concern
|
3
3
|
|
4
|
+
def __promiscuous_eventual_consistency_update(operation)
|
5
|
+
return true unless Promiscuous::Config.consistency == :eventual
|
6
|
+
return true unless operation.message.has_dependencies?
|
7
|
+
|
8
|
+
version = operation.message_processor.instance_dep.version
|
9
|
+
generation = operation.message.generation
|
10
|
+
version = (generation << 50) | version
|
11
|
+
|
12
|
+
if self.attributes[Promiscuous::Config.version_field].to_i <= version
|
13
|
+
self.send("#{Promiscuous::Config.version_field}=", version)
|
14
|
+
true
|
15
|
+
else
|
16
|
+
Promiscuous.debug "[receive] out of order message #{self.class}/#{id}/g:#{generation},v:#{version}"
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
4
21
|
def __promiscuous_update(payload, options={})
|
5
22
|
self.class.subscribed_attrs.map(&:to_s).each do |attr|
|
6
23
|
unless payload.attributes.has_key?(attr)
|
@@ -84,18 +101,5 @@ module Promiscuous::Subscriber::Model::Base
|
|
84
101
|
def __promiscuous_fetch_new(id)
|
85
102
|
new.tap { |m| m.__send__("#{subscribe_foreign_key}=", id) }
|
86
103
|
end
|
87
|
-
|
88
|
-
def __promiscuous_fetch_existing(id)
|
89
|
-
key = subscribe_foreign_key
|
90
|
-
if promiscuous_root_class.respond_to?("find_by_#{key}!")
|
91
|
-
promiscuous_root_class.__send__("find_by_#{key}!", id)
|
92
|
-
elsif respond_to?("find_by")
|
93
|
-
promiscuous_root_class.find_by(key => id)
|
94
|
-
else
|
95
|
-
instance = promiscuous_root_class.where(key => id).first
|
96
|
-
raise __promiscuous_missing_record_exception.new(promiscuous_root_class, id) if instance.nil?
|
97
|
-
instance
|
98
|
-
end
|
99
|
-
end
|
100
104
|
end
|
101
105
|
end
|
@@ -8,6 +8,10 @@ module Promiscuous::Subscriber::Model::Mongoid
|
|
8
8
|
!self.embedded? || options[:old_value] != self
|
9
9
|
end
|
10
10
|
|
11
|
+
included do
|
12
|
+
field :_v if Promiscuous::Config.consistency == :eventual
|
13
|
+
end
|
14
|
+
|
11
15
|
module ClassMethods
|
12
16
|
def subscribe(*args, &block)
|
13
17
|
super
|
@@ -38,6 +42,21 @@ module Promiscuous::Subscriber::Model::Mongoid
|
|
38
42
|
def __promiscuous_missing_record_exception
|
39
43
|
Mongoid::Errors::DocumentNotFound
|
40
44
|
end
|
45
|
+
|
46
|
+
def __promiscuous_duplicate_key_exception?(e)
|
47
|
+
e.message =~ /E11000/
|
48
|
+
end
|
49
|
+
|
50
|
+
def __promiscuous_fetch_existing(id)
|
51
|
+
key = subscribe_foreign_key
|
52
|
+
if respond_to?("find_by")
|
53
|
+
promiscuous_root_class.find_by(key => id)
|
54
|
+
else
|
55
|
+
instance = promiscuous_root_class.where(key => id).first
|
56
|
+
raise __promiscuous_missing_record_exception.new(promiscuous_root_class, id) if instance.nil?
|
57
|
+
instance
|
58
|
+
end
|
59
|
+
end
|
41
60
|
end
|
42
61
|
|
43
62
|
class EmbeddedDocs
|
@@ -71,7 +90,7 @@ module Promiscuous::Subscriber::Model::Mongoid
|
|
71
90
|
# create all the new ones
|
72
91
|
new_embeddeds.reject { |new_e| new_e['existed'] }.each do |new_e|
|
73
92
|
payload = Promiscuous::Subscriber::Operation::Regular.new(new_e)
|
74
|
-
new_e_instance = payload.model.
|
93
|
+
new_e_instance = payload.model.__promiscuous_fetch_new(payload.id)
|
75
94
|
new_e_instance.__promiscuous_update(payload)
|
76
95
|
options[:parent].__send__(old_embeddeds.metadata[:name]) << new_e_instance
|
77
96
|
end
|