Skip to content

Commit d5f586c

Browse files
Merge pull request #858 from erikdarlingdata/fix/857-azure-master-fallback
Fall back to single-DB mode when Azure master is inaccessible (#857)
2 parents 41f182f + 3e950a5 commit d5f586c

1 file changed

Lines changed: 85 additions & 10 deletions

File tree

Lite/Services/RemoteCollectorService.cs

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ public partial class RemoteCollectorService
102102
private int _logInsertFailures;
103103
private string? _lastLogInsertError;
104104

105+
/// <summary>
106+
/// Per-server flag indicating that master DB enumeration has failed with an access-denied
107+
/// error and should not be retried. Used on Azure SQL DB where per-database logins may not
108+
/// have master access (e.g. Microsoft Dynamics 365 FO). See issue #857.
109+
/// </summary>
110+
private readonly Dictionary<int, bool> _azureMasterInaccessible = new();
111+
private readonly object _azureMasterInaccessibleLock = new();
112+
105113
public RemoteCollectorService(
106114
DuckDbInitializer duckDb,
107115
ServerManager serverManager,
@@ -561,27 +569,94 @@ INSERT INTO collection_log (log_id, server_id, server_name, collector_name, coll
561569
/// Enumerates online databases on an Azure SQL DB logical server.
562570
/// HAS_DBACCESS() returns false for user databases from master on Azure SQL DB,
563571
/// so we skip that filter — inaccessible databases should be handled by callers via try/catch.
572+
///
573+
/// On Azure SQL DB, logins are sometimes granted access only to a specific user database and
574+
/// not to master (e.g. Microsoft Dynamics 365 FO). In that case, master enumeration fails with
575+
/// an access/login error; we fall back to returning the connection's initial catalog as a
576+
/// single-database list, and cache that decision per server so we don't retry master each cycle.
577+
/// See issue #857.
564578
/// </summary>
565579
protected async Task<List<string>> GetAzureDatabaseListAsync(ServerConnection server, CancellationToken cancellationToken)
566580
{
581+
var serverId = GetServerId(server);
567582
var baseConnStr = server.GetConnectionString(_serverManager.CredentialService);
583+
var targetDb = new SqlConnectionStringBuilder(baseConnStr).InitialCatalog;
584+
585+
bool knownInaccessible;
586+
lock (_azureMasterInaccessibleLock)
587+
{
588+
_azureMasterInaccessible.TryGetValue(serverId, out knownInaccessible);
589+
}
590+
591+
if (knownInaccessible)
592+
{
593+
return SingleDbOrEmpty(targetDb);
594+
}
595+
568596
var connStr = new SqlConnectionStringBuilder(baseConnStr)
569597
{
570598
ConnectTimeout = ConnectionTimeoutSeconds,
571599
InitialCatalog = "master"
572600
}.ConnectionString;
573601

574602
var databases = new List<string>();
575-
using var conn = new SqlConnection(connStr);
576-
await conn.OpenAsync(cancellationToken);
577-
using var cmd = new SqlCommand(
578-
"SELECT name FROM sys.databases WHERE state_desc = N'ONLINE' AND database_id > 0 ORDER BY name;",
579-
conn)
580-
{ CommandTimeout = CommandTimeoutSeconds };
581-
using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
582-
while (await reader.ReadAsync(cancellationToken))
583-
databases.Add(reader.GetString(0));
584-
return databases;
603+
try
604+
{
605+
using var conn = new SqlConnection(connStr);
606+
await conn.OpenAsync(cancellationToken);
607+
using var cmd = new SqlCommand(
608+
"SELECT name FROM sys.databases WHERE state_desc = N'ONLINE' AND database_id > 0 ORDER BY name;",
609+
conn)
610+
{ CommandTimeout = CommandTimeoutSeconds };
611+
using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
612+
while (await reader.ReadAsync(cancellationToken))
613+
databases.Add(reader.GetString(0));
614+
return databases;
615+
}
616+
catch (SqlException ex) when (IsMasterAccessDeniedError(ex))
617+
{
618+
lock (_azureMasterInaccessibleLock)
619+
{
620+
_azureMasterInaccessible[serverId] = true;
621+
}
622+
623+
var fallback = SingleDbOrEmpty(targetDb);
624+
if (fallback.Count > 0)
625+
{
626+
AppLogger.Info("Collector", $" [{server.DisplayName}] master DB inaccessible (SQL error {ex.Number}) — collecting from '{targetDb}' only.");
627+
}
628+
else
629+
{
630+
AppLogger.Warn("Collector", $" [{server.DisplayName}] master DB inaccessible (SQL error {ex.Number}) and no target database in connection string — no data will be collected for database-scoped collectors.");
631+
}
632+
return fallback;
633+
}
634+
}
635+
636+
private static List<string> SingleDbOrEmpty(string? targetDb)
637+
{
638+
if (string.IsNullOrEmpty(targetDb) || string.Equals(targetDb, "master", StringComparison.OrdinalIgnoreCase))
639+
return new List<string>();
640+
return new List<string> { targetDb };
641+
}
642+
643+
/// <summary>
644+
/// Error numbers indicating the login cannot open or read from master on Azure SQL DB.
645+
/// Trigger a fallback to single-database mode when we see one of these.
646+
/// </summary>
647+
private static bool IsMasterAccessDeniedError(SqlException ex)
648+
{
649+
return ex.Number switch
650+
{
651+
229 => true, // Permission denied on object
652+
230 => true, // Permission denied on column
653+
916 => true, // Server principal is not able to access the database under the current security context
654+
4060 => true, // Cannot open database requested by the login
655+
18456 => true, // Login failed for user
656+
40613 => true, // Database 'master' on server is not currently available
657+
40615 => true, // Cannot open server — login denied (firewall/auth)
658+
_ => false
659+
};
585660
}
586661

587662
/// <summary>

0 commit comments

Comments
 (0)