sources/Google.Solutions.Apis/Compute/ResourceMetadataExtensions.cs (233 lines of code) (raw):

// // Copyright 2019 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Apis.Compute.v1; using Google.Apis.Compute.v1.Data; using Google.Solutions.Apis.Client; using Google.Solutions.Apis.Locator; using Google.Solutions.Common; using Google.Solutions.Common.Diagnostics; using Google.Solutions.Common.Util; using System; using System.Collections.Generic; using System.Net; using System.Threading; using System.Threading.Tasks; namespace Google.Solutions.Apis.Compute { /// <summary> /// Extension methods for mutating project and instance metadata. /// </summary> public static class ResourceMetadataExtensions { private const uint DefaultAttempts = 6; /// <summary> /// Modify metadata using optimistic concurrency control. /// </summary> private static async Task UpdateMetadataAsync( Func<Task<Metadata>> readMetadata, Func<Metadata, Task> writeMetadata, Action<Metadata> updateMetadata, uint maxAttempts) { for (var attempt = 1; ; attempt++) { // // NB. Metadata must be updated all-at-once. Therefore, // fetch the existing entries first before merging them // with the new entries. // var metadata = await readMetadata() .ConfigureAwait(false); updateMetadata(metadata); try { await writeMetadata(metadata) .ConfigureAwait(false); break; } catch (Exception e) when (e.Unwrap() is GoogleApiException apiException) { if (attempt == maxAttempts) { // // That's enough, give up. // CommonTraceSource.Log.TraceWarning( "SetMetadata failed with {0} (code error {1})", e.Message, apiException.Error?.Code); throw; } else if ( apiException.HttpStatusCode == HttpStatusCode.ServiceUnavailable || apiException.Error != null && apiException.Error.Code == 412) { // // 412 indicates a conflict, meaning we lost the // optimisitic concurrency control race agains // someone else. // // 503 indicates that the API is being flaky. // // In both cases, back off and retry the // read/update/write operation. // var backoff = TimeSpan.FromMilliseconds(10 * attempt); CommonTraceSource.Log.TraceWarning( "SetMetadata failed with {0} (code error {1}) - retrying after {2}", e.Message, apiException.Error?.Code, backoff); await Task .Delay(backoff) .ConfigureAwait(false); } else { CommonTraceSource.Log.TraceWarning( "Setting metadata failed {0} (code error {1})", e.Message, apiException.Error?.Code); throw; } } } } //--------------------------------------------------------------------- // Extension methods for modifying project metadata. //--------------------------------------------------------------------- /// <summary> /// Project/common instance metadata. /// </summary> public static async Task UpdateMetadataAsync( this ProjectsResource resource, string projectId, Action<Metadata> updateMetadata, CancellationToken token, uint maxAttempts = DefaultAttempts) { using (CommonTraceSource.Log.TraceMethod().WithParameters(projectId)) { await UpdateMetadataAsync( async () => { var project = await resource.Get(projectId) .ExecuteAsync(token) .ConfigureAwait(false); return project.CommonInstanceMetadata; }, async metadata => { await resource.SetCommonInstanceMetadata( metadata, projectId) .ExecuteAndAwaitOperationAsync( projectId, token) .ConfigureAwait(false); }, updateMetadata, maxAttempts) .ConfigureAwait(false); } } /// <summary> /// Adds or overwrite a metadata key/value pair. /// Existing metadata is kept as is. /// </summary> public static Task AddMetadataAsync( this ProjectsResource resource, string projectId, string key, string value, CancellationToken token) { return AddMetadataAsync( resource, projectId, new Metadata() { Items = new List<Metadata.ItemsData>() { new Metadata.ItemsData() { Key = key, Value = value } } }, token); } public static Task AddMetadataAsync( this ProjectsResource resource, string projectId, Metadata metadata, CancellationToken token) { return UpdateMetadataAsync( resource, projectId, existingMetadata => { existingMetadata.Add(metadata); }, token); } //--------------------------------------------------------------------- // Extension methods for modifying instance metadata. //--------------------------------------------------------------------- /// <summary> /// Modify instance metadata. /// </summary> public static async Task UpdateMetadataAsync( this InstancesResource resource, InstanceLocator instanceRef, Action<Metadata> updateMetadata, CancellationToken token, uint maxAttempts = DefaultAttempts) { using (CommonTraceSource.Log.TraceMethod().WithParameters(instanceRef)) { await UpdateMetadataAsync( async () => { var instance = await resource.Get( instanceRef.ProjectId, instanceRef.Zone, instanceRef.Name) .ExecuteAsync(token) .ConfigureAwait(false); return instance.Metadata; }, async metadata => { await resource.SetMetadata( metadata, instanceRef.ProjectId, instanceRef.Zone, instanceRef.Name) .ExecuteAndAwaitOperationAsync( instanceRef.ProjectId, token) .ConfigureAwait(false); }, updateMetadata, maxAttempts) .ConfigureAwait(false); } } /// <summary> /// Adds or overwrite a metadata key/value pair. /// Existing metadata is kept as is. /// </summary> public static Task AddMetadataAsync( this InstancesResource resource, InstanceLocator instanceRef, string key, string value, CancellationToken token) { return AddMetadataAsync( resource, instanceRef, new Metadata() { Items = new List<Metadata.ItemsData>() { new Metadata.ItemsData() { Key = key, Value = value } } }, token); } public static Task AddMetadataAsync( this InstancesResource resource, InstanceLocator instanceRef, Metadata metadata, CancellationToken token) { return UpdateMetadataAsync( resource, instanceRef, existingMetadata => { existingMetadata.Add(metadata); }, token); } //--------------------------------------------------------------------- // Extension methods for reading metadata. //--------------------------------------------------------------------- public static bool? GetFlag(this Instance instance, Project project, string flag) { // // NB. The instance value always takes precedence, // even if it's false. // var instanceValue = instance.Metadata.GetFlag(flag); if (instanceValue != null) { return instanceValue.Value; } var projectValue = project.CommonInstanceMetadata.GetFlag(flag); if (projectValue != null) { return projectValue.Value; } return null; } public static bool? GetFlag(this Project project, string flag) { return project.CommonInstanceMetadata.GetFlag(flag); } } }