Skip to content

Commit 8424c6b

Browse files
authored
Merge pull request #41 from Tolyandre/hosted-service
Access MongoDB lasily. Wait jobs to stop gracefully.
2 parents 99858b7 + ca57a86 commit 8424c6b

13 files changed

+315
-42
lines changed

README.md

+1-12
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Add nuget-package Horarium.AspNetCore
5858
dotnet add package Horarium.AspNetCore
5959
```
6060

61-
Add ```Horarium``` in Asp.NET Core DI
61+
Add ```Horarium Server```. This regiters Horarium as a [hosted service](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services), so .Net core runtime automatically starts and gracefully stops Horarium.
6262

6363
```csharp
6464
public void ConfigureServices(IServiceCollection services)
@@ -69,17 +69,6 @@ public void ConfigureServices(IServiceCollection services)
6969
}
7070
```
7171

72-
Start HorariumServer in Asp.NET Core application
73-
74-
```csharp
75-
public void Configure(IApplicationBuilder app)
76-
{
77-
//...
78-
app.ApplicationServices.StartHorariumServer();
79-
//...
80-
}
81-
```
82-
8372
Inject interface ```IHorarium``` into Controller
8473

8574
```csharp

src/Horarium.AspNetCore/Horarium.AspNetCore.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<ItemGroup>
1616
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
1717
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
18+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" />
1819
</ItemGroup>
1920

2021
<ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Horarium.Interfaces;
4+
using Microsoft.Extensions.Hosting;
5+
6+
namespace Horarium.AspNetCore
7+
{
8+
public class HorariumServerHostedService : IHostedService
9+
{
10+
private readonly HorariumServer _horariumServer;
11+
12+
public HorariumServerHostedService(IHorarium horarium)
13+
{
14+
_horariumServer = (HorariumServer) horarium;
15+
}
16+
17+
public Task StartAsync(CancellationToken cancellationToken)
18+
{
19+
_horariumServer.Start();
20+
21+
return Task.CompletedTask;
22+
}
23+
24+
public Task StopAsync(CancellationToken cancellationToken)
25+
{
26+
return _horariumServer.Stop();
27+
}
28+
}
29+
}

src/Horarium.AspNetCore/RegistrationHorariumExtension.cs

+2-6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public static IServiceCollection AddHorariumServer(this IServiceCollection servi
2727
return new HorariumServer(jobRepository, settings);
2828
});
2929

30+
service.AddHostedService<HorariumServerHostedService>();
31+
3032
return service;
3133
}
3234

@@ -52,12 +54,6 @@ public static IServiceCollection AddHorariumClient(this IServiceCollection servi
5254
return service;
5355
}
5456

55-
public static void StartHorariumServer(this IServiceProvider serviceProvider)
56-
{
57-
var server = (HorariumServer)serviceProvider.GetService<IHorarium>();
58-
server.Start();
59-
}
60-
6157
private static void PrepareSettings(HorariumSettings settings, IServiceProvider serviceProvider)
6258
{
6359
if (settings.JobScopeFactory is DefaultJobScopeFactory)

src/Horarium.Mongo/MongoClientProvider.cs

+22-4
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ public sealed class MongoClientProvider : IMongoClientProvider
99
{
1010
private readonly ConcurrentDictionary<Type, string> _collectionNameCache = new ConcurrentDictionary<Type, string>();
1111

12-
private readonly Lazy<MongoClient> _mongoClient;
12+
private readonly MongoClient _mongoClient;
1313
private readonly string _databaseName;
14+
private bool _initialized;
15+
private object _lockObject = new object();
1416

1517
public MongoClientProvider(MongoUrl mongoUrl)
1618
{
1719
_databaseName = mongoUrl.DatabaseName;
18-
_mongoClient = new Lazy<MongoClient>(() => new MongoClient(mongoUrl));
19-
CreateIndexes();
20+
_mongoClient = new MongoClient(mongoUrl);
2021
}
2122

2223
public MongoClientProvider(string mongoConnectionString): this (new MongoUrl(mongoConnectionString))
@@ -35,8 +36,25 @@ private string GetCollectionName(Type entityType)
3536

3637
public IMongoCollection<TEntity> GetCollection<TEntity>()
3738
{
39+
EnsureInitialized();
40+
3841
var collectionName = _collectionNameCache.GetOrAdd(typeof(TEntity), GetCollectionName);
39-
return _mongoClient.Value.GetDatabase(_databaseName).GetCollection<TEntity>(collectionName);
42+
return _mongoClient.GetDatabase(_databaseName).GetCollection<TEntity>(collectionName);
43+
}
44+
45+
private void EnsureInitialized()
46+
{
47+
if (_initialized)
48+
return;
49+
50+
lock (_lockObject)
51+
{
52+
if (_initialized)
53+
return;
54+
55+
_initialized = true;
56+
CreateIndexes();
57+
}
4058
}
4159

4260
private void CreateIndexes()

src/Horarium.Test/AspNetCore/RegistrationHorariumExtensionTest.cs

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using Horarium.AspNetCore;
34
using Horarium.Interfaces;
45
using Horarium.Repository;
@@ -13,27 +14,23 @@ public class RegistrationHorariumExtensionTest
1314
[Fact]
1415
public void AddHorariumServer_DefaultSettings_ReplaceForAspNetCore()
1516
{
16-
var serviceMock = new Mock<IServiceCollection>();
17-
18-
var service = serviceMock.Object;
19-
20-
ServiceDescriptor descriptor = null;
17+
var service = new ServiceCollection();
2118

2219
var settings = new HorariumSettings();
2320

24-
serviceMock.Setup(x => x.Add(It.IsAny<ServiceDescriptor>()))
25-
.Callback<ServiceDescriptor>(x => descriptor = x);
26-
2721
service.AddHorariumServer(Mock.Of<IJobRepository>(),
2822
provider => settings);
2923

24+
var descriptor = service.Single(x => x.ServiceType == typeof(IHorarium));
3025
var horarium = descriptor.ImplementationFactory(Mock.Of<IServiceProvider>());
3126

3227
Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
3328
Assert.Equal(typeof(IHorarium), descriptor.ServiceType);
3429
Assert.Equal(typeof(JobScopeFactory), settings.JobScopeFactory.GetType());
3530
Assert.Equal(typeof(HorariumLogger), settings.Logger.GetType());
3631
Assert.Equal(typeof(HorariumServer), horarium.GetType());
32+
33+
Assert.Contains(service, x => x.ImplementationType == typeof(HorariumServerHostedService));
3734
}
3835

3936
[Fact]

src/Horarium.Test/Mongo/MongoRepositoryFactoryTest.cs

+11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Threading.Tasks;
23
using Horarium.Mongo;
34
using MongoDB.Driver;
45
using Xunit;
@@ -22,5 +23,15 @@ public void Create_NullMongoUrl_Exception()
2223

2324
Assert.Throws<ArgumentNullException>(() => MongoRepositoryFactory.Create(mongoUrl));
2425
}
26+
27+
[Fact]
28+
public async Task Create_WellFormedUrl_AccessMongoLazily()
29+
{
30+
const string stubMongoUrl = "mongodb://fake-url:27017/fake_database_name/?serverSelectionTimeoutMs=100";
31+
32+
var mongoRepository = MongoRepositoryFactory.Create(stubMongoUrl);
33+
34+
await Assert.ThrowsAsync<TimeoutException>(() => mongoRepository.GetJobStatistic());
35+
}
2536
}
2637
}

src/Horarium.Test/RunnerJobTest.cs

+70-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public async Task Start_Stop()
2323
new HorariumSettings(),
2424
new JsonSerializerSettings(),
2525
Mock.Of<IHorariumLogger>(),
26-
Mock.Of<IExecutorJob>());
26+
Mock.Of<IExecutorJob>(),
27+
Mock.Of<IUncompletedTaskList>());
2728

2829
// Act
2930
runnerJobs.Start();
@@ -55,7 +56,8 @@ public async Task Start_RecoverAfterIntervalTimeout_AfterFailedDB()
5556
settings,
5657
new JsonSerializerSettings(),
5758
Mock.Of<IHorariumLogger>(),
58-
Mock.Of<IExecutorJob>());
59+
Mock.Of<IExecutorJob>(),
60+
Mock.Of<IUncompletedTaskList>());
5961

6062
jobRepositoryMock.SetupSequence(x => x.GetReadyJob(It.IsAny<string>(), It.IsAny<TimeSpan>()))
6163
.ThrowsAsync(new Exception())
@@ -84,7 +86,8 @@ public async Task Start_WontRecoverBeforeIntervalTimeout_AfterFailedDB()
8486
settings,
8587
new JsonSerializerSettings(),
8688
Mock.Of<IHorariumLogger>(),
87-
Mock.Of<IExecutorJob>());
89+
Mock.Of<IExecutorJob>(),
90+
Mock.Of<IUncompletedTaskList>());
8891

8992
jobRepositoryMock.SetupSequence(x => x.GetReadyJob(It.IsAny<string>(), It.IsAny<TimeSpan>()))
9093
.ThrowsAsync(new Exception())
@@ -97,5 +100,69 @@ public async Task Start_WontRecoverBeforeIntervalTimeout_AfterFailedDB()
97100
// Assert
98101
jobRepositoryMock.Verify(r => r.GetReadyJob(It.IsAny<string>(), It.IsAny<TimeSpan>()), Times.Once);
99102
}
103+
104+
[Fact]
105+
public async Task Start_NextJobStarted_AddsJobTaskToUncompletedTasks()
106+
{
107+
// Arrange
108+
var jobRepositoryMock = new Mock<IJobRepository>();
109+
var uncompletedTaskList = new Mock<IUncompletedTaskList>();
110+
111+
uncompletedTaskList.Setup(x => x.Add(It.IsAny<Task>()));
112+
113+
jobRepositoryMock.Setup(x => x.GetReadyJob(It.IsAny<string>(), It.IsAny<TimeSpan>()))
114+
.ReturnsAsync(new JobDb
115+
{
116+
JobType = typeof(object).ToString(),
117+
});
118+
119+
var runnerJobs = new RunnerJobs(jobRepositoryMock.Object,
120+
new HorariumSettings
121+
{
122+
IntervalStartJob = TimeSpan.FromHours(1), // prevent second job from starting
123+
},
124+
new JsonSerializerSettings(),
125+
Mock.Of<IHorariumLogger>(),
126+
Mock.Of<IExecutorJob>(),
127+
uncompletedTaskList.Object);
128+
129+
// Act
130+
runnerJobs.Start();
131+
await Task.Delay(TimeSpan.FromSeconds(5));
132+
await runnerJobs.Stop();
133+
134+
// Assert
135+
uncompletedTaskList.Verify(x=>x.Add(It.IsAny<Task>()), Times.Once);
136+
}
137+
138+
[Fact]
139+
public async Task StopAsync_AwaitsWhenAllCompleted()
140+
{
141+
// Arrange
142+
var jobRepositoryMock = new Mock<IJobRepository>();
143+
var uncompletedTaskList = new Mock<IUncompletedTaskList>();
144+
145+
var settings = new HorariumSettings
146+
{
147+
IntervalStartJob = TimeSpan.FromSeconds(2),
148+
};
149+
150+
var runnerJobs = new RunnerJobs(jobRepositoryMock.Object,
151+
settings,
152+
new JsonSerializerSettings(),
153+
Mock.Of<IHorariumLogger>(),
154+
Mock.Of<IExecutorJob>(),
155+
uncompletedTaskList.Object);
156+
157+
jobRepositoryMock.Setup(x => x.GetReadyJob(It.IsAny<string>(), It.IsAny<TimeSpan>()));
158+
159+
// Act
160+
runnerJobs.Start();
161+
await Task.Delay(TimeSpan.FromSeconds(1));
162+
await runnerJobs.Stop();
163+
164+
// Assert
165+
uncompletedTaskList.Verify(x => x.WhenAllCompleted(), Times.Once);
166+
}
100167
}
101168
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Horarium.Handlers;
4+
using Xunit;
5+
6+
namespace Horarium.Test
7+
{
8+
public class UncompletedTaskListTests
9+
{
10+
private readonly UncompletedTaskList _uncompletedTaskList = new UncompletedTaskList();
11+
12+
[Fact]
13+
public async Task Add_TaskWithAnyResult_KeepsTaskUntilCompleted()
14+
{
15+
var tcs1 = new TaskCompletionSource<bool>();
16+
var tcs2 = new TaskCompletionSource<bool>();
17+
var tcs3 = new TaskCompletionSource<bool>();
18+
19+
_uncompletedTaskList.Add(tcs1.Task);
20+
_uncompletedTaskList.Add(tcs2.Task);
21+
_uncompletedTaskList.Add(tcs3.Task);
22+
23+
Assert.Equal(3, _uncompletedTaskList.Count);
24+
25+
tcs1.SetResult(false);
26+
await Task.Delay(TimeSpan.FromSeconds(1)); // give a chance to finish continuations
27+
Assert.Equal(2, _uncompletedTaskList.Count);
28+
29+
tcs2.SetException(new ApplicationException());
30+
await Task.Delay(TimeSpan.FromSeconds(1));
31+
Assert.Equal(1, _uncompletedTaskList.Count);
32+
33+
tcs3.SetCanceled();
34+
await Task.Delay(TimeSpan.FromSeconds(1));
35+
Assert.Equal(0, _uncompletedTaskList.Count);
36+
}
37+
38+
[Fact]
39+
public async Task WhenAllCompleted_NoTasks_ReturnsCompletedTask()
40+
{
41+
// Act
42+
var whenAll = _uncompletedTaskList.WhenAllCompleted();
43+
44+
// Assert
45+
Assert.True(whenAll.IsCompletedSuccessfully);
46+
await whenAll;
47+
}
48+
49+
[Fact]
50+
public async Task WhenAllCompleted_TaskNotCompleted_AwaitsUntilTaskCompleted()
51+
{
52+
// Arrange
53+
var tcs = new TaskCompletionSource<bool>();
54+
_uncompletedTaskList.Add(tcs.Task);
55+
56+
// Act
57+
var whenAll = _uncompletedTaskList.WhenAllCompleted();
58+
59+
// Assert
60+
await Task.Delay(TimeSpan.FromSeconds(1)); // give a chance to finish any running tasks
61+
Assert.False(whenAll.IsCompleted);
62+
63+
tcs.SetResult(false);
64+
await Task.Delay(TimeSpan.FromSeconds(1));
65+
Assert.True(whenAll.IsCompletedSuccessfully);
66+
67+
await whenAll;
68+
}
69+
70+
[Fact]
71+
public async Task WhenAllCompleted_TaskFaulted_DoesNotThrow()
72+
{
73+
// Arrange
74+
_uncompletedTaskList.Add(Task.FromException(new ApplicationException()));
75+
76+
// Act
77+
var whenAll = _uncompletedTaskList.WhenAllCompleted();
78+
79+
await whenAll;
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)