{"record":{"id":"38c54816397bf6f3","repo":"TechnitiumSoftware/DnsServer","slug":"failed-to-add-secondary-node-please-try-again","errorCode":null,"errorMessage":"Failed to add Secondary node: please try again.","messagePattern":"Failed to add Secondary node: please try again\\.","errorType":"exception","errorClass":"DnsServerException","httpStatus":null,"severity":"error","filePath":"DnsServerCore/Cluster/ClusterManager.cs","lineNumber":720,"sourceCode":"                    throw new DnsServerException(\"Failed to add Secondary node: A node with the same DNS Server Domain Name already exists in the Cluster. Please try again after changing the Secondary node's DNS Server Domain Name.\");\n            }\n\n            //add secondary node to cluster nodes\n            ClusterNode secondaryNode = new ClusterNode(this, secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Unknown);\n            Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count + 1);\n\n            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)\n                updatedClusterNodes[existingClusterNode.Value.Id] = existingClusterNode.Value;\n\n            if (!updatedClusterNodes.TryAdd(secondaryNode.Id, secondaryNode))\n                throw new DnsServerException(\"Failed to add Secondary node: node ID already exists in the Cluster. Please try again.\");\n\n            if (updatedClusterNodes.Count > 255)\n                throw new DnsServerException(\"Failed to add Secondary node: a maximum of 255 nodes are supported by the Cluster.\");\n\n            IReadOnlyDictionary<int, ClusterNode> originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes);\n            if (!ReferenceEquals(originalValue, existingClusterNodes))\n                throw new DnsServerException(\"Failed to add Secondary node: please try again.\");\n\n            secondaryNode.InitializeHeartbeatTimer();\n\n            //update cluster zone and save zone file\n            FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values\n            AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate);\n\n            //update cluster catalog zone ACLs and save zone file\n            UpdateClusterCatalogZoneOptions();\n\n            //save all changes\n            SaveConfigFile();\n\n            //notify all secondary nodes\n            TriggerNotifyAllSecondaryNodes();\n\n            //trigger NS and SOA update for member zones\n            TriggerRecordUpdateForClusterCatalogMemberZones();","sourceCodeStart":702,"sourceCodeEnd":738,"githubUrl":"https://github.com/TechnitiumSoftware/DnsServer/blob/d0484b6c1e7439cdc53d67d81e9c876cda2ad756/DnsServerCore/Cluster/ClusterManager.cs#L702-L738","documentation":"Thrown by JoinCluster when Interlocked.CompareExchange fails because _clusterNodes was modified by another thread between the initial read (existingClusterNodes snapshot) and the commit attempt. This is optimistic concurrency control — the node list is an immutable snapshot swapped atomically. The message 'please try again' indicates the operation is safe to retry.","triggerScenarios":"Two or more threads call JoinCluster or DeleteSecondaryNode concurrently. Thread A reads _clusterNodes into existingClusterNodes, builds updatedClusterNodes, but before it calls CompareExchange, thread B already swapped _clusterNodes to a new reference. The CompareExchange at line 718 returns B's reference, which is not ReferenceEquals to A's snapshot, so the guard fires.","commonSituations":"Concurrent API requests adding multiple Secondaries at once; a delete running in parallel with a join; automated orchestration issuing parallel membership changes.","solutions":["Retry the JoinCluster call — the optimistic lock failure is transient by design.","Serialize cluster membership operations (add/remove) so only one runs at a time, eliminating the race."],"exampleFix":"// before\nawait _clusterManager.JoinCluster(nodeId, nodeUrl, ips, cert);\n// after — retry on optimistic-lock conflict\nfor (int attempt = 0; attempt < 3; attempt++)\n{\n    try\n    {\n        await _clusterManager.JoinCluster(nodeId, nodeUrl, ips, cert);\n        break;\n    }\n    catch (DnsServerException ex) when (ex.Message.Contains(\"please try again\"))\n    {\n        if (attempt == 2) throw;\n        await Task.Delay(100 * (attempt + 1));\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for (int attempt = 0; attempt < 3; attempt++)\n{\n    try\n    {\n        await _clusterManager.JoinClusterAsync(nodeId, nodeUrl, nodeIps, cert);\n        break;\n    }\n    catch (DnsServerException ex) when (ex.Message.Contains(\"please try again\"))\n    {\n        if (attempt == 2) throw;\n        await Task.Delay(100 * (attempt + 1));\n    }\n}","preventionTips":["Serialize cluster membership operations (add/remove) to avoid concurrent _clusterNodes modifications.","Use a distributed lock or queue for cluster-management API calls in multi-admin environments.","Implement retry-with-backoff for optimistic-concurrency failures on JoinCluster."],"tags":["cluster","concurrency","retry","optimistic-locking","race-condition"],"backgroundTag":null,"analyzedSha":"d0484b6c1e7439cdc53d67d81e9c876cda2ad756","analyzedAt":"2026-08-13T22:57:35.508Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}