@@ -25,7 +25,8 @@ public class WebJobsScriptHostServiceTests
25
25
{
26
26
private WebJobsScriptHostService _hostService ;
27
27
private ScriptApplicationHostOptionsMonitor _monitor ;
28
- private TestLoggerProvider _testLoggerProvider ;
28
+ private TestLoggerProvider _webHostLoggerProvider = new TestLoggerProvider ( ) ;
29
+ private TestLoggerProvider _jobHostLoggerProvider = new TestLoggerProvider ( ) ;
29
30
private ILoggerFactory _loggerFactory ;
30
31
private Mock < IServiceProvider > _mockRootServiceProvider ;
31
32
private Mock < IServiceScopeFactory > _mockRootScopeFactory ;
@@ -43,12 +44,10 @@ public WebJobsScriptHostServiceTests()
43
44
LogPath = @"c:\tests\logs" ,
44
45
} ;
45
46
_monitor = new ScriptApplicationHostOptionsMonitor ( options ) ;
46
- var services = new ServiceCollection ( )
47
- . AddLogging ( )
48
- . BuildServiceProvider ( ) ;
49
- _host = new Mock < IHost > ( ) ;
50
- _host . Setup ( h => h . Services )
51
- . Returns ( services ) ;
47
+ _loggerFactory = new LoggerFactory ( ) ;
48
+ _loggerFactory . AddProvider ( _webHostLoggerProvider ) ;
49
+
50
+ _host = CreateMockHost ( ) ;
52
51
53
52
_mockRootServiceProvider = new Mock < IServiceProvider > ( ) ;
54
53
_mockRootScopeFactory = new Mock < IServiceScopeFactory > ( ) ;
@@ -58,16 +57,42 @@ public WebJobsScriptHostServiceTests()
58
57
_hostPerformanceManager = new HostPerformanceManager ( _mockEnvironment . Object , _healthMonitorOptions ) ;
59
58
}
60
59
60
+ private Mock < IHost > CreateMockHost ( )
61
+ {
62
+ // The tests can pull the logger from a specific host if they need to.
63
+ var services = new ServiceCollection ( )
64
+ . AddLogging ( l => l . Services . AddSingleton < ILoggerProvider , TestLoggerProvider > ( ) )
65
+ . BuildServiceProvider ( ) ;
66
+
67
+ var host = new Mock < IHost > ( ) ;
68
+
69
+ host . Setup ( h => h . Services )
70
+ . Returns ( services ) ;
71
+
72
+ host . Setup ( h => h . Dispose ( ) )
73
+ . Callback ( ( ) => services . Dispose ( ) ) ;
74
+
75
+ return host ;
76
+ }
77
+
61
78
[ Fact ]
62
79
public async Task HostInitialization_OnInitializationException_MaintainsErrorInformation ( )
63
80
{
64
- _host . SetupSequence ( h => h . StartAsync ( It . IsAny < CancellationToken > ( ) ) )
65
- . Throws ( new HostInitializationException ( "boom" ) )
81
+ // When an exception is thrown, we'll create a new host. Make sure
82
+ // we don't return the same one (with disposed services) the second time.
83
+ var hostA = CreateMockHost ( ) ;
84
+ hostA . Setup ( h => h . StartAsync ( It . IsAny < CancellationToken > ( ) ) )
85
+ . Throws ( new HostInitializationException ( "boom" ) ) ;
86
+
87
+ var hostB = CreateMockHost ( ) ;
88
+ hostB . Setup ( h => h . StartAsync ( It . IsAny < CancellationToken > ( ) ) )
66
89
. Returns ( Task . CompletedTask ) ;
67
90
68
91
var hostBuilder = new Mock < IScriptHostBuilder > ( ) ;
69
- hostBuilder . Setup ( b => b . BuildHost ( It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
70
- . Returns ( _host . Object ) ;
92
+ hostBuilder . SetupSequence ( b => b . BuildHost ( It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
93
+ . Returns ( hostA . Object )
94
+ . Returns ( hostB . Object ) ;
95
+
71
96
_hostService = new WebJobsScriptHostService (
72
97
_monitor , hostBuilder . Object , NullLoggerFactory . Instance , _mockRootServiceProvider . Object , _mockRootScopeFactory . Object ,
73
98
_mockScriptWebHostEnvironment . Object , _mockEnvironment . Object , _hostPerformanceManager , _healthMonitorOptions ) ;
@@ -88,9 +113,9 @@ public async Task HostRestart_Specialization_Succeeds()
88
113
hostBuilder . Setup ( b => b . BuildHost ( It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
89
114
. Returns ( _host . Object ) ;
90
115
91
- _testLoggerProvider = new TestLoggerProvider ( ) ;
116
+ _webHostLoggerProvider = new TestLoggerProvider ( ) ;
92
117
_loggerFactory = new LoggerFactory ( ) ;
93
- _loggerFactory . AddProvider ( _testLoggerProvider ) ;
118
+ _loggerFactory . AddProvider ( _webHostLoggerProvider ) ;
94
119
95
120
_hostService = new WebJobsScriptHostService (
96
121
_monitor , hostBuilder . Object , _loggerFactory , _mockRootServiceProvider . Object , _mockRootScopeFactory . Object ,
@@ -105,10 +130,80 @@ public async Task HostRestart_Specialization_Succeeds()
105
130
restartHostThread . Join ( ) ;
106
131
specializeHostThread . Join ( ) ;
107
132
108
- var logMessages = _testLoggerProvider . GetAllLogMessages ( ) . Where ( m => m . FormattedMessage . Contains ( "Restarting host." ) ) ;
133
+ var logMessages = _webHostLoggerProvider . GetAllLogMessages ( ) . Where ( m => m . FormattedMessage . Contains ( "Restarting host." ) ) ;
109
134
Assert . Equal ( 2 , logMessages . Count ( ) ) ;
110
135
}
111
136
137
+ [ Fact ]
138
+ public async Task HostRestart_DuringInitialization_Recovers ( )
139
+ {
140
+ SemaphoreSlim semaphore = new SemaphoreSlim ( 1 , 1 ) ;
141
+ await semaphore . WaitAsync ( ) ;
142
+
143
+ // Have the first host start, but pause. We'll issue a restart, wait for the
144
+ // second host to be running, then let this first host throw an exception.
145
+ var hostA = CreateMockHost ( ) ;
146
+ hostA
147
+ . Setup ( h => h . StartAsync ( It . IsAny < CancellationToken > ( ) ) )
148
+ . Returns ( async ( ) =>
149
+ {
150
+ await semaphore . WaitAsync ( ) ;
151
+ throw new InvalidOperationException ( "Something happened at startup!" ) ;
152
+ } ) ;
153
+
154
+ var hostB = CreateMockHost ( ) ;
155
+ hostB
156
+ . Setup ( h => h . StartAsync ( It . IsAny < CancellationToken > ( ) ) )
157
+ . Returns ( ( ) => Task . CompletedTask ) ;
158
+
159
+ var hostBuilder = new Mock < IScriptHostBuilder > ( ) ;
160
+ hostBuilder . SetupSequence ( b => b . BuildHost ( It . IsAny < bool > ( ) , It . IsAny < bool > ( ) ) )
161
+ . Returns ( hostA . Object )
162
+ . Returns ( hostB . Object ) ;
163
+
164
+ _webHostLoggerProvider = new TestLoggerProvider ( ) ;
165
+ _loggerFactory = new LoggerFactory ( ) ;
166
+ _loggerFactory . AddProvider ( _webHostLoggerProvider ) ;
167
+
168
+ _hostService = new WebJobsScriptHostService (
169
+ _monitor , hostBuilder . Object , _loggerFactory , _mockRootServiceProvider . Object , _mockRootScopeFactory . Object ,
170
+ _mockScriptWebHostEnvironment . Object , _mockEnvironment . Object , _hostPerformanceManager , _healthMonitorOptions ) ;
171
+
172
+ Task initialStart = _hostService . StartAsync ( CancellationToken . None ) ;
173
+
174
+ Thread restartHostThread = new Thread ( new ThreadStart ( RestartHost ) ) ;
175
+ restartHostThread . Start ( ) ;
176
+ restartHostThread . Join ( ) ;
177
+
178
+ await TestHelpers . Await ( ( ) => _hostService . State == ScriptHostState . Running ) ;
179
+
180
+ // Now let the first host throw its startup exception.
181
+ semaphore . Release ( ) ;
182
+
183
+ await initialStart ;
184
+
185
+ // Note that HostA is disposed so its services cannot be accessed. Logging will fall
186
+ // back to using the WebHost's logger.
187
+ Assert . Throws < ObjectDisposedException > ( ( ) => hostA . Object . GetTestLoggerProvider ( ) ) ;
188
+ TestLoggerProvider hostBLogger = hostB . Object . GetTestLoggerProvider ( ) ;
189
+
190
+ // Make sure the error was logged to the correct logger
191
+ Assert . Contains ( _webHostLoggerProvider . GetAllLogMessages ( ) , m => m . FormattedMessage != null && m . FormattedMessage . Contains ( "A host error has occurred on an inactive host" ) ) ;
192
+ Assert . DoesNotContain ( hostBLogger . GetAllLogMessages ( ) , m => m . FormattedMessage != null && m . FormattedMessage . Contains ( "A host error has occurred" ) ) ;
193
+
194
+ // Make sure we orphaned the correct host when the exception was thrown. This will happen
195
+ // twice: once during Restart and once during the Orphan call when handling the exception.
196
+ hostA . Verify ( m => m . StopAsync ( It . IsAny < CancellationToken > ( ) ) , Times . Exactly ( 2 ) ) ;
197
+ hostA . Verify ( m => m . Dispose ( ) , Times . Exactly ( 2 ) ) ;
198
+
199
+ // We should not be calling Orphan on the good host
200
+ hostB . Verify ( m => m . StopAsync ( It . IsAny < CancellationToken > ( ) ) , Times . Never ) ;
201
+ hostB . Verify ( m => m . Dispose ( ) , Times . Never ) ;
202
+
203
+ // The "late" exception from the first host shouldn't bring things down
204
+ Assert . Equal ( ScriptHostState . Running , _hostService . State ) ;
205
+ }
206
+
112
207
public void RestartHost ( )
113
208
{
114
209
_hostService . RestartHostAsync ( CancellationToken . None ) . Wait ( ) ;
0 commit comments