TechnitiumSoftware/DnsServer · error · DnsServerException

Failed to add Secondary node: please try again.

Error message

Failed to add Secondary node: please try again.

What it means

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.

Source

Thrown at DnsServerCore/Cluster/ClusterManager.cs:720

                    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.");
            }

            //add secondary node to cluster nodes
            ClusterNode secondaryNode = new ClusterNode(this, secondaryNodeId, secondaryNodeUrl, secondaryNodeIpAddresses, ClusterNodeType.Secondary, ClusterNodeState.Unknown);
            Dictionary<int, ClusterNode> updatedClusterNodes = new Dictionary<int, ClusterNode>(existingClusterNodes.Count + 1);

            foreach (KeyValuePair<int, ClusterNode> existingClusterNode in existingClusterNodes)
                updatedClusterNodes[existingClusterNode.Value.Id] = existingClusterNode.Value;

            if (!updatedClusterNodes.TryAdd(secondaryNode.Id, secondaryNode))
                throw new DnsServerException("Failed to add Secondary node: node ID already exists in the Cluster. Please try again.");

            if (updatedClusterNodes.Count > 255)
                throw new DnsServerException("Failed to add Secondary node: a maximum of 255 nodes are supported by the Cluster.");

            IReadOnlyDictionary<int, ClusterNode> originalValue = Interlocked.CompareExchange(ref _clusterNodes, updatedClusterNodes, existingClusterNodes);
            if (!ReferenceEquals(originalValue, existingClusterNodes))
                throw new DnsServerException("Failed to add Secondary node: please try again.");

            secondaryNode.InitializeHeartbeatTimer();

            //update cluster zone and save zone file
            FindExistingRecordTtlValues(out uint nsTtl, out uint aTtl); //find existing record TTL values
            AddClusterPrimaryZoneRecordsFor(secondaryNode, nsTtl, aTtl, secondaryNodeCertificate);

            //update cluster catalog zone ACLs and save zone file
            UpdateClusterCatalogZoneOptions();

            //save all changes
            SaveConfigFile();

            //notify all secondary nodes
            TriggerNotifyAllSecondaryNodes();

            //trigger NS and SOA update for member zones
            TriggerRecordUpdateForClusterCatalogMemberZones();

View on GitHub (pinned to d0484b6c1e)

Solutions

  1. Retry the JoinCluster call — the optimistic lock failure is transient by design.
  2. Serialize cluster membership operations (add/remove) so only one runs at a time, eliminating the race.

Example fix

// before
await _clusterManager.JoinCluster(nodeId, nodeUrl, ips, cert);
// after — retry on optimistic-lock conflict
for (int attempt = 0; attempt < 3; attempt++)
{
    try
    {
        await _clusterManager.JoinCluster(nodeId, nodeUrl, ips, cert);
        break;
    }
    catch (DnsServerException ex) when (ex.Message.Contains("please try again"))
    {
        if (attempt == 2) throw;
        await Task.Delay(100 * (attempt + 1));
    }
}
Defensive patterns

Strategy: retry

Try / catch

for (int attempt = 0; attempt < 3; attempt++)
{
    try
    {
        await _clusterManager.JoinClusterAsync(nodeId, nodeUrl, nodeIps, cert);
        break;
    }
    catch (DnsServerException ex) when (ex.Message.Contains("please try again"))
    {
        if (attempt == 2) throw;
        await Task.Delay(100 * (attempt + 1));
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: Concurrent API requests adding multiple Secondaries at once; a delete running in parallel with a join; automated orchestration issuing parallel membership changes.

Related errors


AI-assisted analysis of TechnitiumSoftware/DnsServer@d0484b6c1e (2026-08-13). Data as JSON: /api/errors/38c54816397bf6f3. Report an issue: GitHub.