@@ -98,6 +98,16 @@ Value ApiListener::ConfigUpdateObjectAPIHandler(const MessageOrigin::Ptr& origin
9898 /* update the object */
9999 double objVersion = params->Get (" version" );
100100
101+ auto deletedVersion = listener->GetRuntimeObjectDeletionTs (objType, objName);
102+ if (deletedVersion >= objVersion) {
103+ Log (LogInformation, " ApiListener" )
104+ << " Ignoring config update for deleted object '" << objName << " ' of type '" << objType << " ' from '"
105+ << identity << " ' (endpoint: '" << endpoint->GetName () << " ', zone: '" << endpointZone->GetName ()
106+ << " '): Object version " << std::fixed << objVersion << " is less recent than the deleted version "
107+ << std::fixed << deletedVersion << " ." ;
108+ return Empty;
109+ }
110+
101111 Type::Ptr ptype = Type::GetByName (objType);
102112 auto *ctype = dynamic_cast <ConfigType *>(ptype.get ());
103113
@@ -274,6 +284,24 @@ Value ApiListener::ConfigDeleteObjectAPIHandler(const MessageOrigin::Ptr& origin
274284
275285 ConfigObject::Ptr object = ctype->GetObject (objName);
276286
287+ // Check if the deletion is for an older object version
288+ double objVersion = params->Get (" version" );
289+ if (object && objVersion < object->GetVersion ()) {
290+ Log (LogInformation, " ApiListener" )
291+ << " Ignoring 'config delete object' message"
292+ << " from '" << identity << " ' (endpoint: '" << endpoint->GetName () << " ', zone: '" << endpointZone->GetName () << " ')"
293+ << " for object '" << object->GetName ()
294+ << " ': Object version " << std::fixed << object->GetVersion ()
295+ << " is more recent than the deleted version " << std::fixed << objVersion << " ." ;
296+
297+ return Empty;
298+ }
299+
300+ /* We need to store the deletion timestamp even if no object exists, to protect against
301+ * endpoints that try to sync back objects that would have been deleted by this message.
302+ */
303+ listener->UpdateRuntimeObjectDeletionTs (objType, objName, objVersion);
304+
277305 if (!object) {
278306 Log (LogNotice, " ApiListener" )
279307 << " Could not delete non-existent object '" << objName << " ' with type '" << params->Get (" type" ) << " '." ;
@@ -437,6 +465,24 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess
437465 if (object->GetPackage () != " _api" )
438466 return ;
439467
468+ auto typeName = object->GetReflectionType ()->GetName ();
469+ auto objName = object->GetName ();
470+
471+ /* Update the deletion timestamp only if the deletion didn't originate from another
472+ * endpoint, because when this deletion was triggered via a JSON-RPC message, the
473+ * stored timestamp is the one that was set in `ApiListener::ConfigDeleteObjectAPIHandler`
474+ * and already reflects the time the original deletion was triggered.
475+ * Also set if no timestamp was stored yet, as a safeguard. There shouldn't be any
476+ * situation where this happens, but that relies on the correct execution order of
477+ * many other functions, like cascading deletes always propagating in the correct
478+ * order through JSON-RPC, so the children are always deleted first.
479+ */
480+ double deletionTime = GetRuntimeObjectDeletionTs (typeName, objName);
481+ if (!origin || origin->IsLocal () || deletionTime == 0 ) {
482+ deletionTime = Utility::GetTime ();
483+ UpdateRuntimeObjectDeletionTs (typeName, objName, deletionTime);
484+ }
485+
440486 /* only send objects to zones which have access to the object */
441487 if (client) {
442488 Zone::Ptr target_zone = client->GetEndpoint ()->GetZone ();
@@ -460,7 +506,7 @@ void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const Mess
460506
461507 params->Set (" name" , object->GetName ());
462508 params->Set (" type" , object->GetReflectionType ()->GetName ());
463- params->Set (" version" , object-> GetVersion () );
509+ params->Set (" version" , deletionTime );
464510
465511
466512#ifdef I2_DEBUG
@@ -508,3 +554,79 @@ void ApiListener::SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient
508554 Log (LogInformation, " ApiListener" )
509555 << " Finished syncing runtime objects to endpoint '" << endpoint->GetName () << " '." ;
510556}
557+
558+ /* *
559+ * Prunes the list of deleted runtime objects.
560+ *
561+ * This takes into account the longest log duration of all the endpoints in the cluster and
562+ * then deletes the entries for objects that are older than that.
563+ *
564+ * The reasoning is that once a deletion is older than log duration of an endpoint it is
565+ * unlikely that we could still get any delayed updates to the object versions that were deleted.
566+ * At that point, either all endpoints are up-to-date, or the problematic messages have been
567+ * dropped from the replay log anyway.
568+ */
569+ void ApiListener::PruneDeletedRuntimeObjects ()
570+ {
571+ auto deletedRuntimeObjects = GetDeletedRuntimeObjects ();
572+
573+ double maxLogDuration = 0 ;
574+ for (const auto &endpoint : ConfigType::GetObjectsByType<Endpoint>()) {
575+ maxLogDuration = std::max (maxLogDuration, endpoint->GetLogDuration ());
576+ }
577+ double cutoff = Utility::GetTime () - maxLogDuration;
578+
579+ ObjectLock lock (deletedRuntimeObjects);
580+
581+ for (auto it = deletedRuntimeObjects->Begin (); it != deletedRuntimeObjects->End ();) {
582+ if (it->second < cutoff) {
583+ it = deletedRuntimeObjects->Remove (it);
584+ } else {
585+ it++;
586+ };
587+ }
588+ }
589+
590+ static String MakeDeletionTimestampKey (const String& typeName, const String& objName)
591+ {
592+ return typeName + " :" + objName;
593+ }
594+
595+ /* *
596+ * Get the timestamp at which the object has been most recently deleted.
597+ *
598+ * @param typeName The name of the tyope of the object
599+ * @param objName The name of the object
600+ * @return the timestamp of the object, or 0 if there was no entry.
601+ */
602+ double ApiListener::GetRuntimeObjectDeletionTs (const String& typeName, const String& objName)
603+ {
604+ auto dict = GetDeletedRuntimeObjects ();
605+ auto ts = dict->Get (MakeDeletionTimestampKey (typeName, objName));
606+ if (ts.IsNumber ()) {
607+ return ts.Get <double >();
608+ }
609+ return 0 ;
610+ }
611+
612+ /* *
613+ * Updates the deletion timestamp for the object.
614+ *
615+ * The timestamp will not be changed in case a newer timestamp has already been stored
616+ * for the object.
617+ *
618+ * @param typeName The name of the tyope of the object
619+ * @param objName The name of the object
620+ * @param ts the timestamp
621+ */
622+ bool ApiListener::UpdateRuntimeObjectDeletionTs (const String& typeName, const String& objName, double ts)
623+ {
624+ auto dict = GetDeletedRuntimeObjects ();
625+ ObjectLock lock (dict);
626+ auto current = GetRuntimeObjectDeletionTs (typeName, objName);
627+ if (current > ts) {
628+ return false ;
629+ }
630+ dict->Set (MakeDeletionTimestampKey (typeName, objName), ts);
631+ return true ;
632+ }
0 commit comments