Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/multi-plugin-smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ jobs:
name: 🔥 Plugin Co-existence
uses: RicherTunes/Lidarr.Plugin.Common/.github/workflows/multi-plugin-smoke-test.yml@main
with:
caller_plugin: brainarr
test_plugins: 'brainarr,qobuzarr,tidalarr'
lidarr_docker_version: 'pr-plugins-2.14.2.4786'
secrets:
SUBMODULES_TOKEN: ${{ secrets.SUBMODULES_TOKEN }}
lidarr_tag: 'pr-plugins-2.14.2.4786'
secrets: inherit
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
fetch-depth: 0
submodules: false
token: ${{ secrets.SUBMODULES_TOKEN || github.token }}
token: ${{ github.token }}

- name: Init Common submodule
uses: ./.github/actions/init-common-submodule
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/packaging-closure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
fetch-depth: 0
submodules: false
token: ${{ secrets.SUBMODULES_TOKEN || github.token }}
token: ${{ github.token }}

- name: Init Common submodule
uses: ./.github/actions/init-common-submodule
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/plugin-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
with:
fetch-depth: 0
submodules: false
token: ${{ secrets.SUBMODULES_TOKEN || github.token }}
token: ${{ github.token }}

- name: Init Common submodule
uses: ./.github/actions/init-common-submodule
Expand Down Expand Up @@ -198,7 +198,7 @@ jobs:
with:
fetch-depth: 0
submodules: false
token: ${{ secrets.SUBMODULES_TOKEN || github.token }}
token: ${{ github.token }}

- name: Download packaged artifact
uses: actions/download-artifact@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
with:
fetch-depth: 0
submodules: false
token: ${{ secrets.SUBMODULES_TOKEN || github.token }}
token: ${{ github.token }}


- name: Init Common submodule
Expand Down
8 changes: 5 additions & 3 deletions Brainarr.Plugin/Resilience/CircuitBreaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
int failureThreshold = 3,
int openDurationSeconds = 60,
int timeoutSeconds = 30,
Logger logger = null)
Logger logger = null,
TimeProvider timeProvider = null)
{
_timeout = TimeSpan.FromSeconds(timeoutSeconds);
_logger = logger ?? LogManager.GetCurrentClassLogger();
Expand All @@ -92,11 +93,12 @@
SuccessThresholdInHalfOpen = 1
};

// Create inner circuit breaker with NLog adapter
// Create inner circuit breaker with NLog adapter and optional TimeProvider
_inner = new CommonResilience.CircuitBreaker(
$"brainarr-{Guid.NewGuid():N}",
options,
new NLogAdapter(_logger));
new NLogAdapter(_logger),
timeProvider);
}

/// <summary>
Expand Down Expand Up @@ -129,7 +131,7 @@
return await task.ConfigureAwait(false);
}, cancellationToken, operationName).ConfigureAwait(false);
}
catch (CommonResilience.CircuitBreakerOpenException ex)

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Sanity Build (Ubuntu)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Sanity Build (Ubuntu)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Registry Validation

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Registry Validation

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Verify Package Closure

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Verify Package Closure

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / test

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / test

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (macos-latest, 8.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (macos-latest, 8.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (macos-latest, 6.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (macos-latest, 6.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (ubuntu-latest, 6.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (ubuntu-latest, 6.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (ubuntu-latest, 8.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (ubuntu-latest, 8.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test (Windows)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test (Windows)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (windows-latest, 6.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (windows-latest, 6.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (windows-latest, 8.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Test & Build (windows-latest, 8.0.x)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Analyze (CodeQL) (csharp)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Analyze (CodeQL) (csharp)

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Security Scan

The variable 'ex' is declared but never used

Check warning on line 134 in Brainarr.Plugin/Resilience/CircuitBreaker.cs

View workflow job for this annotation

GitHub Actions / Security Scan

The variable 'ex' is declared but never used
{
var msg = $"Circuit breaker is open for {operationName}. Service unavailable.";
_logger.Warn(msg);
Expand Down
3 changes: 3 additions & 0 deletions Brainarr.Tests/Brainarr.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
<AdditionalProperties>PluginPackagingDisable=true</AdditionalProperties>
</ProjectReference>
<ProjectReference Include="..\tests\Brainarr.TestKit.Providers\Brainarr.TestKit.Providers.csproj" />
<ProjectReference Include="..\ext\lidarr.plugin.common\testkit\Lidarr.Plugin.Common.TestKit.csproj">
<AdditionalProperties>PluginPackagingDisable=true</AdditionalProperties>
</ProjectReference>
</ItemGroup>

<ItemGroup>
Expand Down
30 changes: 18 additions & 12 deletions Brainarr.Tests/Resilience/CircuitBreakerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Lidarr.Plugin.Common.TestKit.Testing;
using NLog;
using NzbDrone.Core.ImportLists.Brainarr.Resilience;
using Xunit;
Expand Down Expand Up @@ -159,11 +160,14 @@ await cb.ExecuteAsync<int>(
[Fact]
public async Task CircuitBreaker_transitions_to_half_open_after_duration()
{
// Use FakeTimeProvider for deterministic time control (no flaky Task.Delay)
var fakeTime = new FakeTimeProvider();
var cb = new CircuitBreaker(
failureThreshold: 1,
openDurationSeconds: 1, // 1 second for test
timeoutSeconds: 30,
logger: L);
logger: L,
timeProvider: fakeTime);

// Open the circuit
try
Expand All @@ -176,8 +180,8 @@ await cb.ExecuteAsync<int>(

cb.State.Should().Be(CircuitState.Open);

// Wait for open duration to pass
await Task.Delay(1500); // Wait 1.5 seconds
// Advance fake time past open duration (deterministic, instant)
fakeTime.Advance(TimeSpan.FromSeconds(1.5));

// Next call should find circuit in half-open and succeed
var result = await cb.ExecuteAsync(
Expand All @@ -198,9 +202,9 @@ public async Task Timeout_opens_circuit_and_reset_allows_success()
// Immediate timeout and open
var cb = new CircuitBreaker(failureThreshold: 1, openDurationSeconds: 1, timeoutSeconds: 3, logger: L);

// First call times out -> opens
// First call times out -> opens (simulated timeout; no real delay required)
await Assert.ThrowsAsync<TimeoutException>(async () =>
await cb.ExecuteAsync(async () => { await Task.Delay(4000); return 1; }, "slow")
await cb.ExecuteAsync(() => Task.FromException<int>(new TimeoutException("Simulated timeout")), "slow")
);
cb.State.Should().Be(CircuitState.Open);
cb.FailureCount.Should().BeGreaterThan(0);
Expand All @@ -215,20 +219,22 @@ await Assert.ThrowsAsync<TimeoutException>(async () =>
[Fact]
public async Task Half_open_failure_reopens_circuit()
{
var cb = new CircuitBreaker(failureThreshold: 1, openDurationSeconds: 1, timeoutSeconds: 1, logger: L);
// Use FakeTimeProvider for open duration transitions (deterministic)
var fakeTime = new FakeTimeProvider();
var cb = new CircuitBreaker(failureThreshold: 1, openDurationSeconds: 1, timeoutSeconds: 1, logger: L, timeProvider: fakeTime);

// Open
// Open circuit via timeout (simulated timeout; no real delay required)
await Assert.ThrowsAsync<TimeoutException>(async () =>
await cb.ExecuteAsync(async () => { await Task.Delay(2000); return 1; }, "slow")
await cb.ExecuteAsync(() => Task.FromException<int>(new TimeoutException("Simulated timeout")), "slow")
);
cb.State.Should().Be(CircuitState.Open);

// Wait for open duration to pass so circuit transitions to half-open
await Task.Delay(1100);
// Advance fake time past open duration (instant, deterministic)
fakeTime.Advance(TimeSpan.FromSeconds(1.1));

// Half-open attempt fails with timeout -> should re-open
// Half-open attempt fails with timeout -> should re-open (simulated timeout; no real delay required)
await Assert.ThrowsAsync<TimeoutException>(async () =>
await cb.ExecuteAsync(async () => { await Task.Delay(2000); return 1; }, "still-slow")
await cb.ExecuteAsync(() => Task.FromException<int>(new TimeoutException("Simulated timeout")), "still-slow")
);
cb.State.Should().Be(CircuitState.Open);
}
Expand Down
75 changes: 44 additions & 31 deletions Brainarr.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,12 @@
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "8.0.1",
"contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==",
"resolved": "9.0.0",
"contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
"Microsoft.Extensions.DependencyInjection": "9.0.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.0",
"Microsoft.Extensions.Options": "9.0.0"
}
},
"Microsoft.Extensions.Logging.Configuration": {
Expand Down Expand Up @@ -414,6 +414,14 @@
"resolved": "6.0.0",
"contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A=="
},
"NSubstitute": {
"type": "Transitive",
"resolved": "5.3.0",
"contentHash": "lJ47Cps5Qzr86N99lcwd+OUvQma7+fBgr8+Mn+aOC0WrlqMNkdivaYD9IvnZ5Mqo6Ky3LS7ZI+tUq1/s9ERd0Q==",
"dependencies": {
"Castle.Core": "5.1.1"
}
},
"System.ClientModel": {
"type": "Transitive",
"resolved": "1.4.2",
Expand Down Expand Up @@ -451,11 +459,6 @@
"resolved": "1.6.0",
"contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "6.0.0",
Expand Down Expand Up @@ -563,7 +566,7 @@
"Lidarr.Plugin.Brainarr": {
"type": "Project",
"dependencies": {
"Lidarr.Plugin.Common": "[1.4.0, )",
"Lidarr.Plugin.Common": "[1.5.0, )",
"Microsoft.Extensions.Caching.Memory": "[8.0.1, )",
"Microsoft.Extensions.DependencyInjection": "[8.0.1, )",
"Newtonsoft.Json": "[13.0.4, )",
Expand All @@ -576,7 +579,7 @@
"Azure.Extensions.AspNetCore.DataProtection.Keys": "[1.6.1, )",
"Azure.Identity": "[1.12.0, )",
"FluentValidation": "[9.5.4, )",
"Lidarr.Plugin.Abstractions": "[1.4.0, )",
"Lidarr.Plugin.Abstractions": "[1.5.0, )",
"Microsoft.AspNetCore.DataProtection": "[8.0.16, )",
"Microsoft.AspNetCore.DataProtection.Extensions": "[8.0.16, )",
"Microsoft.Extensions.Caching.Memory": "[8.0.1, )",
Expand All @@ -592,6 +595,18 @@
"TagLibSharp": "[2.3.0, )"
}
},
"lidarr.plugin.common.testkit": {
"type": "Project",
"dependencies": {
"Lidarr.Plugin.Abstractions": "[1.5.0, )",
"Lidarr.Plugin.Common": "[1.5.0, )",
"Microsoft.Extensions.DependencyInjection": "[9.0.0, )",
"Microsoft.Extensions.Logging": "[9.0.0, )",
"Microsoft.Extensions.Logging.Abstractions": "[9.0.0, )",
"Moq": "[4.20.72, )",
"NSubstitute": "[5.3.0, )"
}
},
"FluentValidation": {
"type": "CentralTransitive",
"requested": "[11.11.0, )",
Expand Down Expand Up @@ -623,51 +638,49 @@
"Microsoft.Extensions.DependencyInjection": {
"type": "CentralTransitive",
"requested": "[8.0.1, )",
"resolved": "8.0.1",
"contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
"resolved": "9.0.0",
"contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "CentralTransitive",
"requested": "[8.0.2, )",
"resolved": "8.0.2",
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
"resolved": "9.0.0",
"contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg=="
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[7.0.0, )",
"resolved": "8.0.3",
"contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
"resolved": "9.0.0",
"contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"System.Diagnostics.DiagnosticSource": "9.0.0"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[7.0.0, )",
"resolved": "8.0.2",
"contentHash": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"resolved": "9.0.0",
"contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0",
"Microsoft.Extensions.Primitives": "9.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "CentralTransitive",
"requested": "[7.0.0, )",
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
"resolved": "9.0.0",
"contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg=="
},
"System.Diagnostics.DiagnosticSource": {
"type": "CentralTransitive",
"requested": "[7.0.2, )",
"resolved": "6.0.1",
"contentHash": "KiLYDu2k2J82Q9BJpWiuQqCkFjRBWVq4jDzKKWawVi9KWzyD0XG3cmfX0vqTQlL14Wi9EufJrbL0+KCLTbqWiQ==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
"resolved": "9.0.0",
"contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw=="
},
"System.Security.Cryptography.ProtectedData": {
"type": "CentralTransitive",
Expand Down
1 change: 1 addition & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<package pattern="FluentValidation*" />
<package pattern="xunit*" />
<package pattern="Moq*" />
<package pattern="NSubstitute*" />
<package pattern="Castle.*" />
<package pattern="FluentAssertions*" />
<package pattern="coverlet.*" />
Expand Down
2 changes: 1 addition & 1 deletion ext-common-sha.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9ea7dcd9006006dbafc856493cd98b25f3dd986b
e1fd02e15f7e94bdc8d83c245691a1f8ae07cb65
Loading