|
16 | 16 |
|
17 | 17 | package com.google.cloud.resourcemanager.spi.v1beta1; |
18 | 18 |
|
| 19 | +import static com.google.cloud.RetryHelper.runWithRetries; |
19 | 20 | import static com.google.common.base.MoreObjects.firstNonNull; |
20 | 21 | import static java.net.HttpURLConnection.HTTP_FORBIDDEN; |
21 | 22 | import static java.net.HttpURLConnection.HTTP_NOT_FOUND; |
22 | 23 |
|
23 | 24 | import com.google.api.client.http.HttpRequestInitializer; |
24 | 25 | import com.google.api.client.http.HttpTransport; |
| 26 | +import com.google.api.client.json.JsonFactory; |
25 | 27 | import com.google.api.client.json.jackson.JacksonFactory; |
26 | | -import com.google.api.services.cloudresourcemanager.Cloudresourcemanager; |
| 28 | +import com.google.api.core.ApiClock; |
| 29 | +import com.google.api.gax.retrying.ResultRetryAlgorithm; |
| 30 | +import com.google.api.gax.retrying.RetrySettings; |
| 31 | +import com.google.api.gax.retrying.TimedAttemptSettings; |
| 32 | +import com.google.api.services.cloudresourcemanager.CloudResourceManager; |
27 | 33 | import com.google.api.services.cloudresourcemanager.model.GetIamPolicyRequest; |
28 | 34 | import com.google.api.services.cloudresourcemanager.model.ListProjectsResponse; |
| 35 | +import com.google.api.services.cloudresourcemanager.model.Operation; |
29 | 36 | import com.google.api.services.cloudresourcemanager.model.Policy; |
30 | 37 | import com.google.api.services.cloudresourcemanager.model.Project; |
31 | 38 | import com.google.api.services.cloudresourcemanager.model.SetIamPolicyRequest; |
| 39 | +import com.google.api.services.cloudresourcemanager.model.Status; |
32 | 40 | import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsRequest; |
33 | 41 | import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsResponse; |
| 42 | +import com.google.api.services.cloudresourcemanager.model.UndeleteProjectRequest; |
34 | 43 | import com.google.cloud.Tuple; |
| 44 | +import com.google.cloud.http.BaseHttpServiceException; |
35 | 45 | import com.google.cloud.http.HttpTransportOptions; |
36 | 46 | import com.google.cloud.resourcemanager.ResourceManagerException; |
37 | 47 | import com.google.cloud.resourcemanager.ResourceManagerOptions; |
38 | 48 | import com.google.common.collect.ImmutableList; |
| 49 | +import com.google.common.collect.ImmutableMap; |
39 | 50 | import com.google.common.collect.ImmutableSet; |
40 | 51 | import java.io.IOException; |
41 | 52 | import java.util.List; |
42 | 53 | import java.util.Map; |
43 | 54 | import java.util.Set; |
| 55 | +import java.util.concurrent.Callable; |
| 56 | +import org.threeten.bp.Duration; |
44 | 57 |
|
45 | 58 | public class HttpResourceManagerRpc implements ResourceManagerRpc { |
46 | 59 |
|
47 | | - private final Cloudresourcemanager resourceManager; |
| 60 | + private static final JsonFactory JSON_FACTORY = |
| 61 | + new JacksonFactory(); |
| 62 | + |
| 63 | + // See doc of create() for more details: |
| 64 | + // https://developers.google.com/resources/api-libraries/documentation/cloudresourcemanager/v1/java/latest/com/google/api/services/cloudresourcemanager/CloudResourceManager.Projects.html#create(com.google.api.services.cloudresourcemanager.model.Project) |
| 65 | + private static final RetrySettings CREATE_RETRY_SETTINGS = |
| 66 | + RetrySettings.newBuilder() |
| 67 | + // SLO permits 30s at 90th percentile, 4x it for total limit. |
| 68 | + // Observed latency is much lower: 11s at 95th percentile. |
| 69 | + .setTotalTimeout(Duration.ofMinutes(2)) |
| 70 | + // Linked doc recommends polling at 5th second. |
| 71 | + .setInitialRetryDelay(Duration.ofSeconds(5)) |
| 72 | + .setRetryDelayMultiplier(1.5) |
| 73 | + // Observed P95 latency is 11s. We probably shouldn't sleep longer than this. |
| 74 | + .setMaxRetryDelay(Duration.ofSeconds(11)) |
| 75 | + .setJittered(true) |
| 76 | + .setInitialRpcTimeout(Duration.ofSeconds(5)) |
| 77 | + .setMaxRpcTimeout(Duration.ofSeconds(5)) |
| 78 | + .build(); |
| 79 | + |
| 80 | + // reference: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto |
| 81 | + private static final ImmutableMap<Integer, Integer> RPC_TO_HTTP_CODES = |
| 82 | + ImmutableMap.<Integer, Integer>builder() |
| 83 | + .put(0, 200) |
| 84 | + .put(1, 499) |
| 85 | + .put(2, 500) |
| 86 | + .put(3, 400) |
| 87 | + .put(4, 504) |
| 88 | + .put(5, 404) |
| 89 | + .put(6, 409) |
| 90 | + .put(7, 403) |
| 91 | + .put(16, 401) |
| 92 | + .put(8, 429) |
| 93 | + .put(9, 400) |
| 94 | + .put(10, 409) |
| 95 | + .put(11, 400) |
| 96 | + .put(12, 501) |
| 97 | + .put(13, 500) |
| 98 | + .put(14, 503) |
| 99 | + .put(15, 500) |
| 100 | + .build(); |
| 101 | + |
| 102 | + private static final ResultRetryAlgorithm<Operation> OPERATION_HANDLER = |
| 103 | + new ResultRetryAlgorithm<Operation>() { |
| 104 | + @Override |
| 105 | + public TimedAttemptSettings createNextAttempt( |
| 106 | + Throwable prevThrowable, Operation prevResponse, TimedAttemptSettings prevSettings) { |
| 107 | + return null; |
| 108 | + } |
| 109 | + |
| 110 | + @Override |
| 111 | + public boolean shouldRetry(Throwable prevThrowable, Operation prevOp) { |
| 112 | + if (prevThrowable == null) { |
| 113 | + return prevOp.getDone() == null || !prevOp.getDone(); |
| 114 | + } |
| 115 | + return prevThrowable instanceof ResourceManagerException |
| 116 | + && ((ResourceManagerException) prevThrowable).isRetryable(); |
| 117 | + } |
| 118 | + }; |
| 119 | + |
| 120 | + private final CloudResourceManager resourceManager; |
| 121 | + private final ApiClock clock; |
48 | 122 |
|
49 | 123 | public HttpResourceManagerRpc(ResourceManagerOptions options) { |
50 | 124 | HttpTransportOptions transportOptions = (HttpTransportOptions) options.getTransportOptions(); |
51 | 125 | HttpTransport transport = transportOptions.getHttpTransportFactory().create(); |
52 | 126 | HttpRequestInitializer initializer = transportOptions.getHttpRequestInitializer(options); |
53 | 127 | resourceManager = |
54 | | - new Cloudresourcemanager.Builder(transport, new JacksonFactory(), initializer) |
| 128 | + new CloudResourceManager.Builder(transport, new JacksonFactory(), initializer) |
55 | 129 | .setRootUrl(options.getHost()) |
56 | 130 | .setApplicationName(options.getApplicationName()) |
57 | 131 | .build(); |
| 132 | + clock = options.getClock(); |
58 | 133 | } |
59 | 134 |
|
60 | 135 | private static ResourceManagerException translate(IOException exception) { |
61 | 136 | return new ResourceManagerException(exception); |
62 | 137 | } |
63 | 138 |
|
| 139 | + private static ResourceManagerException translate(Status status) { |
| 140 | + Integer code = RPC_TO_HTTP_CODES.get(status.getCode()); |
| 141 | + if (code == null) { |
| 142 | + code = BaseHttpServiceException.UNKNOWN_CODE; |
| 143 | + } |
| 144 | + return new ResourceManagerException(code, status.getMessage()); |
| 145 | + } |
| 146 | + |
64 | 147 | @Override |
65 | 148 | public Project create(Project project) { |
| 149 | + final Operation operation; |
| 150 | + try { |
| 151 | + operation = resourceManager.projects().create(project).execute(); |
| 152 | + } catch (IOException ex) { |
| 153 | + throw translate(ex); |
| 154 | + } |
| 155 | + |
| 156 | + Operation finishedOp = |
| 157 | + runWithRetries( |
| 158 | + new Callable<Operation>() { |
| 159 | + @Override |
| 160 | + public Operation call() { |
| 161 | + try { |
| 162 | + return resourceManager.operations().get(operation.getName()).execute(); |
| 163 | + } catch (IOException ex) { |
| 164 | + throw translate(ex); |
| 165 | + } |
| 166 | + } |
| 167 | + }, |
| 168 | + CREATE_RETRY_SETTINGS, |
| 169 | + OPERATION_HANDLER, |
| 170 | + clock); |
| 171 | + if (finishedOp.getError() != null) { |
| 172 | + throw translate(finishedOp.getError()); |
| 173 | + } |
| 174 | + |
| 175 | + // NOTE(pongad): Operation.getResponse() returns a Map<String, Object>. |
| 176 | + // 1. `(Project) finishedOp.getResponse()` doesn't work, |
| 177 | + // because JSON deserializer in execute() didn't know to create a Project object. |
| 178 | + // 2. `new Project().putAll(finishedOp.getResponse())` doesn't work either. |
| 179 | + // 64-bit integers are sent as strings in JSON, |
| 180 | + // so execute(), not knowing the type, parses it as String, not Long. |
66 | 181 | try { |
67 | | - return resourceManager.projects().create(project).execute(); |
| 182 | + String responseTxt = JSON_FACTORY.toString(finishedOp.getResponse()); |
| 183 | + return JSON_FACTORY.fromString(responseTxt, Project.class); |
68 | 184 | } catch (IOException ex) { |
69 | 185 | throw translate(ex); |
70 | 186 | } |
@@ -117,7 +233,7 @@ public Tuple<String, Iterable<Project>> list(Map<Option, ?> options) { |
117 | 233 | @Override |
118 | 234 | public void undelete(String projectId) { |
119 | 235 | try { |
120 | | - resourceManager.projects().undelete(projectId).execute(); |
| 236 | + resourceManager.projects().undelete(projectId, new UndeleteProjectRequest()).execute(); |
121 | 237 | } catch (IOException ex) { |
122 | 238 | throw translate(ex); |
123 | 239 | } |
|
0 commit comments