1111using System . Linq ;
1212using System . Threading ;
1313using System . Threading . Tasks ;
14+ using Azure . Storage . Blobs ;
1415using Microsoft . AspNetCore . Hosting ;
16+ using Microsoft . Azure . WebJobs . Host . Storage ;
1517using Microsoft . Azure . WebJobs . Script . WebHost ;
1618using Microsoft . Azure . WebJobs . Script . Workers . Rpc ;
1719using Microsoft . Extensions . DependencyInjection ;
1820using Microsoft . WebJobs . Script . Tests ;
1921using Xunit ;
22+ using static Microsoft . Azure . WebJobs . Script . HostIdValidator ;
2023
2124namespace Microsoft . Azure . WebJobs . Script . Tests
2225{
@@ -28,6 +31,8 @@ public class StandbyManagerE2ETests_Windows : StandbyManagerE2ETestBase
2831
2932 public StandbyManagerE2ETests_Windows ( )
3033 {
34+ Utility . ColdStartDelayMS = 1000 ;
35+
3136 _settings = new Dictionary < string , string > ( )
3237 {
3338 { EnvironmentSettingNames . AzureWebsitePlaceholderMode , "1" } ,
@@ -67,7 +72,7 @@ public async Task ZipPackageFailure_DetectedOnSpecialization()
6772 Assert . True ( environment . IsContainerReady ( ) ) ;
6873
6974 // wait for shutdown to be triggered
70- var applicationLifetime = host . Services . GetServices < IApplicationLifetime > ( ) . Single ( ) ;
75+ var applicationLifetime = host . Services . GetServices < Microsoft . AspNetCore . Hosting . IApplicationLifetime > ( ) . Single ( ) ;
7176 await TestHelpers . RunWithTimeoutAsync ( ( ) => applicationLifetime . ApplicationStopping . WaitHandle . WaitOneAsync ( ) , TimeSpan . FromSeconds ( 30 ) ) ;
7277
7378 // ensure the host was specialized and the expected error was logged
@@ -168,6 +173,105 @@ await TestHelpers.Await(() =>
168173 Assert . NotSame ( GetCachedTimeZoneInfo ( ) , _originalTimeZoneInfoCache ) ;
169174 }
170175
176+ [ Fact ]
177+ public async Task StandbyModeE2E_Dotnet_HostIdValidator_DoesNotRunInPlaceholderMode ( )
178+ {
179+ _settings . Add ( EnvironmentSettingNames . AzureWebsiteInstanceId , Guid . NewGuid ( ) . ToString ( ) ) ;
180+
181+ string siteName = "areallylongnamethatexceedsthemaxhostidlength" ;
182+ string expectedHostId = siteName . Substring ( 0 , 32 ) ;
183+ string expectedHostName = $ "{ siteName } .azurewebsites.net";
184+
185+ string placeholderSiteName = "functionsv4x86inproc8placeholdertemplatesite" ;
186+ string placeholderHostId = placeholderSiteName . Substring ( 0 , 32 ) ;
187+ string placeholderHostName = $ "{ placeholderSiteName } .azurewebsites.net";
188+
189+ var environment = new TestEnvironment ( _settings ) ;
190+ await InitializeTestHostAsync ( "Windows" , environment , placeholderSiteName ) ;
191+
192+ // Directly configure a bad placeholder mode host ID record.
193+ // Before the fix for this bug, such records were being generated
194+ // as part of a specialization race condition.
195+ var serviceProvider = _httpServer . Host . Services ;
196+ var blobStorageProvider = serviceProvider . GetService < IAzureBlobStorageProvider > ( ) ;
197+ Assert . True ( blobStorageProvider . TryCreateHostingBlobContainerClient ( out var blobContainerClient ) ) ;
198+ var hostIdInfo = new HostIdValidator . HostIdInfo { Hostname = expectedHostName } ;
199+ string blobPath = string . Format ( BlobPathFormat , placeholderHostId ) ;
200+ BlobClient blobClient = blobContainerClient . GetBlobClient ( blobPath ) ;
201+ BinaryData data = BinaryData . FromObjectAsJson ( hostIdInfo ) ;
202+ await blobClient . UploadAsync ( data , overwrite : true ) ;
203+
204+ await VerifyWarmupSucceeds ( ) ;
205+ await VerifyWarmupSucceeds ( restart : true ) ;
206+
207+ // before specialization, the hostname will contain the placeholder site name
208+ var hostNameProvider = serviceProvider . GetService < HostNameProvider > ( ) ;
209+ Assert . Equal ( placeholderHostName , hostNameProvider . Value ) ;
210+
211+ // allow time for any scheduled host ID check to happen
212+ await Task . Delay ( Utility . ColdStartDelayMS ) ;
213+
214+ // now specialize the host
215+ environment . SetEnvironmentVariable ( EnvironmentSettingNames . AzureWebsitePlaceholderMode , "0" ) ;
216+ environment . SetEnvironmentVariable ( EnvironmentSettingNames . AzureWebsiteContainerReady , "1" ) ;
217+ environment . SetEnvironmentVariable ( RpcWorkerConstants . FunctionWorkerRuntimeSettingName , "dotnet" ) ;
218+ environment . SetEnvironmentVariable ( EnvironmentSettingNames . AzureWebsiteName , siteName ) ;
219+
220+ Assert . False ( environment . IsPlaceholderModeEnabled ( ) ) ;
221+ Assert . True ( environment . IsContainerReady ( ) ) ;
222+
223+ // give time for the specialization to happen
224+ string [ ] logLines = null ;
225+ LogMessage [ ] errors = null ;
226+ await TestHelpers . Await ( ( ) =>
227+ {
228+ errors = _loggerProvider . GetAllLogMessages ( ) . Where ( p => p . Level == Microsoft . Extensions . Logging . LogLevel . Error && p . EventId . Id != 302 ) . ToArray ( ) ;
229+ if ( errors . Any ( ) )
230+ {
231+ // one or more unexpected errors
232+ return true ;
233+ }
234+
235+ // wait for the trace indicating that the host has been specialized
236+ logLines = _loggerProvider . GetAllLogMessages ( ) . Where ( p => p . FormattedMessage != null ) . Select ( p => p . FormattedMessage ) . ToArray ( ) ;
237+
238+ return logLines . Contains ( "Generating 0 job function(s)" ) && logLines . Contains ( "Stopping JobHost" ) ;
239+ } , userMessageCallback : ( ) => string . Join ( Environment . NewLine , _loggerProvider . GetAllLogMessages ( ) . Select ( p => $ "[{ p . Timestamp . ToString ( "HH:mm:ss.fff" ) } ] { p . FormattedMessage } ") ) ) ;
240+
241+ Assert . Empty ( errors ) ;
242+
243+ var logs = _loggerProvider . GetAllLogMessages ( ) . ToArray ( ) ;
244+
245+ // after specialization, the hostname changes
246+ Assert . Equal ( expectedHostName , hostNameProvider . Value ) ;
247+
248+ // verify the rest of the expected logs
249+ logLines = logs . Where ( p => p . FormattedMessage != null ) . Select ( p => p . FormattedMessage ) . ToArray ( ) ;
250+ Assert . True ( logLines . Count ( p => p . Contains ( "Stopping JobHost" ) ) >= 1 ) ;
251+ Assert . Equal ( 1 , logLines . Count ( p => p . Contains ( "Creating StandbyMode placeholder function directory" ) ) ) ;
252+ Assert . Equal ( 1 , logLines . Count ( p => p . Contains ( "StandbyMode placeholder function directory created" ) ) ) ;
253+ Assert . Equal ( 2 , logLines . Count ( p => p . Contains ( "Host is in standby mode" ) ) ) ;
254+ Assert . Equal ( 2 , logLines . Count ( p => p . Contains ( "Executed 'Functions.WarmUp' (Succeeded" ) ) ) ;
255+ Assert . Equal ( 1 , logLines . Count ( p => p . Contains ( "Starting host specialization" ) ) ) ;
256+ Assert . Equal ( 1 , logLines . Count ( p => p . Contains ( "Starting language worker channel specialization" ) ) ) ;
257+ Assert . Equal ( 6 , logLines . Count ( p => p . Contains ( $ "Loading functions metadata") ) ) ;
258+ Assert . Equal ( 4 , logLines . Count ( p => p . Contains ( $ "1 functions loaded") ) ) ;
259+ Assert . Equal ( 2 , logLines . Count ( p => p . Contains ( $ "0 functions loaded") ) ) ;
260+ Assert . Contains ( "Generating 0 job function(s)" , logLines ) ;
261+
262+ // we expect to see host startup logs for both the placeholder site as well as the
263+ // specialized site
264+ Assert . Equal ( 2 , logLines . Count ( p => p . Contains ( $ "Starting Host (HostId={ placeholderHostId } ") ) ) ;
265+ Assert . Equal ( 1 , logLines . Count ( p => p . Contains ( $ "Starting Host (HostId={ expectedHostId } ") ) ) ;
266+
267+ // allow time for any scheduled host ID check to happen
268+ await Task . Delay ( Utility . ColdStartDelayMS ) ;
269+
270+ // Don't expect any errors (other than bundle download errors)
271+ errors = _loggerProvider . GetAllLogMessages ( ) . Where ( p => p . Level == Microsoft . Extensions . Logging . LogLevel . Error && p . EventId . Id != 302 ) . ToArray ( ) ;
272+ Assert . Empty ( errors ) ;
273+ }
274+
171275 [ Fact ( Skip = "https://github.com/Azure/azure-functions-host/issues/7805" ) ]
172276 public async Task InitializeAsync_WithSpecializedSite_SkipsWarmupFunctionsAndLogs ( )
173277 {
0 commit comments