@@ -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
@@ -287,6 +294,11 @@ Value ApiListener::ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin
287294 return Empty;
288295 }
289296
297+ /* We need to store the deletion timestamp even if no object exists, to protect against
298+ * endpoints that try to sync back objects that would have been deleted by this message.
299+ */
300+ listener->UpdateRuntimeObjectDeletionTs (objType, objName, objVersion);
301+
290302 if (!object) {
291303 Log (LogNotice, " ApiListener" )
292304 << " Could not delete non-existent object '" << objName << " ' with type '" << params->Get (" type" ) << " '." ;
@@ -450,6 +462,24 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess
450462 if (object->GetPackage () != " _api" )
451463 return ;
452464
465+ auto typeName = object->GetReflectionType ()->GetName ();
466+ auto objName = object->GetName ();
467+
468+ /* Update the deletion timestamp only if the deletion didn't originate from another
469+ * endpoint, because when this deletion was triggered via a JSON-RPC message, the
470+ * stored timestamp is the one that was set in `ApiListener::ConfigDeleteObjectAPIHandler`
471+ * and already reflects the time the original deletion was triggered.
472+ * Also set if no timestamp was stored yet, as a safeguard. There shouldn't be any
473+ * situation where this happens, but that relies on the correct execution order of
474+ * many other functions, like cascading deletes always propagating in the correct
475+ * order through JSON-RPC, so the children are always deleted first.
476+ */
477+ double deletionTime = GetRuntimeObjectDeletionTs (typeName, objName);
478+ if (!origin || origin->IsLocal () || deletionTime == 0 ) {
479+ deletionTime = Utility::GetTime ();
480+ UpdateRuntimeObjectDeletionTs (typeName, objName, deletionTime);
481+ }
482+
453483 /* only send objects to zones which have access to the object */
454484 if (client) {
455485 Zone::Ptr target_zone = client->GetEndpoint ()->GetZone ();
@@ -473,7 +503,7 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess
473503
474504 params->Set (" name" , object->GetName ());
475505 params->Set (" type" , object->GetReflectionType ()->GetName ());
476- params->Set (" version" , object-> GetVersion () );
506+ params->Set (" version" , deletionTime );
477507
478508
479509#ifdef I2_DEBUG
@@ -521,3 +551,47 @@ void ApiListener::SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient
521551 Log (LogInformation, " ApiListener" )
522552 << " Finished syncing runtime objects to endpoint '" << endpoint->GetName () << " '." ;
523553}
554+
555+ static String MakeDeletionTimestampKey (const String& typeName, const String& objName)
556+ {
557+ return typeName + " :" + objName;
558+ }
559+
560+ /* *
561+ * Get the timestamp at which the object has been most recently deleted.
562+ *
563+ * @param typeName The name of the tyope of the object
564+ * @param objName The name of the object
565+ * @return the timestamp of the object, or 0 if there was no entry.
566+ */
567+ double ApiListener::GetRuntimeObjectDeletionTs (const String& typeName, const String& objName)
568+ {
569+ auto dict = GetDeletedRuntimeObjects ();
570+ auto ts = dict->Get (MakeDeletionTimestampKey (typeName, objName));
571+ if (ts.IsNumber ()) {
572+ return ts.Get <double >();
573+ }
574+ return 0 ;
575+ }
576+
577+ /* *
578+ * Updates the deletion timestamp for the object.
579+ *
580+ * The timestamp will not be changed in case a newer timestamp has already been stored
581+ * for the object.
582+ *
583+ * @param typeName The name of the tyope of the object
584+ * @param objName The name of the object
585+ * @param ts the timestamp
586+ */
587+ bool ApiListener::UpdateRuntimeObjectDeletionTs (const String& typeName, const String& objName, double ts)
588+ {
589+ auto dict = GetDeletedRuntimeObjects ();
590+ ObjectLock lock (dict);
591+ auto current = GetRuntimeObjectDeletionTs (typeName, objName);
592+ if (current > ts) {
593+ return false ;
594+ }
595+ dict->Set (MakeDeletionTimestampKey (typeName, objName), ts);
596+ return true ;
597+ }
0 commit comments