Skip to content

Commit 0e4f755

Browse files
committed
fix: add locking around the auto-install of SQL Server objects so that multiple hubs don't try to enable service broker or create the initial schema at the same time.
1 parent 4614bb2 commit 0e4f755

File tree

3 files changed

+27
-27
lines changed

3 files changed

+27
-27
lines changed

src/IntelliTect.AspNetCore.SignalR.SqlServer/Internal/SqlServer/SqlInstaller.cs

Lines changed: 18 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,27 @@
1010

1111
namespace IntelliTect.AspNetCore.SignalR.SqlServer.Internal
1212
{
13-
internal class SqlInstaller
13+
internal class SqlInstaller(SqlServerOptions options, ILogger logger, string messagesTableNamePrefix, string tracePrefix)
1414
{
1515
private const int SchemaVersion = 1;
1616

17-
private readonly string _messagesTableNamePrefix;
18-
private readonly ILogger _logger;
19-
private readonly SqlServerOptions _options;
20-
21-
public SqlInstaller(SqlServerOptions options, ILogger logger, string messagesTableNamePrefix)
22-
{
23-
_logger = logger;
24-
_options = options;
25-
_messagesTableNamePrefix = messagesTableNamePrefix;
26-
}
27-
2817
public async Task Install()
2918
{
30-
if (!_options.AutoEnableServiceBroker && !_options.AutoInstallSchema)
19+
if (!options.AutoEnableServiceBroker && !options.AutoInstallSchema)
3120
{
32-
_logger.LogInformation("Skipping install of SignalR SQL objects");
21+
logger.LogInformation("{HubName}: Skipping install of SignalR SQL objects", tracePrefix);
3322
return;
3423
}
3524

36-
_logger.LogInformation("Start installing SignalR SQL objects");
25+
await options.InstallLock.WaitAsync();
26+
logger.LogInformation("{HubName}: Start installing SignalR SQL objects", tracePrefix);
3727
try
3828
{
39-
using var connection = new SqlConnection(_options.ConnectionString);
29+
using var connection = new SqlConnection(options.ConnectionString);
4030
await connection.OpenAsync();
4131
using var command = connection.CreateCommand();
4232

43-
if (_options.AutoEnableServiceBroker)
33+
if (options.AutoEnableServiceBroker)
4434
{
4535
try
4636
{
@@ -49,31 +39,34 @@ public async Task Install()
4939
}
5040
catch (Exception ex)
5141
{
52-
_logger.LogError(ex, "Unable to automatically enable SQL Server Service Broker.");
42+
logger.LogError(ex, "Unable to automatically enable SQL Server Service Broker.");
5343
}
5444
}
5545

56-
if (_options.AutoInstallSchema)
46+
if (options.AutoInstallSchema)
5747
{
5848
var script = GetType().Assembly.StringResource("install.sql");
5949

60-
script = script.Replace("SET @SCHEMA_NAME = 'SignalR';", "SET @SCHEMA_NAME = '" + _options.SchemaName + "';");
50+
script = script.Replace("SET @SCHEMA_NAME = 'SignalR';", "SET @SCHEMA_NAME = '" + options.SchemaName + "';");
6151
script = script.Replace("SET @TARGET_SCHEMA_VERSION = 1;", "SET @TARGET_SCHEMA_VERSION = " + SchemaVersion + ";");
62-
script = script.Replace("SET @MESSAGE_TABLE_COUNT = 1;", "SET @MESSAGE_TABLE_COUNT = " + _options.TableCount + ";");
63-
script = script.Replace("SET @MESSAGE_TABLE_NAME = 'Messages_YourHubName';", "SET @MESSAGE_TABLE_NAME = '" + _messagesTableNamePrefix + "';");
52+
script = script.Replace("SET @MESSAGE_TABLE_COUNT = 1;", "SET @MESSAGE_TABLE_COUNT = " + options.TableCount + ";");
53+
script = script.Replace("SET @MESSAGE_TABLE_NAME = 'Messages_YourHubName';", "SET @MESSAGE_TABLE_NAME = '" + messagesTableNamePrefix + "';");
6454

6555
command.CommandText = script;
6656
await command.ExecuteNonQueryAsync();
6757

68-
_logger.LogInformation("SignalR SQL objects installed");
58+
logger.LogInformation("{HubName}: SignalR SQL objects installed", messagesTableNamePrefix);
6959
}
7060
}
7161
catch (Exception ex)
7262
{
73-
_logger.LogError(ex, "Unable to install SignalR SQL objects");
63+
logger.LogError(ex, "{HubName}: Unable to install SignalR SQL objects", messagesTableNamePrefix);
7464
throw;
7565
}
76-
66+
finally
67+
{
68+
options.InstallLock.Release();
69+
}
7770
}
7871
}
7972
}

src/IntelliTect.AspNetCore.SignalR.SqlServer/SqlServerHubLifetimeManager.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,14 +371,14 @@ private async Task EnsureSqlServerConnection()
371371
{
372372
if (_streams.Count == 0)
373373
{
374-
var installer = new SqlInstaller(_options, _logger, _tableNamePrefix);
374+
var installer = new SqlInstaller(_options, _logger, _tableNamePrefix, typeof(THub).FullName!);
375375
await installer.Install();
376376

377377
for (var i = 0; i < _options.TableCount; i++)
378378
{
379379
var streamIndex = i;
380380
var tableName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", _tableNamePrefix, streamIndex);
381-
var tracePrefix = $"{typeof(THub).FullName}:{streamIndex}: ";
381+
var tracePrefix = $"{typeof(THub).FullName}:{streamIndex}";
382382

383383
var stream = new SqlStream(
384384
_options,

src/IntelliTect.AspNetCore.SignalR.SqlServer/SqlServerOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ namespace IntelliTect.AspNetCore.SignalR.SqlServer
1414
/// </summary>
1515
public class SqlServerOptions
1616
{
17+
/// <summary>
18+
/// Shared lock to prevent multiple concurrent installs against the same DB.
19+
/// This prevents auto-enable of service broker from deadlocking
20+
/// when an application has multiple hubs.
21+
/// </summary>
22+
internal readonly SemaphoreSlim InstallLock = new SemaphoreSlim(1);
23+
1724
/// <summary>
1825
/// The SQL Server connection string to use.
1926
/// </summary>

0 commit comments

Comments
 (0)