blob: be87f5c006964ff5ea5426624b5205d88a09ea27 [file] [log] [blame]
Zsolt Haraszti00d9a842016-11-23 11:18:23 -08001#
2# Copyright 2016 the original author or authors.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
17"""
183-way merge function for config rev objects.
19"""
20from collections import OrderedDict
21from copy import copy
22
23from voltha.core.config.config_proxy import CallbackType, OperationContext
24from voltha.core.config.config_rev import children_fields
25
26
27class MergeConflictException(Exception):
28 pass
29
30
31def merge_3way(fork_rev, src_rev, dst_rev, merge_child_func, dry_run=False):
32 """
33 Attempt to merge src_rev into dst_rev but taking into account what have
34 changed in both revs since the last known common point, the fork_rev.
35 In case of conflict, raise a MergeConflictException(). If dry run is True,
36 don't actually perform the merge, but detect potential conflicts.
37
38 This function recurses into all children nodes stored under the rev and
39 performs the merge if the children is also part of a transaction branch.
40
41 :param fork_rev: Point of forking (last known common state between branches
42 :param src_rev: Latest rev from which we merge to dst_rev
43 :param dst_rev: Target (destination) rev
44 :param merge_child_fun: To run a potential merge in all children that
45 may need merge (determined from the local changes)
46 :param dry_run: If True, do not perform the merge, but detect merge
47 conflicts.
48 :return: The new dst_rev (a new rev instance) the list of changes that
49 occurred in this node or any of its children as part of this merge.
50 """
51
52 # to collect change tuples of (<callback-type>, <op-context>)
53 changes = []
54
55 class AnalyzeChanges(object):
56 def __init__(self, lst1, lst2, keyname):
57 self.keymap1 = OrderedDict((getattr(rev._config._data, keyname), i)
58 for i, rev in enumerate(lst1))
59 self.keymap2 = OrderedDict((getattr(rev._config._data, keyname), i)
60 for i, rev in enumerate(lst2))
61 self.added_keys = [
62 k for k in self.keymap2.iterkeys() if k not in self.keymap1]
63 self.removed_keys = [
64 k for k in self.keymap1.iterkeys() if k not in self.keymap2]
65 self.changed_keys = [
66 k for k in self.keymap1.iterkeys()
67 if k in self.keymap2 and
68 lst1[self.keymap1[k]]._hash != lst2[self.keymap2[k]]._hash
69 ]
70
71 # Note: there are a couple of special cases that can be optimized
72 # for larer on. But since premature optimization is a bad idea, we
73 # defer them.
74
75 # deal with config data first
76 if dst_rev._config is fork_rev._config:
77 # no change in master, accept src if different
78 config_changed = dst_rev._config != src_rev._config
79 else:
80 if dst_rev._config.hash != src_rev._config.hash:
81 raise MergeConflictException('Config collision')
82 config_changed = True
83
84 # now to the external children fields
85 new_children = dst_rev._children.copy()
86 _children_fields = children_fields(fork_rev.data.__class__)
87
88 for field_name, field in _children_fields.iteritems():
89
90 fork_list = fork_rev._children[field_name]
91 src_list = src_rev._children[field_name]
92 dst_list = dst_rev._children[field_name]
93
94 if dst_list == src_list:
95 # we do not need to change the dst, however we still need
96 # to complete the branch purging in child nodes so not
97 # to leave dangling branches around
98 [merge_child_func(rev) for rev in src_list]
99 continue
100
101 if not field.key:
102 # If the list is not keyed, we really should not merge. We merely
103 # check for collision, i.e., if both changed (and not same)
104 if dst_list == fork_list:
105 # dst branch did not change since fork
106
107 assert src_list != fork_list, 'We should not be here otherwise'
108
109 # the incoming (src) rev changed, and we have to apply it
110 new_children[field_name] = [
111 merge_child_func(rev) for rev in src_list]
112
113 if field.is_container:
114 changes.append((CallbackType.POST_LISTCHANGE,
115 OperationContext(field_name=field_name)))
116
117 else:
118 if src_list != fork_list:
119 raise MergeConflictException(
120 'Cannot merge because single child node or un-keyed'
121 'children list has changed')
122
123 else:
124
125 if dst_list == fork_list:
126 # Destination did not change
127
128 # We need to analyze only the changes on the incoming rev
129 # since fork
130 src = AnalyzeChanges(fork_list, src_list, field.key)
131
132 new_list = copy(src_list) # we start from the source list
133
134 for key in src.added_keys:
135 idx = src.keymap2[key]
136 new_rev = merge_child_func(new_list[idx])
137 new_list[idx] = new_rev
138 changes.append(
139 (CallbackType.POST_ADD,
140 new_rev.data))
141 # OperationContext(
142 # field_name=field_name,
143 # child_key=key,
144 # data=new_rev.data)))
145
146 for key in src.removed_keys:
147 old_rev = fork_list[src.keymap1[key]]
148 changes.append((
149 CallbackType.POST_REMOVE,
150 old_rev.data))
151 # OperationContext(
152 # field_name=field_name,
153 # child_key=key,
154 # data=old_rev.data)))
155
156 for key in src.changed_keys:
157 idx = src.keymap2[key]
158 new_rev = merge_child_func(new_list[idx])
159 new_list[idx] = new_rev
160 # updated child gets its own change event
161
162 new_children[field_name] = new_list
163
164 else:
165
166 # For keyed fields we can really investigate what has been
167 # added, removed, or changed in both branches and do a
168 # fine-grained collision detection and merge
169
170 src = AnalyzeChanges(fork_list, src_list, field.key)
171 dst = AnalyzeChanges(fork_list, dst_list, field.key)
172
173 new_list = copy(dst_list) # this time we start with the dst
174
175 for key in src.added_keys:
176 # we cannot add if it has been added and is different
177 if key in dst.added_keys:
178 # it has been added to both, we need to check if
179 # they are the same
180 child_dst_rev = dst_list[dst.keymap2[key]]
181 child_src_rev = src_list[src.keymap2[key]]
182 if child_dst_rev.hash == child_src_rev.hash:
183 # they match, so we do not need to change the
184 # dst list, but we still need to purge the src
185 # branch
186 merge_child_func(child_dst_rev)
187 else:
188 raise MergeConflictException(
189 'Cannot add because it has been added and '
190 'different'
191 )
192 else:
193 # this is a brand new key, need to add it
194 new_rev = merge_child_func(src_list[src.keymap2[key]])
195 new_list.append(new_rev)
196 changes.append((
197 CallbackType.POST_ADD,
198 new_rev.data))
199 # OperationContext(
200 # field_name=field_name,
201 # child_key=key,
202 # data=new_rev.data)))
203
204 for key in src.changed_keys:
205 # we cannot change if it was removed in dst
206 if key in dst.removed_keys:
207 raise MergeConflictException(
208 'Cannot change because it has been removed')
209
210 # if it changed in dst as well, we need to check if they
211 # match (same change
212 elif key in dst.changed_keys:
213 child_dst_rev = dst_list[dst.keymap2[key]]
214 child_src_rev = src_list[src.keymap2[key]]
215 if child_dst_rev.hash == child_src_rev.hash:
216 # they match, so we do not need to change the
217 # dst list, but we still need to purge the src
218 # branch
219 merge_child_func(child_src_rev)
220 elif child_dst_rev._config.hash != child_src_rev._config.hash:
221 raise MergeConflictException(
222 'Cannot update because it has been changed and '
223 'different'
224 )
225 else:
226 new_rev = merge_child_func(
227 src_list[src.keymap2[key]])
228 new_list[dst.keymap2[key]] = new_rev
229 # no announcement for child update
230
231 else:
232 # it only changed in src branch
233 new_rev = merge_child_func(src_list[src.keymap2[key]])
234 new_list[dst.keymap2[key]] = new_rev
235 # no announcement for child update
236
237 for key in reversed(src.removed_keys): # we go from highest
238 # index to lowest
239
240 # we cannot remove if it has changed in dst
241 if key in dst.changed_keys:
242 raise MergeConflictException(
243 'Cannot remove because it has changed')
244
245 # if it has not been removed yet from dst, then remove it
246 if key not in dst.removed_keys:
247 dst_idx = dst.keymap2[key]
248 old_rev = new_list.pop(dst_idx)
249 changes.append((
250 CallbackType.POST_REMOVE,
251 old_rev.data))
252 # OperationContext(
253 # field_name=field_name,
254 # child_key=key,
255 # data=old_rev.data)))
256
257 new_children[field_name] = new_list
258
259 if not dry_run:
260 rev = src_rev if config_changed else dst_rev
261 rev = rev.update_all_children(new_children, dst_rev._branch)
262 if config_changed:
263 changes.append((CallbackType.POST_UPDATE, rev.data))
264 return rev, changes
265
266 else:
267 return None, None