
com.vmware.photon.controller.model.adapters.gcp.stats.GCPStatsService Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2015-2016 VMware, Inc. All Rights Reserved.
*
* Licensed 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.
*/
package com.vmware.photon.controller.model.adapters.gcp.stats;
import static com.vmware.photon.controller.model.util.PhotonModelUriUtils.createInventoryUri;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import com.vmware.photon.controller.model.adapterapi.ComputeStatsRequest;
import com.vmware.photon.controller.model.adapterapi.ComputeStatsResponse.ComputeStats;
import com.vmware.photon.controller.model.adapters.gcp.GCPUriPaths;
import com.vmware.photon.controller.model.adapters.gcp.constants.GCPConstants;
import com.vmware.photon.controller.model.adapters.gcp.podo.authorization.GCPAccessTokenResponse;
import com.vmware.photon.controller.model.adapters.gcp.podo.stats.GCPMetricResponse;
import com.vmware.photon.controller.model.adapters.gcp.podo.stats.TimeSeries;
import com.vmware.photon.controller.model.adapters.gcp.utils.GCPStatsNormalizer;
import com.vmware.photon.controller.model.adapters.gcp.utils.GCPUtils;
import com.vmware.photon.controller.model.adapters.gcp.utils.JSONWebToken;
import com.vmware.photon.controller.model.adapters.util.AdapterUtils;
import com.vmware.photon.controller.model.adapters.util.TaskManager;
import com.vmware.photon.controller.model.resources.ComputeService.ComputeStateWithDescription;
import com.vmware.photon.controller.model.resources.ResourceGroupService.ResourceGroupState;
import com.vmware.photon.controller.model.security.util.EncryptionUtils;
import com.vmware.photon.controller.model.tasks.monitoring.SingleResourceStatsCollectionTaskService.SingleResourceStatsCollectionTaskState;
import com.vmware.photon.controller.model.tasks.monitoring.SingleResourceStatsCollectionTaskService.SingleResourceTaskCollectionStage;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.ServiceStats.ServiceStat;
import com.vmware.xenon.common.StatelessService;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.services.common.AuthCredentialsService;
import com.vmware.xenon.services.common.AuthCredentialsService.AuthCredentialsServiceState;
/**
* Metrics collection service for Google Cloud Platform. Gets authenticated using OAuth
* API. Collects instance level and host level metrics for Google Compute Engine instances using
* Stackdriver monitoring API.
*/
public class GCPStatsService extends StatelessService {
public static final String SELF_LINK = GCPUriPaths.GCP_STATS_ADAPTER;
/**
* Stores GCP metric names and their corresponding units.
* Metric units are not provided as a part of the response by the API, hence they are
* hard coded.
* TODO: VSYM-1462 - Get metric units by making a request to monitoring API
*/
public static final String[][] METRIC_NAMES_UNITS = {{GCPConstants.CPU_UTILIZATION,
GCPConstants.UNIT_PERCENT}, {GCPConstants.DISK_READ_BYTES, GCPConstants.UNIT_BYTE},
{GCPConstants.DISK_READ_OPERATIONS, GCPConstants.UNIT_COUNT},
{GCPConstants.DISK_WRITE_BYTES, GCPConstants.UNIT_BYTE},
{GCPConstants.DISK_WRITE_OPERATIONS, GCPConstants.UNIT_COUNT},
{GCPConstants.NETWORK_IN_BYTES, GCPConstants.UNIT_BYTE},
{GCPConstants.NETWORK_IN_PACKETS, GCPConstants.UNIT_COUNT},
{GCPConstants.NETWORK_OUT_BYTES, GCPConstants.UNIT_BYTE},
{GCPConstants.NETWORK_OUT_PACKETS, GCPConstants.UNIT_COUNT}};
public GCPStatsService() {
super.toggleOption(ServiceOption.INSTRUMENTATION, true);
}
/**
* Stages of GCP stats collection
*/
private enum StatsCollectionStage {
/**
* Default first stage for the service. Collecting the VM description.
*/
VM_DESC,
/**
* Collecting compute host description.
*/
PARENT_VM_DESC,
/**
* Collecting credentials from AuthCredentialService.
*/
CREDENTIALS,
/**
* Collecting project name from Resource group.
*/
PROJECT_ID,
/**
* Getting access token using OAuth.
*/
ACCESS_TOKEN,
/**
* Collecting stats by making requests to Stackdriver monitoring API.
*/
STATS,
/**
* Error stage
*/
ERROR,
/**
* Stage to handle mock request, directly patches back to parent.
*/
FINISHED
}
/**
* Data holder class for GCPStatsService. Stores all the fields used by the service.
*/
private class GCPStatsDataHolder {
public ComputeStateWithDescription computeDesc;
public ComputeStateWithDescription parentDesc;
public StatsCollectionStage stage;
public AuthCredentialsService.AuthCredentialsServiceState parentAuth;
public ComputeStatsRequest statsRequest;
public ComputeStats statsResponse;
public Throwable error;
public AtomicInteger numResponses = new AtomicInteger(0);
public boolean isComputeHost;
public String userEmail;
public String privateKey;
public String accessToken;
public String projectId;
public String instanceId;
public Operation gcpStatsCollectionOperation;
public TaskManager taskManager;
public GCPStatsDataHolder(Operation op) {
this.gcpStatsCollectionOperation = op;
this.statsResponse = new ComputeStats();
// create a thread safe map to hold stats values for resource
this.statsResponse.statValues = new ConcurrentSkipListMap<>();
}
}
/**
* Convert timestamp associated with metric from response format (RFC 3339) to microseconds.
* @param timestamp Timestamp of metric in RFC 3339 format.
* @return Timestamp of metric in microseconds.
* @throws ParseException
*/
private long getTimestampInMicros(String timestamp) throws ParseException {
SimpleDateFormat dateFormat = new SimpleDateFormat(GCPConstants.TIME_INTERVAL_FORMAT);
dateFormat.setTimeZone(TimeZone.getTimeZone(GCPConstants.UTC_TIMEZONE_ID));
Date date = dateFormat.parse(timestamp);
long time = TimeUnit.MILLISECONDS.toMicros(date.getTime());
return time;
}
/**
* Gets the start time parameter required for metric request URI.
* New SimpleDateFromat and Date instances are created for every call as these classes are
* not thread safe.
* @return startTime parameter of the metric request URI in RFC 3339 format.
*/
private static String getStartTime() {
SimpleDateFormat dateFormat = new SimpleDateFormat(GCPConstants.TIME_INTERVAL_FORMAT);
dateFormat.setTimeZone(TimeZone.getTimeZone(GCPConstants.UTC_TIMEZONE_ID));
/*
* Subtract 4 minutes from current time.
* Currently, 3 minutes old metrics are obtained to account for latency.
* Request for more recent metrics mostly results in empty response body.
*/
Date date = new Date(TimeUnit.MICROSECONDS.toMillis(Utils.getNowMicrosUtc()) - GCPConstants.START_TIME_MILLIS);
return dateFormat.format(date);
}
/**
* Gets the end time parameter required for metric request URI in RFC 3339 format.
* New SimpleDateFromat and Date instances are created for every call as these classes are
* not thread safe.
* @return endTime parameter of the metric request URI in RFC 3339 format.
*/
private static String getEndTime() {
SimpleDateFormat dateFormat = new SimpleDateFormat(GCPConstants.TIME_INTERVAL_FORMAT);
dateFormat.setTimeZone(TimeZone.getTimeZone(GCPConstants.UTC_TIMEZONE_ID));
/*
* Subtract 3 minutes from current time.
* Data points collected by the monitoring API are spaced 60 seconds apart, hence we set
* interval to 60 seconds to obtain one data point at the most.
* This sometimes results in empty response body, if the interval contains no data points.
*/
Date date = new Date(TimeUnit.MICROSECONDS.toMillis(Utils.getNowMicrosUtc()) - GCPConstants.END_TIME_MILLIS);
return dateFormat.format(date);
}
/**
* Builds the metric request filter value string.
* @param metricName Name of the metric to be requested.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @return Metric request filter value string
*/
private String getRequestFilterValue(String metricName, GCPStatsDataHolder statsData) {
String filterValue;
/* If the given resource is a compute host, do not add instance ID parameter in
* the filter value.
* If the given resource is a VM, add instance ID parameter in the filter value.
*/
if (statsData.isComputeHost) {
filterValue = GCPConstants.METRIC_TYPE_FILTER + "=\""
+ GCPConstants.METRIC_NAME_PREFIX + metricName + "\"";
} else {
filterValue = GCPConstants.METRIC_TYPE_FILTER + "=\""
+ GCPConstants.METRIC_NAME_PREFIX + metricName + "\""
+ "+AND+" + GCPConstants.INSTANCE_NAME_FILTER + "=\""
+ statsData.instanceId + "\"";
}
return filterValue;
}
/**
* Method for building VM level stats metric request URI.
* The URI has nested key, value pairs in the query.
* Use of UriUtils methods for creating the entire query results in double encoding, thus
* manual string concatenation is used.
* @param metricName Name of the metric to be requested.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @return Metric request URI for VM level stats.
*/
private URI getRequestUriForVM(String metricName, GCPStatsDataHolder statsData) {
try {
URI baseUri = new URI(GCPConstants.MONITORING_API_URI
+ statsData.projectId + GCPConstants.TIMESERIES_PREFIX);
String filterValue = getRequestFilterValue(metricName, statsData);
URI uri = UriUtils.extendUriWithQuery(baseUri, GCPConstants.FILTER_KEY, filterValue,
GCPConstants.INTERVAL_START_TIME, getStartTime(),
GCPConstants.INTERVAL_END_TIME, getEndTime());
return uri;
} catch (URISyntaxException e) {
handleError(statsData, e);
return null;
}
}
/**
* Method for building host level stats metric request URI.
* The URI has nested key, value pairs in the query.
* Use of UriUtils methods for creating the entire query results in double encoding, thus
* manual string concatenation is used.
* @param metricName Name of the metric to be requested.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @return Metric request URI for host level stats.
*/
private URI getRequestUriForHost(String metricName, GCPStatsDataHolder statsData) {
try {
URI baseUri = new URI(GCPConstants.MONITORING_API_URI
+ statsData.projectId + GCPConstants.TIMESERIES_PREFIX);
String filterValue = getRequestFilterValue(metricName, statsData);
URI uri;
/*
* Values for aggregation enums are different for CPU Utilization and other metrics.
* CPU Utilization readings are instantaneous, mean of all data points is taken
* during aggregation.
* All other stats are observed over time, sum of all data points is taken during
* aggregation.
* Thus, build a different URI if the metric is CPU Utilization.
*/
if (metricName.equals(GCPConstants.CPU_UTILIZATION)) {
uri = UriUtils.extendUriWithQuery(baseUri, GCPConstants.AGGREGATION_ALIGNMENT_PERIOD,
GCPConstants.AGGREGATION_ALIGNMENT_PERIOD_VALUE,
GCPConstants.AGGREGATION_CROSS_SERIES_REDUCER, GCPConstants.CPU_UTIL_CROSS_SERIES_REDUCER_VALUE,
GCPConstants.AGGREGATION_PER_SERIES_ALIGNER, GCPConstants.CPU_UTIL_PER_SERIES_ALIGNER_VALUE,
GCPConstants.FILTER_KEY, filterValue,
GCPConstants.INTERVAL_START_TIME, getStartTime(),
GCPConstants.INTERVAL_END_TIME, getEndTime());
} else {
uri = UriUtils.extendUriWithQuery(baseUri, GCPConstants.AGGREGATION_ALIGNMENT_PERIOD,
GCPConstants.AGGREGATION_ALIGNMENT_PERIOD_VALUE,
GCPConstants.AGGREGATION_CROSS_SERIES_REDUCER, GCPConstants.CROSS_SERIES_REDUCER_VALUE,
GCPConstants.AGGREGATION_PER_SERIES_ALIGNER, GCPConstants.PER_SERIES_ALIGNER_VALUE,
GCPConstants.FILTER_KEY, filterValue,
GCPConstants.INTERVAL_START_TIME, getStartTime(),
GCPConstants.INTERVAL_END_TIME, getEndTime());
}
return uri;
} catch (URISyntaxException e) {
handleError(statsData, e);
return null;
}
}
/**
* The REST PATCH request handler. This is the entry of starting stats collection.
* @param patch Operation which should contain request body.
*/
@Override
public void handlePatch(Operation patch) {
setOperationHandlerInvokeTimeStat(patch);
if (!patch.hasBody()) {
patch.fail(new IllegalArgumentException("body is required"));
return;
}
patch.complete();
ComputeStatsRequest statsRequest = patch.getBody(ComputeStatsRequest.class);
GCPStatsDataHolder statsData = new GCPStatsDataHolder(patch);
statsData.statsRequest = statsRequest;
statsData.taskManager = new TaskManager(this, statsRequest.taskReference,
statsRequest.resourceLink());
// If mock mode is enabled, patch back to the parent.
if (statsData.statsRequest.isMockRequest) {
statsData.stage = StatsCollectionStage.FINISHED;
handleStatsRequest(statsData);
} else {
statsData.stage = StatsCollectionStage.VM_DESC;
handleStatsRequest(statsData);
}
}
/**
* The flow for dealing with each stage in the service.
* @param statsData The GCPStatsDataHolder instance which decides the current stage.
*/
public void handleStatsRequest(GCPStatsDataHolder statsData) {
switch (statsData.stage) {
case VM_DESC:
getVMDescription(statsData, StatsCollectionStage.PARENT_VM_DESC,
StatsCollectionStage.CREDENTIALS);
break;
case PARENT_VM_DESC:
getParentVMDescription(statsData, StatsCollectionStage.CREDENTIALS);
break;
case CREDENTIALS:
getParentAuth(statsData, StatsCollectionStage.PROJECT_ID);
break;
case PROJECT_ID:
getProjectId(statsData, StatsCollectionStage.ACCESS_TOKEN);
break;
case ACCESS_TOKEN:
try {
getAccessToken(statsData, StatsCollectionStage.STATS);
} catch (GeneralSecurityException | IOException e) {
handleError(statsData, e);
return;
}
break;
case STATS:
getStats(statsData, StatsCollectionStage.FINISHED);
break;
case ERROR:
statsData.taskManager.patchTaskToFailure(statsData.error);
break;
case FINISHED:
// Patch status to parent task
statsData.taskManager.finishTask();
break;
default:
String err = String.format("Unknown GCP stats collection stage %s ", statsData.stage.toString());
logSevere(err);
statsData.error = new IllegalStateException(err);
statsData.gcpStatsCollectionOperation.fail(statsData.error);
// Patch failure back to parent task
statsData.taskManager.patchTaskToFailure(statsData.error);
}
}
/**
* Gets the description of the VM for which stats are to be collected.
* Sets the VM ID, required for making metric requests, in current GCPStatsDataHolder
* instance.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param nextStage The next stage of StatsCollectionStage for the service.
*/
private void getVMDescription(GCPStatsDataHolder statsData,
StatsCollectionStage nextStageForVM,
StatsCollectionStage nextStageForComputeHost) {
Consumer onSuccess = (op) -> {
statsData.computeDesc = op.getBody(ComputeStateWithDescription.class);
statsData.instanceId = statsData.computeDesc.id;
statsData.isComputeHost = AdapterUtils.isComputeHost(statsData.computeDesc);
// If the given resource is a compute host, directly get parent auth.
if (statsData.isComputeHost) {
statsData.stage = nextStageForComputeHost;
} else {
statsData.stage = nextStageForVM;
}
handleStatsRequest(statsData);
};
URI computeUri = UriUtils.extendUriWithQuery(statsData.statsRequest.resourceReference,
UriUtils.URI_PARAM_ODATA_EXPAND, Boolean.TRUE.toString());
AdapterUtils.getServiceState(this, computeUri, onSuccess, getFailureConsumer(statsData));
}
/**
* Gets the description of the compute host corresponding to the VM for which stats
* are to be collected.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param nextStage The next stage of StatsCollectionStage for the service.
*/
private void getParentVMDescription(GCPStatsDataHolder statsData, StatsCollectionStage nextStage) {
Consumer onSuccess = (op) -> {
statsData.parentDesc = op.getBody(ComputeStateWithDescription.class);
statsData.stage = nextStage;
handleStatsRequest(statsData);
};
URI computeUri = UriUtils.extendUriWithQuery(
UriUtils.buildUri(getHost(), statsData.computeDesc.parentLink),
UriUtils.URI_PARAM_ODATA_EXPAND, Boolean.TRUE.toString());
AdapterUtils.getServiceState(this, computeUri, onSuccess, getFailureConsumer(statsData));
}
/**
* Gets the credentials for the corresponding compute host from AuthCredentialService.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param nextStage The next stage of StatsCollectionStage for the service.
*/
private void getParentAuth(GCPStatsDataHolder statsData, StatsCollectionStage nextStage) {
Consumer onSuccess = (op) -> {
statsData.parentAuth = op.getBody(AuthCredentialsServiceState.class);
statsData.userEmail = statsData.parentAuth.userEmail;
statsData.privateKey = EncryptionUtils.decrypt(statsData.parentAuth.privateKey);
statsData.stage = nextStage;
handleStatsRequest(statsData);
};
String authLink;
if (statsData.isComputeHost) {
authLink = statsData.computeDesc.description.authCredentialsLink;
} else {
authLink = statsData.parentDesc.description.authCredentialsLink;
}
AdapterUtils.getServiceState(this, createInventoryUri(this.getHost(), authLink), onSuccess,
getFailureConsumer(statsData));
}
/**
* Gets the project name for the corresponding compute host.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param nextStage The next stage of StatsCollectionStage for the service.
*/
private void getProjectId(GCPStatsDataHolder statsData, StatsCollectionStage nextStage) {
Consumer onSuccess = (op) -> {
ResourceGroupState rgs = op.getBody(ResourceGroupState.class);
statsData.projectId = rgs.name;
statsData.stage = nextStage;
handleStatsRequest(statsData);
};
ArrayList groupLink;
/*
* If given resource is a compute host, we directly get groupLinks from the resource
* description.
*/
if (statsData.isComputeHost) {
groupLink = new ArrayList<>(statsData.computeDesc.description.groupLinks);
} else {
groupLink = new ArrayList<>(statsData.parentDesc.description.groupLinks);
}
AdapterUtils.getServiceState(this, groupLink.get(0), onSuccess, getFailureConsumer(statsData));
}
/**
* Gets the access token required for making requests to the monitoring API.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param nextStage The next stage of StatsCollectionStage for the service.
* @throws GeneralSecurityException
* @throws IOException
*/
private void getAccessToken(GCPStatsDataHolder statsData, StatsCollectionStage nextStage)
throws GeneralSecurityException, IOException {
Consumer onSuccess = (response) -> {
statsData.accessToken = response.access_token;
statsData.stage = nextStage;
handleStatsRequest(statsData);
};
JSONWebToken jwt = new JSONWebToken(statsData.userEmail, GCPConstants.SCOPES,
GCPUtils.privateKeyFromPkcs8(statsData.privateKey));
String assertion = jwt.getAssertion();
GCPUtils.getAccessToken(this, assertion, onSuccess, getFailureConsumer(statsData));
}
/**
* Makes request to the monitoring API to get stats.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param nextStage The next stage of StatsCollectionStage for the service.
*/
private void getStats(GCPStatsDataHolder statsData, StatsCollectionStage nextStage) {
for (String[] metricInfo : METRIC_NAMES_UNITS) {
URI uri;
/*
* Do not specify instance id in the metric request URI if the given resource is
* a compute host and specify aggregation parameters in the metric request URI
* in order to get host level stats.
*/
if (statsData.isComputeHost) {
uri = getRequestUriForHost(metricInfo[0], statsData);
} else {
uri = getRequestUriForVM(metricInfo[0], statsData);
}
if (uri == null) {
statsData.error = new IllegalStateException("The request URI is null.");
statsData.stage = StatsCollectionStage.ERROR;
handleStatsRequest(statsData);
}
Operation.createGet(uri).addRequestHeader(Operation.AUTHORIZATION_HEADER,
GCPConstants.AUTH_HEADER_BEARER_PREFIX + statsData.accessToken)
.setCompletion((o, e) -> {
if (e != null) {
handleError(statsData, e);
return;
}
GCPMetricResponse response = o.getBody(GCPMetricResponse.class);
storeAndSendStats(statsData, metricInfo, response, nextStage);
}).sendWith(this);
}
}
/**
* Stores the stats in current GCPStatsDataHolder instance and patches back the response
* to the caller task.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param metricInfo Metric name and unit pair from METRIC_NAMES_UNITS array.
* @param response The response from the monitoring API associated with metric in
* the current metricInfo pair.
* @param nextStage The next stage of StatsCollectionStage for the service.
*/
private void storeAndSendStats(GCPStatsDataHolder statsData, String[] metricInfo,
GCPMetricResponse response, StatsCollectionStage nextStage) {
ServiceStat stat = new ServiceStat();
List datapoint = new ArrayList<>();
if (response.timeSeries != null) {
TimeSeries ts = response.timeSeries[0];
stat.latestValue = ts.points[0].value.int64Value == null ?
Double.parseDouble(ts.points[0].value.doubleValue) :
Double.parseDouble(ts.points[0].value.int64Value);
stat.unit = GCPStatsNormalizer.getNormalizedUnitValue(metricInfo[1]);
try {
stat.sourceTimeMicrosUtc = getTimestampInMicros(ts.points[0]
.interval.startTime);
} catch (ParseException e) {
handleError(statsData, e);
return;
}
datapoint = Collections.singletonList(stat);
}
statsData.statsResponse.statValues.put(GCPStatsNormalizer.getNormalizedStatKeyValue
(metricInfo[0]), datapoint);
// After all the metrics are collected, send them as a response to the caller task.
if (statsData.numResponses.incrementAndGet() == METRIC_NAMES_UNITS.length) {
SingleResourceStatsCollectionTaskState respBody = new SingleResourceStatsCollectionTaskState();
statsData.statsResponse.computeLink = statsData.computeDesc.documentSelfLink;
respBody.taskStage = SingleResourceTaskCollectionStage.valueOf(statsData.statsRequest.nextStage);
respBody.statsList = Collections.singletonList(statsData.statsResponse);
setOperationDurationStat(statsData.gcpStatsCollectionOperation);
statsData.gcpStatsCollectionOperation.complete();
respBody.statsAdapterReference = UriUtils.buildUri(getHost(), SELF_LINK);
this.sendRequest(Operation.createPatch(statsData.statsRequest.taskReference)
.setBody(respBody));
}
}
/**
* Error handler for GCPStatsService.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @param e The throwable associated with error.
*/
private void handleError(GCPStatsDataHolder statsData, Throwable e) {
logSevere(e);
statsData.error = e;
statsData.stage = StatsCollectionStage.ERROR;
handleStatsRequest(statsData);
}
/**
* Failure consumer for AdapterUtils.getServiceState method.
* @param statsData The GCPStatsDataHolder instance containing statsRequest.
* @return Throwable
*/
private Consumer getFailureConsumer(GCPStatsDataHolder statsData) {
return ((t) -> {
statsData.taskManager.patchTaskToFailure(t);
});
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy