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>
|
/// <para>
|
||||||
/// Thread-model note: the indexer get/set block synchronously
|
/// Thread-model note: the indexer get/set block synchronously
|
||||||
/// on the Instance Actor Ask (and, for data-connected attributes, the DCL
|
/// on the Instance Actor Ask (and, for data-connected attributes, the DCL
|
||||||
/// round-trip). Runtime helper continuations use ConfigureAwait(false), so they
|
/// round-trip). The runtime helper awaits in <see cref="ScriptRuntimeContext"/> and
|
||||||
/// can complete even when every dedicated worker is blocked in an indexer.
|
/// <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
|
/// Script bodies execute on the dedicated
|
||||||
/// <see cref="ScriptExecutionScheduler"/> threads, not the
|
/// <see cref="ScriptExecutionScheduler"/> threads, not the
|
||||||
/// shared <see cref="System.Threading.ThreadPool"/> — so a blocked accessor
|
/// shared <see cref="System.Threading.ThreadPool"/> — so a blocked accessor
|
||||||
|
|||||||
@@ -1150,7 +1150,7 @@ public class ScriptRuntimeContext
|
|||||||
Trace: _isTraced);
|
Trace: _isTraced);
|
||||||
|
|
||||||
// Ask the Instance Actor, which routes to the appropriate Script Actor
|
// 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)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
@@ -1285,7 +1285,7 @@ public class ScriptRuntimeContext
|
|||||||
Guid.NewGuid().ToString(), _instanceName, DateTimeOffset.UtcNow);
|
Guid.NewGuid().ToString(), _instanceName, DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
var response = await _instanceActor.Ask<GetAlarmSnapshotResponse>(
|
var response = await _instanceActor.Ask<GetAlarmSnapshotResponse>(
|
||||||
request, _askTimeout, cancellationToken);
|
request, _askTimeout, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return response.Alarms.Select(ToScriptAlarm).ToList();
|
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>
|
/// <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>
|
/// <returns>The MES-reportable subset of the instance's current alarm conditions.</returns>
|
||||||
internal async Task<IReadOnlyList<ScriptAlarm>> GetMesReportableAlarmsAsync(CancellationToken cancellationToken = default)
|
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>
|
/// <summary>
|
||||||
/// Projects a retained <see cref="AlarmStateChanged"/> onto the flat, script-facing
|
/// Projects a retained <see cref="AlarmStateChanged"/> onto the flat, script-facing
|
||||||
@@ -1407,7 +1407,7 @@ public class ScriptRuntimeContext
|
|||||||
var childContext = _context.CreateChildContextForSharedScript(nextDepth);
|
var childContext = _context.CreateChildContextForSharedScript(nextDepth);
|
||||||
|
|
||||||
return await _library.ExecuteAsync(
|
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;
|
Exception? thrown = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = await _client.CallAsync(systemName, methodName, parameters, cancellationToken);
|
result = await _client.CallAsync(systemName, methodName, parameters, cancellationToken).ConfigureAwait(false);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -2269,7 +2269,7 @@ public class ScriptRuntimeContext
|
|||||||
if (_gateway == null)
|
if (_gateway == null)
|
||||||
throw new InvalidOperationException("Database gateway not available");
|
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
|
// Wrap in an auditing decorator so
|
||||||
// every script-initiated Execute* / ExecuteReader on the returned
|
// every script-initiated Execute* / ExecuteReader on the returned
|
||||||
@@ -2735,7 +2735,7 @@ public class ScriptRuntimeContext
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = await _siteCommunicationActor
|
response = await _siteCommunicationActor
|
||||||
.Ask<NotificationStatusResponse>(query, _askTimeout);
|
.Ask<NotificationStatusResponse>(query, _askTimeout).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -2760,7 +2760,7 @@ public class ScriptRuntimeContext
|
|||||||
// genuinely unknown (never sent, or already forwarded and central lost it).
|
// genuinely unknown (never sent, or already forwarded and central lost it).
|
||||||
if (_storeAndForward != null)
|
if (_storeAndForward != null)
|
||||||
{
|
{
|
||||||
var buffered = await _storeAndForward.GetMessageByIdAsync(notificationId);
|
var buffered = await _storeAndForward.GetMessageByIdAsync(notificationId).ConfigureAwait(false);
|
||||||
if (buffered != null)
|
if (buffered != null)
|
||||||
{
|
{
|
||||||
return new NotificationDeliveryStatus(
|
return new NotificationDeliveryStatus(
|
||||||
@@ -2943,7 +2943,7 @@ public class ScriptRuntimeContext
|
|||||||
// thread: buffer due-immediately and kick the sweep. Send returns
|
// thread: buffer due-immediately and kick the sweep. Send returns
|
||||||
// in milliseconds whether central is up or down.
|
// in milliseconds whether central is up or down.
|
||||||
deferToSweep: true,
|
deferToSweep: true,
|
||||||
messageId: notificationId);
|
messageId: notificationId).ConfigureAwait(false);
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Notify enqueued notification {NotificationId} to list '{List}' for central delivery",
|
"Notify enqueued notification {NotificationId} to list '{List}' for central delivery",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ public class SharedScriptLibrary
|
|||||||
CancellationToken = cancellationToken
|
CancellationToken = cancellationToken
|
||||||
};
|
};
|
||||||
|
|
||||||
var state = await script.RunAsync(globals, cancellationToken);
|
var state = await script.RunAsync(globals, cancellationToken).ConfigureAwait(false);
|
||||||
return state.ReturnValue;
|
return state.ReturnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,4 +254,24 @@ public class AlarmsAccessorTests : TestKit, IDisposable
|
|||||||
|
|
||||||
Assert.Single(await pending);
|
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