From b1d762675e693b4cb3fdc2e3d4da140652b6a9a8 Mon Sep 17 00:00:00 2001 From: RameshRaparthi Date: Thu, 4 Jun 2026 10:54:59 -0500 Subject: [PATCH] This Commit has code related to fixing/handling of the SaveEvent duplicate records while the mobile device gets locked, the api call is in flight and did not get the response in time. --- snd/src/org/labkey/snd/SNDManager.java | 18 ++++++++++++++++++ snd/src/org/labkey/snd/SNDServiceImpl.java | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/snd/src/org/labkey/snd/SNDManager.java b/snd/src/org/labkey/snd/SNDManager.java index 22cc1448a..f193aa7b2 100644 --- a/snd/src/org/labkey/snd/SNDManager.java +++ b/snd/src/org/labkey/snd/SNDManager.java @@ -2207,6 +2207,24 @@ public boolean eventExists(Container c, User u, int eventId) return selector.exists(); } + /** + * Returns the EventId for an existing event that was saved with the given ObjectId, or null if not found. + * Used by saveEvent for idempotent retries: when a client-generated ObjectId is provided and already + * exists in the DB, the event was already committed and should not be inserted again. + */ + public Integer getEventIdByObjectId(Container c, User u, String objectId) + { + UserSchema schema = getSndUserSchema(c, u); + + SQLFragment sql = new SQLFragment("SELECT EventId FROM "); + sql.append(schema.getTable(SNDSchema.EVENTS_TABLE_NAME), "ev"); + sql.append(" WHERE ObjectId = ?"); + sql.add(objectId); + SqlSelector selector = new SqlSelector(schema.getDbSchema(), sql); + + return selector.getObject(Integer.class); + } + /** * Get a project ObjectId given a projectId and revision in the format projectId|rev (ex. 61|1). */ diff --git a/snd/src/org/labkey/snd/SNDServiceImpl.java b/snd/src/org/labkey/snd/SNDServiceImpl.java index ff44daa53..cba3b2cdd 100644 --- a/snd/src/org/labkey/snd/SNDServiceImpl.java +++ b/snd/src/org/labkey/snd/SNDServiceImpl.java @@ -349,6 +349,24 @@ public Event saveEvent(Container c, User u, Event event, boolean validateOnly) { try (DbScope.Transaction tx = SNDSchema.getInstance().getSchema().getScope().ensureTransaction(lock)) { + // Idempotency check: if a client-generated objectId is present, look up whether + // this event was already committed (e.g. device lost the response after the server + // wrote the row). If found, override the auto-generated eventId with the existing one + // so the request falls through to the update path instead of creating a duplicate. + // Note: the Event constructor always auto-assigns an eventId via SNDSequencer even + // when the caller passes null, so checking event.getEventId() == null would never + // be true here and the idempotency check would never run. We guard only on objectId. + // For update requests the client omits objectId, so the controller supplies a random + // GUID that will never match an existing row — making this lookup a safe no-op. + if (event.getObjectId() != null) + { + Integer existingEventId = SNDManager.get().getEventIdByObjectId(c, u, event.getObjectId()); + if (existingEventId != null) + { + event.setEventId(existingEventId); + } + } + if (event.getEventId() != null && SNDManager.get().eventExists(c, u, event.getEventId())) { if ((event.getEventData() == null || event.getEventData().isEmpty()) &&