Skip to content

Commit 398fc95

Browse files
authored
Merge pull request #316 from bjester/des-exc
Add deserialization_exception field for programmatic error handling
2 parents 5a9e53d + 75c1f8c commit 398fc95

15 files changed

Lines changed: 530 additions & 238 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
List of the most important changes for each release.
44

5+
## 0.8.11
6+
- Adds additional `deserialization_exception` field to `Store` model to track the fully qualified exception path
7+
58
## 0.8.10
69
- Fixes silent failure during deserialization of records that fail unique constraints
710

morango/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.8.10"
1+
__version__ = "0.8.11"

morango/errors.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
from morango.utils import exception_path
2+
3+
14
class MorangoError(Exception):
2-
pass
5+
@classmethod
6+
def path(cls):
7+
return exception_path(cls)
38

49

510
class ModelRegistryNotReady(MorangoError):
@@ -80,3 +85,11 @@ class MorangoSkipOperation(MorangoError):
8085

8186
class MorangoDatabaseError(MorangoError):
8287
pass
88+
89+
90+
class MorangoDirtyParent(MorangoError):
91+
pass
92+
93+
94+
class MorangoMissingParent(MorangoError):
95+
pass
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 3.2.25 on 2026-04-14 15:54
2+
from django.db import migrations
3+
from django.db import models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("morango", "0002_store_idx_morango_deserialize"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="store",
15+
name="deserialization_exception",
16+
field=models.CharField(blank=True, max_length=255, null=True),
17+
),
18+
migrations.AlterField(
19+
model_name="store",
20+
name="deserialization_error",
21+
field=models.TextField(blank=True, null=True),
22+
),
23+
]

morango/models/core.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@
4141
from morango.models.utils import get_0_5_system_id
4242
from morango.registry import syncable_models
4343
from morango.utils import _assert
44+
from morango.utils import exception_path
4445
from morango.utils import SETTINGS
4546

47+
4648
logger = logging.getLogger(__name__)
4749

4850

@@ -439,7 +441,8 @@ class Store(AbstractStore):
439441
id = UUIDField(primary_key=True)
440442
# used to know which store records need to be deserialized into the app layer models
441443
dirty_bit = models.BooleanField(default=False)
442-
deserialization_error = models.TextField(blank=True)
444+
deserialization_error = models.TextField(blank=True, null=True)
445+
deserialization_exception = models.CharField(max_length=255, blank=True, null=True)
443446

444447
last_transfer_session_id = UUIDField(
445448
blank=True, null=True, default=None, db_index=True
@@ -453,6 +456,14 @@ class Meta:
453456
models.Index(fields=["profile", "model_name", "partition", "dirty_bit"], condition=models.Q(dirty_bit=True), name="idx_morango_deserialize"),
454457
]
455458

459+
def set_deserialization_error(self, exc):
460+
self.deserialization_error = str(exc)
461+
self.deserialization_exception = exception_path(exc)
462+
463+
def unset_deserialization_error(self):
464+
self.deserialization_error = None
465+
self.deserialization_exception = None
466+
456467
def _deserialize_store_model(self, fk_cache, defer_fks=False, sync_filter=None): # noqa: C901
457468
"""
458469
When deserializing a store model, we look at the deleted flags to know if we should delete the app model.

morango/sync/backends/postgres.py

Lines changed: 128 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -203,22 +203,53 @@ def _dequeuing_merge_conflict_rmcb(self, cursor, transfersession_id):
203203

204204
def _dequeuing_merge_conflict_buffer(self, cursor, current_id, transfersession_id):
205205
# transfer buffer serialized into conflicting store
206-
merge_conflict_store = """UPDATE {store} as store SET (serialized, deleted, last_saved_instance, last_saved_counter, hard_deleted, model_name,
207-
profile, partition, source_id, conflicting_serialized_data, dirty_bit, _self_ref_fk, deserialization_error, last_transfer_session_id)
208-
= (CASE buffer.hard_deleted WHEN TRUE THEN '' ELSE store.serialized END, store.deleted OR buffer.deleted, '{current_instance_id}',
209-
{current_instance_counter}, store.hard_deleted, store.model_name, store.profile, store.partition, store.source_id,
210-
CASE buffer.hard_deleted WHEN TRUE THEN '' ELSE buffer.serialized || '\n' || store.conflicting_serialized_data END, TRUE, store._self_ref_fk,
211-
'', '{transfer_session_id}')
212-
/*Scope to a single record.*/
213-
FROM {buffer} AS buffer
214-
WHERE store.id = buffer.model_uuid
215-
AND buffer.transfer_session_id = '{transfer_session_id}'
216-
/*Exclude fast-forwards*/
217-
AND NOT EXISTS (SELECT 1 FROM {rmcb} AS rmcb2 WHERE store.id = rmcb2.model_uuid
218-
AND store.last_saved_instance = rmcb2.instance_id
219-
AND store.last_saved_counter <= rmcb2.counter
220-
AND rmcb2.transfer_session_id = '{transfer_session_id}')
221-
""".format(
206+
merge_conflict_store = """
207+
UPDATE {store} as store SET (
208+
serialized,
209+
deleted,
210+
last_saved_instance,
211+
last_saved_counter,
212+
hard_deleted,
213+
model_name,
214+
profile,
215+
partition,
216+
source_id,
217+
conflicting_serialized_data,
218+
dirty_bit,
219+
_self_ref_fk,
220+
deserialization_error,
221+
deserialization_exception,
222+
last_transfer_session_id
223+
) = (
224+
CASE buffer.hard_deleted WHEN TRUE THEN '' ELSE store.serialized END,
225+
store.deleted OR buffer.deleted,
226+
'{current_instance_id}',
227+
{current_instance_counter},
228+
store.hard_deleted,
229+
store.model_name,
230+
store.profile,
231+
store.partition,
232+
store.source_id,
233+
CASE buffer.hard_deleted WHEN TRUE THEN '' ELSE buffer.serialized || '\n' || store.conflicting_serialized_data END,
234+
TRUE,
235+
store._self_ref_fk,
236+
NULL,
237+
NULL,
238+
'{transfer_session_id}'
239+
)
240+
/*Scope to a single record.*/
241+
FROM {buffer} AS buffer
242+
WHERE store.id = buffer.model_uuid
243+
AND buffer.transfer_session_id = '{transfer_session_id}'
244+
/*Exclude fast-forwards*/
245+
AND NOT EXISTS (
246+
SELECT 1 FROM {rmcb} AS rmcb2
247+
WHERE store.id = rmcb2.model_uuid
248+
AND store.last_saved_instance = rmcb2.instance_id
249+
AND store.last_saved_counter <= rmcb2.counter
250+
AND rmcb2.transfer_session_id = '{transfer_session_id}'
251+
)
252+
""".format(
222253
buffer=Buffer._meta.db_table,
223254
rmcb=RecordMaxCounterBuffer._meta.db_table,
224255
store=Store._meta.db_table,
@@ -277,27 +308,96 @@ def _dequeuing_insert_remaining_buffer(self, cursor, transfersession_id):
277308
insert_remaining_buffer = """
278309
WITH new_values as
279310
(
280-
SELECT buffer.model_uuid, buffer.serialized, buffer.deleted, buffer.last_saved_instance, buffer.last_saved_counter, buffer.hard_deleted,
281-
buffer.model_name, buffer.profile, buffer.partition, buffer.source_id, buffer.conflicting_serialized_data, buffer._self_ref_fk
311+
SELECT
312+
buffer.model_uuid,
313+
buffer.serialized,
314+
buffer.deleted,
315+
buffer.last_saved_instance,
316+
buffer.last_saved_counter,
317+
buffer.hard_deleted,
318+
buffer.model_name,
319+
buffer.profile,
320+
buffer.partition,
321+
buffer.source_id,
322+
buffer.conflicting_serialized_data,
323+
buffer._self_ref_fk
282324
FROM {buffer} as buffer
283325
WHERE buffer.transfer_session_id = '{transfer_session_id}'
284326
),
285327
updated as
286328
(
287-
UPDATE {store} store SET (serialized, deleted, last_saved_instance, last_saved_counter, hard_deleted, model_name, profile,
288-
partition, source_id, conflicting_serialized_data, dirty_bit, _self_ref_fk, deserialization_error, last_transfer_session_id)
289-
= (nv.serialized, nv.deleted, nv.last_saved_instance, nv.last_saved_counter, nv.hard_deleted,
290-
nv.model_name, nv.profile, nv.partition, nv.source_id, nv.conflicting_serialized_data, TRUE,
291-
nv._self_ref_fk, '', '{transfer_session_id}')
329+
UPDATE {store} store SET (
330+
serialized,
331+
deleted,
332+
last_saved_instance,
333+
last_saved_counter,
334+
hard_deleted,
335+
model_name,
336+
profile,
337+
partition,
338+
source_id,
339+
conflicting_serialized_data,
340+
dirty_bit,
341+
_self_ref_fk,
342+
deserialization_error,
343+
deserialization_exception,
344+
last_transfer_session_id
345+
) = (
346+
nv.serialized,
347+
nv.deleted,
348+
nv.last_saved_instance,
349+
nv.last_saved_counter,
350+
nv.hard_deleted,
351+
nv.model_name,
352+
nv.profile,
353+
nv.partition,
354+
nv.source_id,
355+
nv.conflicting_serialized_data,
356+
TRUE,
357+
nv._self_ref_fk,
358+
NULL,
359+
NULL,
360+
'{transfer_session_id}'
361+
)
292362
FROM new_values nv
293363
WHERE nv.model_uuid = store.id
294364
returning store.*
295365
)
296-
INSERT INTO {store}(id, serialized, deleted, last_saved_instance, last_saved_counter, hard_deleted, model_name, profile,
297-
partition, source_id, conflicting_serialized_data, dirty_bit, _self_ref_fk, deserialization_error, last_transfer_session_id)
298-
SELECT ut.model_uuid, ut.serialized, ut.deleted, ut.last_saved_instance, ut.last_saved_counter, ut.hard_deleted,
299-
ut.model_name, ut.profile, ut.partition, ut.source_id, ut.conflicting_serialized_data, TRUE,
300-
ut._self_ref_fk, '', '{transfer_session_id}'
366+
INSERT INTO {store}(
367+
id,
368+
serialized,
369+
deleted,
370+
last_saved_instance,
371+
last_saved_counter,
372+
hard_deleted,
373+
model_name,
374+
profile,
375+
partition,
376+
source_id,
377+
conflicting_serialized_data,
378+
dirty_bit,
379+
_self_ref_fk,
380+
deserialization_error,
381+
deserialization_exception,
382+
last_transfer_session_id
383+
)
384+
SELECT
385+
ut.model_uuid,
386+
ut.serialized,
387+
ut.deleted,
388+
ut.last_saved_instance,
389+
ut.last_saved_counter,
390+
ut.hard_deleted,
391+
ut.model_name,
392+
ut.profile,
393+
ut.partition,
394+
ut.source_id,
395+
ut.conflicting_serialized_data,
396+
TRUE,
397+
ut._self_ref_fk,
398+
NULL,
399+
NULL,
400+
'{transfer_session_id}'
301401
FROM new_values ut
302402
WHERE ut.model_uuid not in (SELECT id FROM updated)
303403
""".format(

0 commit comments

Comments
 (0)