Skip to content

Commit 9a050cd

Browse files
authored
Merge pull request #124 from JeringTech/additions_and_changes_to_retries
Additions and changes to retries
2 parents 8763a51 + fbd8dfa commit 9a050cd

File tree

6 files changed

+195
-29
lines changed

6 files changed

+195
-29
lines changed

Changelog.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ This project uses [semantic versioning](http://semver.org/spec/v2.0.0.html). Ref
33
*[Semantic Versioning in Practice](https://www.jering.tech/articles/semantic-versioning-in-practice)*
44
for an overview of semantic versioning.
55

6-
## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/6.1.0...HEAD)
6+
## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/6.2.0...HEAD)
7+
8+
## [6.2.0](https://github.com/JeringTech/Javascript.NodeJS/compare/6.1.0...6.2.0) - Nov 26, 2021
9+
### Additions
10+
- Added `OutOfProcessNodeJSServiceOptions.EnableProcessRetriesForJavascriptErrors` option. Enables users to choose whether process retries occur for
11+
invocations that fail due to Javascript errors. ([#124](https://github.com/JeringTech/Javascript.NodeJS/pull/124)).
12+
### Fixes
13+
- Fixed infinite process retries bug. ([#124](https://github.com/JeringTech/Javascript.NodeJS/pull/124)).
14+
- Fixed missing log entry for last retry. ([#124](https://github.com/JeringTech/Javascript.NodeJS/pull/124)).
715

816
## [6.1.0](https://github.com/JeringTech/Javascript.NodeJS/compare/6.0.1...6.1.0) - Nov 4, 2021
917
### Additions

ReadMe.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,10 +1338,23 @@ attempt the invocation 3 times.
13381338

13391339
If this value is negative, the library creates new NodeJS processes indefinitely.
13401340

1341+
By default, process retries are disabled for invocation failures caused by javascript errors. See `OutOfProcessNodeJSServiceOptions.EnableProcessRetriesForJavascriptErrors` for more information.
1342+
13411343
If the module source of an invocation is an unseekable stream, the invocation is not retried.
13421344
If you require retries for such streams, copy their contents to a `MemoryStream`.
13431345

13441346
Defaults to 1.
1347+
##### OutOfProcessNodeJSServiceOptions.EnableProcessRetriesForJavascriptErrors
1348+
Whether invocation failures caused by Javascript errors are retried in new processes.
1349+
```csharp
1350+
public bool EnableProcessRetriesForJavascriptErrors { get; set; }
1351+
```
1352+
###### Remarks
1353+
Process retries were introduced to deal with process-level issues. For example, when a NodeJS process becomes unresponsive the only solution is to start a new process.
1354+
1355+
If this value is `true`, process retries also occur on Javascript errors. If it is `false`, they only occur for process-level issues.
1356+
1357+
Defaults to `false`.
13451358
##### OutOfProcessNodeJSServiceOptions.NumConnectionRetries
13461359
Number of times the library retries NodeJS connection attempts.
13471360
```csharp

src/NodeJS/NodeJSServiceImplementations/OutOfProcess/OutOfProcessNodeJSService.cs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public abstract class OutOfProcessNodeJSService : INodeJSService
4747
private readonly int _numRetries;
4848
private readonly int _numProcessRetries;
4949
private readonly int _numConnectionRetries;
50+
private readonly bool _enableProcessRetriesForJavascriptErrors;
5051
private readonly int _timeoutMS;
5152
private readonly ConcurrentDictionary<Task, object?> _trackedInvokeTasks; // TODO use ConcurrentSet when it's available - https://github.com/dotnet/runtime/issues/16443
5253
private readonly CountdownEvent _invokeTaskCreationCountdown;
@@ -97,6 +98,7 @@ protected OutOfProcessNodeJSService(INodeJSProcessFactory nodeProcessFactory,
9798
_numProcessRetries = _options.NumProcessRetries;
9899
_numConnectionRetries = _options.NumConnectionRetries;
99100
_timeoutMS = _options.TimeoutMS;
101+
_enableProcessRetriesForJavascriptErrors = _options.EnableProcessRetriesForJavascriptErrors;
100102

101103
(_trackInvokeTasks, _trackedInvokeTasks, _invokeTaskCreationCountdown) = InitializeFileWatching();
102104
}
@@ -108,7 +110,7 @@ protected OutOfProcessNodeJSService(INodeJSProcessFactory nodeProcessFactory,
108110
/// <param name="invocationRequest">The invocation request to send to the NodeJS process.</param>
109111
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the invocation.</param>
110112
/// <returns>The task object representing the asynchronous operation.</returns>
111-
protected abstract Task<(bool, T?)> TryInvokeAsync<T>(InvocationRequest invocationRequest, CancellationToken cancellationToken);
113+
protected abstract Task<(bool, T?)> TryInvokeAsync<T>(InvocationRequest invocationRequest, CancellationToken cancellationToken);
112114

113115
/// <summary>
114116
/// <para>This method is called when the connection established message from the NodeJS process is received.</para>
@@ -300,32 +302,39 @@ public virtual void MoveToNewProcess()
300302
{
301303
Logger.LogWarning(string.Format(Strings.LogWarning_InvocationAttemptFailed, numRetries < 0 ? "infinity" : numRetries.ToString(), exception.ToString()));
302304
}
305+
306+
if (numRetries == 0 && exception is InvocationException && !_enableProcessRetriesForJavascriptErrors)
307+
{
308+
// Don't retry in new process if exception is caused by JS error and process retries for JS errors is not enabled
309+
throw;
310+
}
311+
}
312+
catch (Exception exception) when (_warningLoggingEnabled) // numRetries == 0 && numProcessRetries == 0
313+
{
314+
Logger.LogWarning(string.Format(Strings.LogWarning_InvocationAttemptFailed, numRetries < 0 ? "infinity" : numRetries.ToString(), exception.ToString()));
315+
throw;
303316
}
304317
finally
305318
{
306319
cancellationTokenSource?.Dispose();
307320
}
308321

309-
if (numRetries == 0)
322+
if (numRetries == 0) // If we get here, numProcessRetries != 0
310323
{
311-
// If retries in the existing process have been exhausted but process retries remain,
312-
// move to new process and reset numRetries.
313-
if (numProcessRetries > 0)
324+
// If retries in the existing process have been exhausted but process retries remain, move to new process and reset numRetries.
325+
if (_warningLoggingEnabled)
314326
{
315-
if (_warningLoggingEnabled)
316-
{
317-
Logger.LogWarning(string.Format(Strings.LogWarning_RetriesInExistingProcessExhausted, numProcessRetries < 0 ? "infinity" : numProcessRetries.ToString()));
318-
}
327+
Logger.LogWarning(string.Format(Strings.LogWarning_RetriesInExistingProcessExhausted, numProcessRetries < 0 ? "infinity" : numProcessRetries.ToString()));
328+
}
319329

320-
numProcessRetries = numProcessRetries > 0 ? numProcessRetries - 1 : numProcessRetries;
321-
numRetries = _numRetries - 1;
330+
numProcessRetries = numProcessRetries > 0 ? numProcessRetries - 1 : numProcessRetries; // numProcessRetries can be negative (retry indefinitely)
331+
numRetries = _numRetries - 1;
322332

323-
MoveToNewProcess(false);
324-
}
333+
MoveToNewProcess(false);
325334
}
326335
else
327336
{
328-
numRetries = numRetries > 0 ? numRetries - 1 : numRetries;
337+
numRetries = numRetries > 0 ? numRetries - 1 : numRetries; // numRetries can be negative (retry indefinitely)
329338
}
330339
}
331340
}

src/NodeJS/NodeJSServiceImplementations/OutOfProcess/OutOfProcessNodeJSServiceOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,21 @@ public class OutOfProcessNodeJSServiceOptions
3232
/// If it fails, it retries the invocation once. If it fails again, the library creates a new process that retries the invocation once. In total, the library
3333
/// attempt the invocation 3 times.</para>
3434
/// <para>If this value is negative, the library creates new NodeJS processes indefinitely.</para>
35+
/// <para>By default, process retries are disabled for invocation failures caused by javascript errors. See <see cref="EnableProcessRetriesForJavascriptErrors"/> for more information.</para>
3536
/// <para>If the module source of an invocation is an unseekable stream, the invocation is not retried.
3637
/// If you require retries for such streams, copy their contents to a <see cref="MemoryStream"/>.</para>
3738
/// <para>Defaults to 1.</para>
3839
/// </remarks>
3940
public int NumProcessRetries { get; set; } = 1;
4041

42+
/// <summary>Whether invocation failures caused by Javascript errors are retried in new processes.</summary>
43+
/// <remarks>
44+
/// <para>Process retries were introduced to deal with process-level issues. For example, when a NodeJS process becomes unresponsive the only solution is to start a new process.</para>
45+
/// <para>If this value is <c>true</c>, process retries also occur on Javascript errors. If it is <c>false</c>, they only occur for process-level issues.</para>
46+
/// <para>Defaults to <c>false</c>.</para>
47+
/// </remarks>
48+
public bool EnableProcessRetriesForJavascriptErrors { get; set; } = false;
49+
4150
/// <summary>Number of times the library retries NodeJS connection attempts.</summary>
4251
/// <remarks>
4352
/// <para>If this value is negative, connection attempts are retried indefinitely.</para>

test/NodeJS/HttpNodeJSServiceIntegrationTests.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,13 +1161,13 @@ public async void MoveToNewProcess_MovesToNewProcess()
11611161
}
11621162

11631163
[Fact(Timeout = TIMEOUT_MS)]
1164-
public async void NewProcessRetries_RetriesFailedInvocationInNewProcess()
1164+
public async void NewProcessRetries_RetriesInvocationThatFailedDueToJavascriptErrorsInNewProcessIfSuchRetriesAreEnabled()
11651165
{
11661166
// Arrange
11671167
const string dummyErrorMessagePrefix = "Error in process with ID: ";
11681168
string dummyErrorThrowingModule = $"module.exports = (callback) => {{throw new Error('{dummyErrorMessagePrefix}' + process.pid);}}";
11691169
var resultStringBuilder = new StringBuilder();
1170-
HttpNodeJSService testSubject = CreateHttpNodeJSService(resultStringBuilder);
1170+
HttpNodeJSService testSubject = CreateHttpNodeJSService(resultStringBuilder, enableProcessRetriesForJavascriptErrors: true);
11711171

11721172
// Act and assert
11731173
InvocationException result = await Assert.ThrowsAsync<InvocationException>(() => testSubject.InvokeFromStringAsync(dummyErrorThrowingModule)).ConfigureAwait(false);
@@ -1185,6 +1185,29 @@ public async void NewProcessRetries_RetriesFailedInvocationInNewProcess()
11851185
Assert.NotEqual(firstExceptionProcessID, thirdExceptionProcessID); // Invocation invoked in new process
11861186
}
11871187

1188+
[Fact(Timeout = TIMEOUT_MS)]
1189+
public async void NewProcessRetries_DoesNotRetryInvocationThatFailedDueToJavascriptErrorsInNewProcessIfSuchRetriesAreDisabled()
1190+
{
1191+
// Arrange
1192+
const string dummyErrorMessagePrefix = "Error in process with ID: ";
1193+
string dummyErrorThrowingModule = $"module.exports = (callback) => {{throw new Error('{dummyErrorMessagePrefix}' + process.pid);}}";
1194+
var resultStringBuilder = new StringBuilder();
1195+
HttpNodeJSService testSubject = CreateHttpNodeJSService(resultStringBuilder, enableProcessRetriesForJavascriptErrors: false);
1196+
1197+
// Act and assert
1198+
InvocationException result = await Assert.ThrowsAsync<InvocationException>(() => testSubject.InvokeFromStringAsync(dummyErrorThrowingModule)).ConfigureAwait(false);
1199+
// Process ID of NodeJS process first two errors were thrown in
1200+
string resultLog = resultStringBuilder.ToString();
1201+
MatchCollection logMatches = Regex.Matches(resultLog, $"{typeof(InvocationException).FullName}: {dummyErrorMessagePrefix}(\\d+)");
1202+
string firstExceptionProcessID = logMatches[0].Groups[1].Captures[0].Value;
1203+
string secondExceptionProcessID = logMatches[1].Groups[1].Captures[0].Value;
1204+
Assert.Equal(firstExceptionProcessID, secondExceptionProcessID);
1205+
// Process ID of NodeJS process last error was thrown in
1206+
MatchCollection exceptionMessageMatches = Regex.Matches(result.Message, $"^{dummyErrorMessagePrefix}(\\d+)");
1207+
string thirdExceptionProcessID = exceptionMessageMatches[0].Groups[1].Captures[0].Value;
1208+
Assert.Equal(firstExceptionProcessID, thirdExceptionProcessID); // Invocation not invoked in new process
1209+
}
1210+
11881211
#if NET5_0 || NETCOREAPP3_1
11891212
[Theory]
11901213
[MemberData(nameof(HttpVersion_IsConfigurable_Data))]
@@ -1221,7 +1244,8 @@ private HttpNodeJSService CreateHttpNodeJSService(StringBuilder? loggerStringBui
12211244
#if NETCOREAPP3_1 || NET5_0
12221245
Version? httpVersion = default,
12231246
#endif
1224-
ServiceCollection? services = default)
1247+
ServiceCollection? services = default,
1248+
bool enableProcessRetriesForJavascriptErrors = false)
12251249
{
12261250
services ??= new ServiceCollection();
12271251
services.AddNodeJS(); // Default INodeJSService is HttpNodeService
@@ -1256,6 +1280,8 @@ private HttpNodeJSService CreateHttpNodeJSService(StringBuilder? loggerStringBui
12561280
services.Configure<OutOfProcessNodeJSServiceOptions>(options => options.TimeoutMS = -1);
12571281
}
12581282

1283+
services.Configure<OutOfProcessNodeJSServiceOptions>(options => options.EnableProcessRetriesForJavascriptErrors = enableProcessRetriesForJavascriptErrors);
1284+
12591285
_serviceProvider = services.BuildServiceProvider();
12601286

12611287
return (HttpNodeJSService)_serviceProvider.GetRequiredService<INodeJSService>();

0 commit comments

Comments
 (0)