fix(site-runtime): ConfigureAwait(false) on remaining script helper awaits (review 2026-10-21 01/SR-U2)
This commit is contained in:
@@ -11,8 +11,11 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
/// <para>
|
||||
/// Thread-model note: the indexer get/set block synchronously
|
||||
/// on the Instance Actor Ask (and, for data-connected attributes, the DCL
|
||||
/// round-trip). Runtime helper continuations use ConfigureAwait(false), so they
|
||||
/// can complete even when every dedicated worker is blocked in an indexer.
|
||||
/// round-trip). The runtime helper awaits in <see cref="ScriptRuntimeContext"/> and
|
||||
/// <see cref="SharedScriptLibrary"/> use ConfigureAwait(false), so their continuations run on
|
||||
/// the shared thread pool and can complete even when every dedicated worker is blocked in an
|
||||
/// indexer. A script that itself blocks on a helper task (<c>.Result</c>/<c>.Wait()</c>) still
|
||||
/// holds its worker until the callee finishes, but no longer deadlocks the scheduler against it.
|
||||
/// Script bodies execute on the dedicated
|
||||
/// <see cref="ScriptExecutionScheduler"/> threads, not the
|
||||
/// shared <see cref="System.Threading.ThreadPool"/> — so a blocked accessor
|
||||
|
||||
@@ -1150,7 +1150,7 @@ public class ScriptRuntimeContext
|
||||
Trace: _isTraced);
|
||||
|
||||
// Ask the Instance Actor, which routes to the appropriate Script Actor
|
||||
var result = await _instanceActor.Ask<ScriptCallResult>(request, _askTimeout);
|
||||
var result = await _instanceActor.Ask<ScriptCallResult>(request, _askTimeout).ConfigureAwait(false);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
@@ -1285,7 +1285,7 @@ public class ScriptRuntimeContext
|
||||
Guid.NewGuid().ToString(), _instanceName, DateTimeOffset.UtcNow);
|
||||
|
||||
var response = await _instanceActor.Ask<GetAlarmSnapshotResponse>(
|
||||
request, _askTimeout, cancellationToken);
|
||||
request, _askTimeout, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response.Alarms.Select(ToScriptAlarm).ToList();
|
||||
}
|
||||
@@ -1294,7 +1294,7 @@ public class ScriptRuntimeContext
|
||||
/// <param name="cancellationToken">Cancels the wait for the actor's reply.</param>
|
||||
/// <returns>The MES-reportable subset of the instance's current alarm conditions.</returns>
|
||||
internal async Task<IReadOnlyList<ScriptAlarm>> GetMesReportableAlarmsAsync(CancellationToken cancellationToken = default)
|
||||
=> (await GetAlarmsAsync(cancellationToken)).Where(a => a.IsMesReportable).ToList();
|
||||
=> (await GetAlarmsAsync(cancellationToken).ConfigureAwait(false)).Where(a => a.IsMesReportable).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Projects a retained <see cref="AlarmStateChanged"/> onto the flat, script-facing
|
||||
@@ -1407,7 +1407,7 @@ public class ScriptRuntimeContext
|
||||
var childContext = _context.CreateChildContextForSharedScript(nextDepth);
|
||||
|
||||
return await _library.ExecuteAsync(
|
||||
scriptName, childContext, ScriptArgs.Normalize(parameters), cancellationToken);
|
||||
scriptName, childContext, ScriptArgs.Normalize(parameters), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1564,7 +1564,7 @@ public class ScriptRuntimeContext
|
||||
Exception? thrown = null;
|
||||
try
|
||||
{
|
||||
result = await _client.CallAsync(systemName, methodName, parameters, cancellationToken);
|
||||
result = await _client.CallAsync(systemName, methodName, parameters, cancellationToken).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -2269,7 +2269,7 @@ public class ScriptRuntimeContext
|
||||
if (_gateway == null)
|
||||
throw new InvalidOperationException("Database gateway not available");
|
||||
|
||||
var inner = await _gateway.GetConnectionAsync(name, cancellationToken);
|
||||
var inner = await _gateway.GetConnectionAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Wrap in an auditing decorator so
|
||||
// every script-initiated Execute* / ExecuteReader on the returned
|
||||
@@ -2735,7 +2735,7 @@ public class ScriptRuntimeContext
|
||||
try
|
||||
{
|
||||
response = await _siteCommunicationActor
|
||||
.Ask<NotificationStatusResponse>(query, _askTimeout);
|
||||
.Ask<NotificationStatusResponse>(query, _askTimeout).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -2760,7 +2760,7 @@ public class ScriptRuntimeContext
|
||||
// genuinely unknown (never sent, or already forwarded and central lost it).
|
||||
if (_storeAndForward != null)
|
||||
{
|
||||
var buffered = await _storeAndForward.GetMessageByIdAsync(notificationId);
|
||||
var buffered = await _storeAndForward.GetMessageByIdAsync(notificationId).ConfigureAwait(false);
|
||||
if (buffered != null)
|
||||
{
|
||||
return new NotificationDeliveryStatus(
|
||||
@@ -2943,7 +2943,7 @@ public class ScriptRuntimeContext
|
||||
// thread: buffer due-immediately and kick the sweep. Send returns
|
||||
// in milliseconds whether central is up or down.
|
||||
deferToSweep: true,
|
||||
messageId: notificationId);
|
||||
messageId: notificationId).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Notify enqueued notification {NotificationId} to list '{List}' for central delivery",
|
||||
|
||||
@@ -102,7 +102,7 @@ public class SharedScriptLibrary
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
|
||||
var state = await script.RunAsync(globals, cancellationToken);
|
||||
var state = await script.RunAsync(globals, cancellationToken).ConfigureAwait(false);
|
||||
return state.ReturnValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -254,4 +254,24 @@ public class AlarmsAccessorTests : TestKit, IDisposable
|
||||
|
||||
Assert.Single(await pending);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The helper's continuation must not capture the script scheduler: a script that blocks its
|
||||
/// only worker on the task (<c>.Wait()</c>) still completes (review 2026-10-21 01/SR-U2).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task CurrentAsync_ContinuationDoesNotNeedTheScriptScheduler_BlockingWaitCompletes()
|
||||
{
|
||||
using var scheduler = new ScriptExecutionScheduler(1);
|
||||
var probe = CreateTestProbe();
|
||||
var ctx = MakeContext(probe.Ref);
|
||||
|
||||
var run = Task.Factory.StartNew(
|
||||
() => ctx.Alarms.CurrentAsync().Wait(TimeSpan.FromSeconds(10)),
|
||||
CancellationToken.None, TaskCreationOptions.None, scheduler);
|
||||
|
||||
RespondWith(probe);
|
||||
|
||||
Assert.True(await run.WaitAsync(TimeSpan.FromSeconds(15)), "CurrentAsync continuation was queued back onto the blocked script scheduler");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user