agones.dev/agones@v1.53.0/sdks/csharp/sdk/Beta.cs (about) 1 // Copyright 2020 Google LLC All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 using Agones.Dev.Sdk.Beta; 15 using Grpc.Core; 16 using Microsoft.Extensions.Logging; 17 using System; 18 using System.Collections.Generic; 19 using System.Linq; 20 using System.Threading; 21 using System.Threading.Tasks; 22 using Grpc.Net.Client; 23 using gProto = Google.Protobuf.WellKnownTypes; 24 25 [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Agones.Test")] 26 namespace Agones 27 { 28 public sealed class Beta : IAgonesBetaSDK 29 { 30 31 /// <summary> 32 /// The timeout for gRPC calls. 33 /// </summary> 34 public double RequestTimeoutSec { get; set; } 35 36 internal SDK.SDKClient client; 37 internal readonly IClientStreamWriter<Empty> healthStream; 38 internal readonly CancellationTokenSource cts; 39 internal readonly bool ownsCts; 40 internal CancellationToken ctoken; 41 42 private readonly ILogger _logger; 43 private bool _disposed; 44 45 public Beta( 46 GrpcChannel channel, 47 double requestTimeoutSec = 15, 48 CancellationTokenSource cancellationTokenSource = null, 49 ILogger logger = null) 50 { 51 _logger = logger; 52 RequestTimeoutSec = requestTimeoutSec; 53 54 if (cancellationTokenSource == null) 55 { 56 cts = new CancellationTokenSource(); 57 ownsCts = true; 58 } 59 else 60 { 61 cts = cancellationTokenSource; 62 ownsCts = false; 63 } 64 65 ctoken = cts.Token; 66 client = new SDK.SDKClient(channel); 67 } 68 69 /// <summary> 70 /// GetCounterCountAsync returns the Count for a Counter, given the Counter's key (name). 71 /// Throws error if the key was not predefined in the GameServer resource on creation. 72 /// </summary> 73 /// <returns>The Counter's Count</returns> 74 public async Task<long> GetCounterCountAsync(string key) 75 { 76 try 77 { 78 var request = new GetCounterRequest() 79 { 80 Name = key, 81 }; 82 var counter = await client.GetCounterAsync(request, 83 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 84 return counter.Count; 85 } 86 catch (RpcException ex) 87 { 88 LogError(ex, $"Unable to invoke GetCounterCount({key})."); 89 throw; 90 } 91 } 92 93 /// <summary> 94 /// IncrementCounterAsync increases a counter by the given nonnegative integer amount. 95 /// Will execute the increment operation against the current CRD value. Will max at max(int64). 96 /// Throws error if the key was not predefined in the GameServer resource on creation. 97 /// Throws error if the count is at the current capacity (to the latest knowledge of the SDK), 98 /// and no increment will occur. 99 /// 100 /// Note: A potential race condition here is that if count values are set from both the SDK and 101 /// through the K8s API (Allocation or otherwise), since the SDK append operation back to the CRD 102 /// value is batched asynchronous any value incremented past the capacity will be silently truncated. 103 /// </summary> 104 public async Task IncrementCounterAsync(string key, long amount) 105 { 106 if (amount < 0) 107 { 108 throw new ArgumentOutOfRangeException($"CountIncrement amount must be a positive number, found {amount}"); 109 } 110 try 111 { 112 var request = new CounterUpdateRequest() 113 { 114 Name = key, 115 CountDiff = amount, 116 }; 117 var updateRequest = new UpdateCounterRequest() 118 { 119 CounterUpdateRequest = request, 120 }; 121 await client.UpdateCounterAsync(updateRequest, 122 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 123 // If there is no error, then the request was successful. 124 } 125 catch (RpcException ex) 126 { 127 LogError(ex, $"Unable to invoke IncrementCounter({key}, {amount})."); 128 throw; 129 } 130 } 131 132 /// <summary> 133 /// DecrementCounterAsync decreases the current count by the given nonnegative integer amount. 134 /// The Counter will not go below 0. Will execute the decrement operation against the current CRD value. 135 /// Throws error if the count is at 0 (to the latest knowledge of the SDK), and no decrement will occur. 136 /// </summary> 137 public async Task DecrementCounterAsync(string key, long amount) 138 { 139 if (amount < 0) 140 { 141 throw new ArgumentOutOfRangeException($"DecrementCounter amount must be a positive number, found {amount}"); 142 } 143 try 144 { 145 var request = new CounterUpdateRequest() 146 { 147 Name = key, 148 CountDiff = amount * -1, 149 }; 150 var updateRequest = new UpdateCounterRequest() 151 { 152 CounterUpdateRequest = request, 153 }; 154 await client.UpdateCounterAsync(updateRequest, 155 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 156 } 157 catch (RpcException ex) 158 { 159 LogError(ex, $"Unable to invoke DecrementCounter({key}, {amount})."); 160 throw; 161 } 162 } 163 164 /// <summary> 165 /// SetCounterCountAsync sets a count to the given value. Use with care, as this will 166 /// overwrite any previous invocations’ value. Cannot be greater than Capacity. 167 /// </summary> 168 public async Task SetCounterCountAsync(string key, long amount) 169 { 170 try 171 { 172 var request = new CounterUpdateRequest() 173 { 174 Name = key, 175 Count = amount, 176 }; 177 var updateRequest = new UpdateCounterRequest() 178 { 179 CounterUpdateRequest = request, 180 }; 181 await client.UpdateCounterAsync(updateRequest, 182 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 183 } 184 catch (RpcException ex) 185 { 186 LogError(ex, $"Unable to invoke SetCounterCount({key}, {amount})."); 187 throw; 188 } 189 } 190 191 /// <summary> 192 /// GetCounterCapacityAsync returns the Capacity for a Counter, given the Counter's key (name). 193 /// Throws error if the key was not predefined in the GameServer resource on creation. 194 /// </summary> 195 /// <returns>The Counter's capacity</returns> 196 public async Task<long> GetCounterCapacityAsync(string key) 197 { 198 try 199 { 200 var request = new GetCounterRequest() 201 { 202 Name = key, 203 }; 204 var counter = await client.GetCounterAsync(request, 205 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 206 return counter.Capacity; 207 } 208 catch (RpcException ex) 209 { 210 LogError(ex, $"Unable to invoke GetCounterCapacity({key})."); 211 throw; 212 } 213 } 214 215 /// <summary> 216 /// SetCounterCapacityAsync sets the capacity for the given Counter. 217 /// A capacity of 0 is no capacity. 218 /// </summary> 219 public async Task SetCounterCapacityAsync(string key, long amount) 220 { 221 try 222 { 223 var request = new CounterUpdateRequest() 224 { 225 Name = key, 226 Capacity = amount, 227 }; 228 var updateRequest = new UpdateCounterRequest() 229 { 230 CounterUpdateRequest = request, 231 }; 232 await client.UpdateCounterAsync(updateRequest, 233 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 234 } 235 catch (RpcException ex) 236 { 237 LogError(ex, $"Unable to invoke SetCounterCapacity({key}, {amount})."); 238 throw; 239 } 240 } 241 242 /// <summary> 243 /// GetListCapacityAsync returns the Capacity for a List, given the List's key (name). 244 /// Throws error if the key was not predefined in the GameServer resource on creation. 245 /// </summary> 246 /// <returns>The List's capacity</returns> 247 public async Task<long> GetListCapacityAsync(string key) 248 { 249 try 250 { 251 var request = new GetListRequest() 252 { 253 Name = key, 254 }; 255 var list = await client.GetListAsync(request, 256 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 257 return list.Capacity; 258 } 259 catch (RpcException ex) 260 { 261 LogError(ex, $"Unable to invoke GetListCapacity({key})."); 262 throw; 263 } 264 } 265 266 /// <summary> 267 /// SetListCapacityAsync sets the capacity for a given list. Capacity must be between 0 and 1000. 268 /// Throws error if the key was not predefined in the GameServer resource on creation. 269 /// </summary> 270 public async Task SetListCapacityAsync(string key, long amount) 271 { 272 try 273 { 274 var list = new List() 275 { 276 Name = key, 277 Capacity = amount, 278 }; 279 // FieldMask to update the capacity field only 280 var updateMask = new gProto.FieldMask() 281 { 282 Paths = { "capacity" }, 283 }; 284 var request = new UpdateListRequest() 285 { 286 List = list, 287 UpdateMask = updateMask, 288 }; 289 await client.UpdateListAsync(request, 290 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 291 } 292 catch (RpcException ex) 293 { 294 LogError(ex, $"Unable to invoke SetListCapacity({key}, {amount})."); 295 throw; 296 } 297 } 298 299 /// <summary> 300 /// ListContainsAsync returns if a string exists in a List's values list, given the List's key 301 /// and the string value. Search is case-sensitive. 302 /// Throws error if the key was not predefined in the GameServer resource on creation. 303 /// </summary> 304 /// <returns>True if the value is found in the List</returns> 305 public async Task<bool> ListContainsAsync(string key, string value) 306 { 307 try 308 { 309 var request = new GetListRequest() 310 { 311 Name = key, 312 }; 313 var list = await client.GetListAsync(request, 314 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 315 if (list.Values.Contains(value)) 316 { 317 return true; 318 }; 319 return false; 320 } 321 catch (RpcException ex) 322 { 323 LogError(ex, $"Unable to invoke ListContains({key}, {value})."); 324 throw; 325 } 326 } 327 328 /// <summary> 329 /// GetListLengthAsync returns the length of the Values list for a List, given the List's key. 330 /// Throws error if the key was not predefined in the GameServer resource on creation. 331 /// </summary> 332 /// <returns>The length of List's values array</returns> 333 public async Task<int> GetListLengthAsync(string key) 334 { 335 try 336 { 337 var request = new GetListRequest() 338 { 339 Name = key, 340 }; 341 var list = await client.GetListAsync(request, 342 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 343 return list.Values.Count; 344 } 345 catch (RpcException ex) 346 { 347 LogError(ex, $"Unable to invoke GetListLength({key})."); 348 throw; 349 } 350 } 351 352 /// <summary> 353 /// GetListValuesAsync returns the Values for a List, given the List's key (name). 354 /// Throws error if the key was not predefined in the GameServer resource on creation. 355 /// </summary> 356 /// <returns>The List's values array</returns> 357 public async Task<IList<string>> GetListValuesAsync(string key) 358 { 359 try 360 { 361 var request = new GetListRequest() 362 { 363 Name = key, 364 }; 365 var list = await client.GetListAsync(request, 366 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 367 return list.Values; 368 } 369 catch (RpcException ex) 370 { 371 LogError(ex, $"Unable to invoke GetListValues({key})."); 372 throw; 373 } 374 } 375 376 /// <summary> 377 /// AppendListValueAsync appends a string to a List's values list, given the List's key (name) 378 /// and the string value. Throws error if the string already exists in the list. 379 /// Throws error if the key was not predefined in the GameServer resource on creation. 380 /// </summary> 381 public async Task AppendListValueAsync(string key, string value) 382 { 383 try 384 { 385 var request = new AddListValueRequest() 386 { 387 Name = key, 388 Value = value, 389 }; 390 await client.AddListValueAsync(request, 391 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 392 } 393 catch (RpcException ex) 394 { 395 LogError(ex, $"Unable to invoke AppendListValue({key}, {value})."); 396 throw; 397 } 398 } 399 400 /// <summary> 401 /// DeleteListValueAsync removes a string from a List's values list, given the List's key 402 /// and the string value. Throws error if the string does not exist in the list. 403 /// Throws error if the key was not predefined in the GameServer resource on creation. 404 /// </summary> 405 public async Task DeleteListValueAsync(string key, string value) 406 { 407 try 408 { 409 var request = new RemoveListValueRequest() 410 { 411 Name = key, 412 Value = value, 413 }; 414 await client.RemoveListValueAsync(request, 415 deadline: DateTime.UtcNow.AddSeconds(RequestTimeoutSec), cancellationToken: ctoken); 416 } 417 catch (RpcException ex) 418 { 419 LogError(ex, $"Unable to invoke DeleteListValue({key}, {value})."); 420 throw; 421 } 422 } 423 424 public void Dispose() 425 { 426 if (_disposed) 427 { 428 return; 429 } 430 431 cts.Cancel(); 432 433 if (ownsCts) 434 { 435 cts.Dispose(); 436 } 437 438 _disposed = true; 439 GC.SuppressFinalize(this); 440 } 441 442 private void LogError(Exception ex, string message) 443 { 444 _logger?.LogError(ex, message); 445 } 446 } 447 }