8
8
using System . Net . Http ;
9
9
using System . Security . Cryptography ;
10
10
using System . Text ;
11
+ using System . Text . RegularExpressions ;
11
12
using System . Threading ;
12
13
using System . Threading . Tasks ;
13
14
using Microsoft . Azure . WebJobs . Host . Executors ;
14
15
using Microsoft . Azure . WebJobs . Script . Description ;
16
+ using Microsoft . Azure . WebJobs . Script . Models ;
15
17
using Microsoft . Azure . WebJobs . Script . WebHost . Extensions ;
16
18
using Microsoft . Azure . WebJobs . Script . WebHost . Security ;
17
19
using Microsoft . Extensions . Configuration ;
@@ -29,9 +31,18 @@ public class FunctionsSyncManager : IFunctionsSyncManager, IDisposable
29
31
private const string HubName = "HubName" ;
30
32
private const string TaskHubName = "taskHubName" ;
31
33
private const string Connection = "connection" ;
32
- private const string DurableTaskStorageConnectionName = "azureStorageConnectionStringName" ;
34
+ private const string DurableTaskV1StorageConnectionName = "azureStorageConnectionStringName" ;
35
+ private const string DurableTaskV2StorageOptions = "storageOptions" ;
36
+ private const string DurableTaskV2StorageConnectionName = "connectionStringName" ;
33
37
private const string DurableTask = "durableTask" ;
34
38
39
+ // 45 alphanumeric characters gives us a buffer in our table/queue/blob container names.
40
+ private const int MaxTaskHubNameSize = 45 ;
41
+ private const int MinTaskHubNameSize = 3 ;
42
+ private const string TaskHubPadding = "Hub" ;
43
+
44
+ private readonly Regex versionRegex = new Regex ( @"Version=(?<majorversion>\d)\.\d\.\d" ) ;
45
+
35
46
private readonly IOptionsMonitor < ScriptApplicationHostOptions > _applicationHostOptions ;
36
47
private readonly ILogger _logger ;
37
48
private readonly HttpClient _httpClient ;
@@ -323,32 +334,21 @@ public async Task<string> GetSyncTriggersPayload()
323
334
324
335
internal async Task < IEnumerable < JObject > > GetFunctionTriggers ( IEnumerable < FunctionMetadata > functionsMetadata , ScriptJobHostOptions hostOptions )
325
336
{
326
- var durableTaskConfig = await ReadDurableTaskConfig ( ) ;
327
337
var triggers = ( await functionsMetadata
328
338
. Where ( f => ! f . IsProxy )
329
339
. Select ( f => f . ToFunctionTrigger ( hostOptions ) )
330
340
. WhenAll ( ) )
331
- . Where ( t => t != null )
332
- . Select ( t =>
341
+ . Where ( t => t != null ) ;
342
+
343
+ if ( triggers . Any ( IsDurableTrigger ) )
344
+ {
345
+ DurableConfig durableTaskConfig = await ReadDurableTaskConfig ( ) ;
346
+ // If any host level durable config values, we need to apply them to all durable triggers
347
+ if ( durableTaskConfig . HasValues ( ) )
333
348
{
334
- // if we have a durableTask hub name and the function trigger is either orchestrationTrigger OR activityTrigger,
335
- // add a property "taskHubName" with durable task hub name.
336
- if ( durableTaskConfig . Any ( )
337
- && ( t [ "type" ] ? . ToString ( ) . Equals ( "orchestrationTrigger" , StringComparison . OrdinalIgnoreCase ) == true
338
- || t [ "type" ] ? . ToString ( ) . Equals ( "activityTrigger" , StringComparison . OrdinalIgnoreCase ) == true ) )
339
- {
340
- if ( durableTaskConfig . ContainsKey ( HubName ) )
341
- {
342
- t [ TaskHubName ] = durableTaskConfig [ HubName ] ;
343
- }
344
-
345
- if ( durableTaskConfig . ContainsKey ( Connection ) )
346
- {
347
- t [ Connection ] = durableTaskConfig [ Connection ] ;
348
- }
349
- }
350
- return t ;
351
- } ) ;
349
+ triggers = triggers . Select ( t => UpdateDurableFunctionConfig ( t , durableTaskConfig ) ) ;
350
+ }
351
+ }
352
352
353
353
if ( FileUtility . FileExists ( Path . Combine ( hostOptions . RootScriptPath , ScriptConstants . ProxyMetadataFileName ) ) )
354
354
{
@@ -359,50 +359,169 @@ internal async Task<IEnumerable<JObject>> GetFunctionTriggers(IEnumerable<Functi
359
359
return triggers ;
360
360
}
361
361
362
- private async Task < Dictionary < string , string > > ReadDurableTaskConfig ( )
362
+ private static bool IsDurableTrigger ( JObject trigger )
363
+ {
364
+ return trigger [ "type" ] ? . ToString ( ) . Equals ( "orchestrationTrigger" , StringComparison . OrdinalIgnoreCase ) == true
365
+ || trigger [ "type" ] ? . ToString ( ) . Equals ( "entityTrigger" , StringComparison . OrdinalIgnoreCase ) == true
366
+ || trigger [ "type" ] ? . ToString ( ) . Equals ( "activityTrigger" , StringComparison . OrdinalIgnoreCase ) == true ;
367
+ }
368
+
369
+ private static JObject UpdateDurableFunctionConfig ( JObject trigger , DurableConfig durableTaskConfig )
370
+ {
371
+ if ( IsDurableTrigger ( trigger ) )
372
+ {
373
+ if ( durableTaskConfig . HubName != null )
374
+ {
375
+ trigger [ TaskHubName ] = durableTaskConfig . HubName ;
376
+ }
377
+
378
+ if ( durableTaskConfig . Connection != null )
379
+ {
380
+ trigger [ Connection ] = durableTaskConfig . Connection ;
381
+ }
382
+ }
383
+ return trigger ;
384
+ }
385
+
386
+ private async Task < DurableConfig > ReadDurableTaskConfig ( )
363
387
{
388
+ JObject hostJson = null ;
389
+ JObject durableHostConfig = null ;
364
390
var hostOptions = _applicationHostOptions . CurrentValue . ToHostOptions ( ) ;
365
391
string hostJsonPath = Path . Combine ( hostOptions . RootScriptPath , ScriptConstants . HostMetadataFileName ) ;
366
- var config = new Dictionary < string , string > ( ) ;
367
392
if ( FileUtility . FileExists ( hostJsonPath ) )
368
393
{
369
- var json = JObject . Parse ( await FileUtility . ReadAsync ( hostJsonPath ) ) ;
394
+ hostJson = JObject . Parse ( await FileUtility . ReadAsync ( hostJsonPath ) ) ;
370
395
371
396
// get the DurableTask extension config section
372
- JToken extensionsValue ;
373
- if ( json . TryGetValue ( "extensions" , StringComparison . OrdinalIgnoreCase , out extensionsValue ) && extensionsValue != null )
397
+ if ( hostJson != null &&
398
+ hostJson . TryGetValue ( "extensions" , StringComparison . OrdinalIgnoreCase , out JToken extensionsValue ) )
374
399
{
375
- json = ( JObject ) extensionsValue ;
400
+ // we will allow case insensitivity given it is likely user hand edited
401
+ // see https://github.com/Azure/azure-functions-durable-extension/issues/111
402
+ var extensions = extensionsValue as JObject ;
403
+ if ( extensions != null &&
404
+ extensions . TryGetValue ( DurableTask , StringComparison . OrdinalIgnoreCase , out JToken durableTaskValue ) )
405
+ {
406
+ durableHostConfig = durableTaskValue as JObject ;
407
+ }
376
408
}
409
+ }
410
+
411
+ var durableMajorVersion = await GetDurableMajorVersionAsync ( hostJson , hostOptions ) ;
412
+ if ( durableMajorVersion == null || durableMajorVersion . Equals ( "1" ) )
413
+ {
414
+ return GetDurableV1Config ( durableHostConfig ) ;
415
+ }
416
+ else
417
+ {
418
+ // v2 or greater
419
+ return GetDurableV2Config ( durableHostConfig ) ;
420
+ }
421
+ }
422
+
423
+ // This is a stopgap approach to get the Durable extension version. It duplicates some logic in ExtensionManager.cs.
424
+ private async Task < string > GetDurableMajorVersionAsync ( JObject hostJson , ScriptJobHostOptions hostOptions )
425
+ {
426
+ bool isUsingBundles = hostJson != null && hostJson . TryGetValue ( "extensionBundle" , StringComparison . OrdinalIgnoreCase , out _ ) ;
427
+ if ( isUsingBundles )
428
+ {
429
+ // TODO: As of 2019-12-12, there are no extension bundles for version 2.x of Durable.
430
+ // This may change in the future.
431
+ return "1" ;
432
+ }
433
+
434
+ string binPath = binPath = Path . Combine ( hostOptions . RootScriptPath , "bin" ) ;
435
+ string metadataFilePath = Path . Combine ( binPath , ScriptConstants . ExtensionsMetadataFileName ) ;
436
+ if ( ! FileUtility . FileExists ( metadataFilePath ) )
437
+ {
438
+ return null ;
439
+ }
440
+
441
+ var extensionMetadata = JObject . Parse ( await FileUtility . ReadAsync ( metadataFilePath ) ) ;
442
+ var extensionItems = extensionMetadata [ "extensions" ] ? . ToObject < List < ExtensionReference > > ( ) ;
443
+
444
+ var durableExtension = extensionItems ? . FirstOrDefault ( ext => string . Equals ( ext . Name , "DurableTask" , StringComparison . OrdinalIgnoreCase ) ) ;
445
+ if ( durableExtension == null )
446
+ {
447
+ return null ;
448
+ }
377
449
378
- // we will allow case insensitivity given it is likely user hand edited
379
- // see https://github.com/Azure/azure-functions-durable-extension/issues/111
380
- JToken durableTaskValue ;
381
- if ( json . TryGetValue ( DurableTask , StringComparison . OrdinalIgnoreCase , out durableTaskValue ) && durableTaskValue != null )
450
+ var versionMatch = versionRegex . Match ( durableExtension . TypeName ) ;
451
+ if ( ! versionMatch . Success )
452
+ {
453
+ return null ;
454
+ }
455
+
456
+ // Grab the captured group.
457
+ return versionMatch . Groups [ "majorversion" ] . Captures [ 0 ] . Value ;
458
+ }
459
+
460
+ private DurableConfig GetDurableV1Config ( JObject durableHostConfig )
461
+ {
462
+ var config = new DurableConfig ( ) ;
463
+ if ( durableHostConfig != null )
464
+ {
465
+ if ( durableHostConfig . TryGetValue ( HubName , StringComparison . OrdinalIgnoreCase , out JToken nameValue ) && nameValue != null )
382
466
{
383
- try
384
- {
385
- var kvp = ( JObject ) durableTaskValue ;
386
- if ( kvp . TryGetValue ( HubName , StringComparison . OrdinalIgnoreCase , out JToken nameValue ) && nameValue != null )
387
- {
388
- config . Add ( HubName , nameValue . ToString ( ) ) ;
389
- }
390
-
391
- if ( kvp . TryGetValue ( DurableTaskStorageConnectionName , StringComparison . OrdinalIgnoreCase , out nameValue ) && nameValue != null )
392
- {
393
- config . Add ( Connection , nameValue . ToString ( ) ) ;
394
- }
395
- }
396
- catch ( Exception )
467
+ config . HubName = nameValue . ToString ( ) ;
468
+ }
469
+
470
+ if ( durableHostConfig . TryGetValue ( DurableTaskV1StorageConnectionName , StringComparison . OrdinalIgnoreCase , out nameValue ) && nameValue != null )
471
+ {
472
+ config . Connection = nameValue . ToString ( ) ;
473
+ }
474
+ }
475
+
476
+ return config ;
477
+ }
478
+
479
+ private DurableConfig GetDurableV2Config ( JObject durableHostConfig )
480
+ {
481
+ var config = new DurableConfig ( ) ;
482
+
483
+ if ( durableHostConfig != null )
484
+ {
485
+ if ( durableHostConfig . TryGetValue ( HubName , StringComparison . OrdinalIgnoreCase , out JToken nameValue ) && nameValue != null )
486
+ {
487
+ config . HubName = nameValue . ToString ( ) ;
488
+ }
489
+
490
+ if ( durableHostConfig . TryGetValue ( DurableTaskV2StorageOptions , StringComparison . OrdinalIgnoreCase , out JToken storageOptions ) && ( storageOptions as JObject ) != null )
491
+ {
492
+ if ( ( ( JObject ) storageOptions ) . TryGetValue ( DurableTaskV2StorageConnectionName , StringComparison . OrdinalIgnoreCase , out nameValue ) && nameValue != null )
397
493
{
398
- throw new InvalidDataException ( "Invalid host.json configuration for 'durableTask'." ) ;
494
+ config . Connection = nameValue . ToString ( ) ;
399
495
}
400
496
}
401
497
}
402
498
499
+ if ( config . HubName == null )
500
+ {
501
+ config . HubName = GetDefaultDurableV2HubName ( ) ;
502
+ }
503
+
403
504
return config ;
404
505
}
405
506
507
+ // This logic will eventually be moved to ScaleController once it has access to version information.
508
+ private string GetDefaultDurableV2HubName ( )
509
+ {
510
+ // See https://github.com/Azure/azure-functions-durable-extension/blob/eb186eadb73a21d0efdc33cd7603fde5d802cab9/src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs#L42
511
+ string hubName = _environment . GetEnvironmentVariable ( EnvironmentSettingNames . AzureWebsiteName ) ;
512
+ // See https://github.com/Azure/azure-functions-durable-extension/blob/eb186eadb73a21d0efdc33cd7603fde5d802cab9/src/WebJobs.Extensions.DurableTask/Options/AzureStorageOptions.cs#L145
513
+ hubName = new string ( hubName . ToCharArray ( )
514
+ . Where ( char . IsLetterOrDigit )
515
+ . Take ( MaxTaskHubNameSize )
516
+ . ToArray ( ) ) ;
517
+ if ( hubName . Length < MinTaskHubNameSize )
518
+ {
519
+ hubName += TaskHubPadding ;
520
+ }
521
+
522
+ return hubName ;
523
+ }
524
+
406
525
internal HttpRequestMessage BuildSetTriggersRequest ( )
407
526
{
408
527
var protocol = "https" ;
@@ -466,5 +585,17 @@ public void Dispose()
466
585
{
467
586
_syncSemaphore . Dispose ( ) ;
468
587
}
588
+
589
+ private class DurableConfig
590
+ {
591
+ public string HubName { get ; set ; }
592
+
593
+ public string Connection { get ; set ; }
594
+
595
+ public bool HasValues ( )
596
+ {
597
+ return this . HubName != null || this . Connection != null ;
598
+ }
599
+ }
469
600
}
470
601
}
0 commit comments