@@ -98,6 +98,13 @@ Value ApiListener::ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin
9898 /* update the object */
9999 double objVersion = params->Get (" version" );
100100
101+ if (listener->GetRuntimeObjectDeletionTs (objType, objName) >= objVersion) {
102+ Log (LogInformation, " ApiListener" )
103+ << " Ignoring config update for deleted object '" << objName << " ' of type '" << objType << " ' from '"
104+ << identity << " ' (endpoint: '" << endpoint->GetName () << " ', zone: '" << endpointZone->GetName () << " ')" ;
105+ return Empty;
106+ }
107+
101108 Type::Ptr ptype = Type::GetByName (objType);
102109 auto *ctype = dynamic_cast <ConfigType *>(ptype.get ());
103110
@@ -274,6 +281,12 @@ Value ApiListener::ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin
274281
275282 ConfigObject::Ptr object = ctype->GetObject (objName);
276283
284+ if (!object) {
285+ Log (LogNotice, " ApiListener" )
286+ << " Could not delete non-existent object '" << objName << " ' with type '" << params->Get (" type" ) << " '." ;
287+ return Empty;
288+ }
289+
277290 // Check if the deletion is for an older object version
278291 double objVersion = params->Get (" version" );
279292 if (object && objVersion < object->GetVersion ()) {
@@ -287,6 +300,11 @@ Value ApiListener::ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin
287300 return Empty;
288301 }
289302
303+ /* We need to store the deletion timestamp even if no object exists, to protect against
304+ * endpoints that try to sync back objects that would have been deleted by this message.
305+ */
306+ listener->UpdateRuntimeObjectDeletionTs (objType, objName, objVersion);
307+
290308 if (!object) {
291309 Log (LogNotice, " ApiListener" )
292310 << " Could not delete non-existent object '" << objName << " ' with type '" << params->Get (" type" ) << " '." ;
@@ -450,6 +468,24 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess
450468 if (object->GetPackage () != " _api" )
451469 return ;
452470
471+ auto typeName = object->GetReflectionType ()->GetName ();
472+ auto objName = object->GetName ();
473+
474+ /* Update the deletion timestamp only if the deletion didn't originate from another
475+ * endpoint, because when this deletion was triggered via a JSON-RPC message, the
476+ * stored timestamp is the one that was set in `ApiListener::ConfigDeleteObjectAPIHandler`
477+ * and already reflects the time the original deletion was triggered.
478+ * Also set if no timestamp was stored yet, as a safeguard. There shouldn't be any
479+ * situation where this happens, but that relies on the correct execution order of
480+ * many other functions, like cascading deletes always propagating in the correct
481+ * order through JSON-RPC, so the children are always deleted first.
482+ */
483+ double deletionTime = GetRuntimeObjectDeletionTs (typeName, objName);
484+ if (!origin || origin->IsLocal () || deletionTime == 0 ) {
485+ deletionTime = Utility::GetTime ();
486+ UpdateRuntimeObjectDeletionTs (typeName, objName, deletionTime);
487+ }
488+
453489 /* only send objects to zones which have access to the object */
454490 if (client) {
455491 Zone::Ptr target_zone = client->GetEndpoint ()->GetZone ();
@@ -473,7 +509,7 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess
473509
474510 params->Set (" name" , object->GetName ());
475511 params->Set (" type" , object->GetReflectionType ()->GetName ());
476- params->Set (" version" , object-> GetVersion () );
512+ params->Set (" version" , deletionTime );
477513
478514
479515#ifdef I2_DEBUG
@@ -521,3 +557,47 @@ void ApiListener::SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient
521557 Log (LogInformation, " ApiListener" )
522558 << " Finished syncing runtime objects to endpoint '" << endpoint->GetName () << " '." ;
523559}
560+
561+ static String MakeDeletionTimestampKey (const String& typeName, const String& objName)
562+ {
563+ return typeName + " :" + objName;
564+ }
565+
566+ /* *
567+ * Get the timestamp at which the object has been most recently deleted.
568+ *
569+ * @param typeName The name of the tyope of the object
570+ * @param objName The name of the object
571+ * @return the timestamp of the object, or 0 if there was no entry.
572+ */
573+ double ApiListener::GetRuntimeObjectDeletionTs (const String& typeName, const String& objName)
574+ {
575+ auto dict = GetDeletedRuntimeObjects ();
576+ auto ts = dict->Get (MakeDeletionTimestampKey (typeName, objName));
577+ if (ts.IsNumber ()) {
578+ return ts.Get <double >();
579+ }
580+ return 0 ;
581+ }
582+
583+ /* *
584+ * Updates the deletion timestamp for the object.
585+ *
586+ * The timestamp will not be changed in case a newer timestamp has already been stored
587+ * for the object.
588+ *
589+ * @param typeName The name of the tyope of the object
590+ * @param objName The name of the object
591+ * @param ts the timestamp
592+ */
593+ bool ApiListener::UpdateRuntimeObjectDeletionTs (const String& typeName, const String& objName, double ts)
594+ {
595+ auto dict = GetDeletedRuntimeObjects ();
596+ ObjectLock lock (dict);
597+ auto current = GetRuntimeObjectDeletionTs (typeName, objName);
598+ if (current > ts) {
599+ return false ;
600+ }
601+ dict->Set (MakeDeletionTimestampKey (typeName, objName), ts);
602+ return true ;
603+ }
0 commit comments