1
+ using System ;
2
+ using System . Collections . Generic ;
3
+ using System . Linq ;
4
+ using System . Threading . Tasks ;
5
+ using Azure . ResourceManager ;
6
+ using Azure . ResourceManager . AppContainers ;
7
+ using Microsoft . Extensions . Configuration ;
8
+ using Microsoft . Extensions . Logging ;
9
+ using Proto . Utils ;
10
+
11
+ namespace Proto . Cluster . AzureContainerApps ;
12
+
13
+ public class AzureContainerAppsProvider : IClusterProvider
14
+ {
15
+ public readonly string AdvertisedHost ;
16
+
17
+ private readonly ArmClient _client ;
18
+ private readonly string _resourceGroup ;
19
+ private readonly string _containerAppName ;
20
+ private readonly string _revisionName ;
21
+ private readonly string _replicaName ;
22
+
23
+ private string _memberId = null ! ;
24
+ private string _address = null ! ;
25
+ private Cluster _cluster = null ! ;
26
+ private string _clusterName = null ! ;
27
+ private string [ ] _kinds = null ! ;
28
+ private string _host = null ! ;
29
+ private int _port ;
30
+
31
+ private readonly IConfiguration _configuration ;
32
+ private static readonly ILogger Logger = Log . CreateLogger < AzureContainerAppsProvider > ( ) ;
33
+ private static readonly TimeSpan PollIntervalInSeconds = TimeSpan . FromSeconds ( 5 ) ;
34
+
35
+ public AzureContainerAppsProvider (
36
+ IConfiguration configuration ,
37
+ ArmClient client ,
38
+ string resourceGroup ,
39
+ string containerAppName ,
40
+ string revisionName ,
41
+ string replicaName ,
42
+ string advertisedHost = default )
43
+ {
44
+ _configuration = configuration ;
45
+ _client = client ;
46
+ _resourceGroup = resourceGroup ;
47
+ _containerAppName = containerAppName ;
48
+ _revisionName = revisionName ;
49
+ _replicaName = replicaName ;
50
+ AdvertisedHost = advertisedHost ;
51
+
52
+ if ( string . IsNullOrEmpty ( AdvertisedHost ) )
53
+ {
54
+ AdvertisedHost = ConfigUtils . FindIpAddress ( ) . ToString ( ) ;
55
+ }
56
+ }
57
+
58
+ public async Task StartMemberAsync ( Cluster cluster )
59
+ {
60
+ var clusterName = cluster . Config . ClusterName ;
61
+ var ( host , port ) = cluster . System . GetAddress ( ) ;
62
+ var kinds = cluster . GetClusterKinds ( ) ;
63
+ _cluster = cluster ;
64
+ _clusterName = clusterName ;
65
+ _memberId = cluster . System . Id ;
66
+ _port = port ;
67
+ _host = host ;
68
+ _kinds = kinds ;
69
+ _address = $ "{ host } :{ port } ";
70
+
71
+ await RegisterMemberAsync ( ) ;
72
+ StartClusterMonitor ( ) ;
73
+ }
74
+
75
+ public Task StartClientAsync ( Cluster cluster )
76
+ {
77
+ var clusterName = cluster . Config . ClusterName ;
78
+ var ( host , port ) = cluster . System . GetAddress ( ) ;
79
+ _cluster = cluster ;
80
+ _clusterName = clusterName ;
81
+ _memberId = cluster . System . Id ;
82
+ _port = port ;
83
+ _host = host ;
84
+ _kinds = Array . Empty < string > ( ) ;
85
+
86
+ StartClusterMonitor ( ) ;
87
+ return Task . CompletedTask ;
88
+ }
89
+
90
+ public async Task ShutdownAsync ( bool graceful ) => await DeregisterMemberAsync ( ) ;
91
+
92
+ private async Task RegisterMemberAsync ( )
93
+ {
94
+ await Retry . Try ( RegisterMemberInner , onError : OnError , onFailed : OnFailed , retryCount : Retry . Forever ) ;
95
+
96
+ static void OnError ( int attempt , Exception exception ) =>
97
+ Logger . LogWarning ( exception , "Failed to register service" ) ;
98
+
99
+ static void OnFailed ( Exception exception ) => Logger . LogError ( exception , "Failed to register service" ) ;
100
+ }
101
+
102
+ private async Task RegisterMemberInner ( )
103
+ {
104
+ var resourceGroup = await _client . GetResourceGroupByName ( _resourceGroup ) ;
105
+ var containerApp = await resourceGroup . Value . GetContainerAppAsync ( _containerAppName ) ;
106
+ var revision = await containerApp . Value . GetContainerAppRevisionAsync ( _revisionName ) ;
107
+
108
+ if ( revision . Value . Data . TrafficWeight . GetValueOrDefault ( 0 ) == 0 )
109
+ {
110
+ return ;
111
+ }
112
+
113
+ Logger . LogInformation (
114
+ "[Cluster][AzureContainerAppsProvider] Registering service {ReplicaName} on {IpAddress}" ,
115
+ _replicaName ,
116
+ _address ) ;
117
+
118
+ var tags = new Dictionary < string , string >
119
+ {
120
+ [ ResourceTagLabels . LabelCluster ( _memberId ) ] = _clusterName ,
121
+ [ ResourceTagLabels . LabelHost ( _memberId ) ] = AdvertisedHost ,
122
+ [ ResourceTagLabels . LabelPort ( _memberId ) ] = _port . ToString ( ) ,
123
+ [ ResourceTagLabels . LabelMemberId ( _memberId ) ] = _memberId ,
124
+ [ ResourceTagLabels . LabelReplicaName ( _memberId ) ] = _replicaName
125
+ } ;
126
+
127
+ foreach ( var kind in _kinds )
128
+ {
129
+ var labelKey = $ "{ ResourceTagLabels . LabelKind ( _memberId ) } -{ kind } ";
130
+ tags . TryAdd ( labelKey , "true" ) ;
131
+ }
132
+
133
+ try
134
+ {
135
+ await _client . AddMemberTags ( _resourceGroup , _containerAppName , tags ) ;
136
+ }
137
+ catch ( Exception x )
138
+ {
139
+ Logger . LogError ( x , "Failed to update metadata" ) ;
140
+ }
141
+ }
142
+
143
+ private void StartClusterMonitor ( ) =>
144
+ _ = SafeTask . Run ( async ( ) =>
145
+ {
146
+ while ( ! _cluster . System . Shutdown . IsCancellationRequested )
147
+ {
148
+ Logger . LogInformation ( "Calling ECS API" ) ;
149
+
150
+ try
151
+ {
152
+ var members = await _client . GetClusterMembers ( _resourceGroup , _containerAppName ) ;
153
+
154
+ if ( members . Any ( ) )
155
+ {
156
+ Logger . LogInformation ( "Got members {Members}" , members . Length ) ;
157
+ _cluster . MemberList . UpdateClusterTopology ( members ) ;
158
+ }
159
+ else
160
+ {
161
+ Logger . LogWarning ( "Failed to get members from Azure Container Apps" ) ;
162
+ }
163
+ }
164
+ catch ( Exception x )
165
+ {
166
+ Logger . LogError ( x , "Failed to get members from Azure Container Apps" ) ;
167
+ }
168
+
169
+ await Task . Delay ( PollIntervalInSeconds ) ;
170
+ }
171
+ }
172
+ ) ;
173
+
174
+ private async Task DeregisterMemberAsync ( )
175
+ {
176
+ await Retry . Try ( DeregisterMemberInner , onError : OnError , onFailed : OnFailed ) ;
177
+
178
+ static void OnError ( int attempt , Exception exception ) =>
179
+ Logger . LogWarning ( exception , "Failed to deregister service" ) ;
180
+
181
+ static void OnFailed ( Exception exception ) => Logger . LogError ( exception , "Failed to deregister service" ) ;
182
+ }
183
+
184
+ private async Task DeregisterMemberInner ( )
185
+ {
186
+ Logger . LogInformation (
187
+ "[Cluster][AzureContainerAppsProvider] Unregistering member {ReplicaName} on {IpAddress}" ,
188
+ _replicaName ,
189
+ _address ) ;
190
+
191
+ await _client . ClearMemberTags ( _resourceGroup , _containerAppName , _memberId ) ;
192
+ }
193
+ }
0 commit comments