1616
1717package io .cdap .cdap .runtime .spi .provisioner .dataproc ;
1818
19+ import com .google .api .gax .grpc .GrpcStatusCode ;
20+ import com .google .api .gax .rpc .ApiException ;
21+ import com .google .api .gax .rpc .StatusCode ;
1922import com .google .cloud .dataproc .v1 .ClusterOperationMetadata ;
2023import com .google .cloud .dataproc .v1 .ClusterStatus .State ;
2124import com .google .common .annotations .VisibleForTesting ;
2225import com .google .common .base .Stopwatch ;
2326import com .google .common .collect .ImmutableSet ;
27+ import com .google .longrunning .Operation ;
28+ import com .google .rpc .Status ;
2429import io .cdap .cdap .api .exception .ErrorCategory ;
2530import io .cdap .cdap .api .exception .ErrorType ;
2631import io .cdap .cdap .runtime .spi .ProgramRunInfo ;
2732import io .cdap .cdap .runtime .spi .RuntimeMonitorType ;
2833import io .cdap .cdap .runtime .spi .common .DataprocImageVersion ;
34+ import io .cdap .cdap .runtime .spi .common .DataprocMetric ;
2935import io .cdap .cdap .runtime .spi .common .DataprocUtils ;
3036import io .cdap .cdap .runtime .spi .provisioner .Cluster ;
3137import io .cdap .cdap .runtime .spi .provisioner .ClusterStatus ;
3743import io .cdap .cdap .runtime .spi .ssh .SSHContext ;
3844import io .cdap .cdap .runtime .spi .ssh .SSHKeyPair ;
3945import io .cdap .cdap .runtime .spi .ssh .SSHPublicKey ;
46+ import io .grpc .Status .Code ;
4047import java .util .Collections ;
48+ import java .util .EnumSet ;
4149import java .util .HashMap ;
4250import java .util .List ;
4351import java .util .Map ;
4452import java .util .Optional ;
53+ import java .util .Set ;
4554import java .util .concurrent .Future ;
4655import java .util .concurrent .TimeUnit ;
4756import java .util .concurrent .TimeoutException ;
4857import java .util .concurrent .atomic .AtomicBoolean ;
4958import java .util .concurrent .locks .Lock ;
5059import java .util .regex .Pattern ;
5160import javax .annotation .Nullable ;
61+
5262import org .slf4j .Logger ;
5363import org .slf4j .LoggerFactory ;
5464
@@ -73,6 +83,12 @@ public class DataprocProvisioner extends AbstractDataprocProvisioner {
7383 //A lock to use for cluster reuse
7484 private static final String REUSE_LOCK = "reuse" ;
7585
86+ private static final Set <ClusterStatus > TRANSITIONING_STATES =
87+ EnumSet .of (ClusterStatus .CREATING , ClusterStatus .DELETING );
88+
89+ private static final Set <ClusterStatus > TERMINAL_STATES =
90+ EnumSet .of (ClusterStatus .RUNNING , ClusterStatus .FAILED , ClusterStatus .NOT_EXISTS );
91+
7692 private final DataprocClientFactory clientFactory ;
7793
7894 @ SuppressWarnings ("WeakerAccess" )
@@ -471,16 +487,21 @@ public ClusterStatus getClusterStatus(ProvisionerContext context, Cluster cluste
471487 DataprocConf conf = DataprocConf .create (createContextProperties (context ));
472488 String clusterName = cluster .getName ();
473489
474- ClusterStatus status = cluster .getStatus ();
490+ ClusterStatus previousStatus = cluster .getStatus ();
475491 // if we are skipping the delete, need to avoid checking the real cluster status and pretend like it is deleted.
476- if (conf .isSkipDelete () && status == ClusterStatus .DELETING ) {
492+ if (conf .isSkipDelete () && previousStatus == ClusterStatus .DELETING ) {
477493 return ClusterStatus .NOT_EXISTS ;
478494 }
479495
496+ ClusterStatus status ;
480497 try (DataprocClient client = clientFactory .create (conf , context .getErrorCategory ())) {
481498 status = client .getClusterStatus (clusterName );
482499 DataprocUtils .emitMetric (context , conf .getRegion (), getImageVersion (context , conf ),
483500 "provisioner.clusterStatus.response.count" );
501+
502+ if (isEndState (previousStatus , status )) {
503+ emitLroMetric (context , client , conf , previousStatus , clusterName );
504+ }
484505 return status ;
485506 } catch (Exception e ) {
486507 DataprocUtils .emitMetric (context , conf .getRegion (), getImageVersion (context , conf ),
@@ -489,6 +510,84 @@ public ClusterStatus getClusterStatus(ProvisionerContext context, Cluster cluste
489510 }
490511 }
491512
513+ private boolean isEndState (ClusterStatus previous , ClusterStatus current ) {
514+ return TRANSITIONING_STATES .contains (previous ) && TERMINAL_STATES .contains (current );
515+ }
516+
517+ /**
518+ * Emits a metric for the outcome of a Long Running Operation (LRO) when a cluster
519+ * transitions to a terminal status.
520+ *
521+ * @param context the provisioner context
522+ * @param client the dataproc client
523+ * @param conf the dataproc configuration
524+ * @param previousStatus the status of the cluster before the transition
525+ * @param clusterName the name of the cluster
526+ */
527+ private void emitLroMetric (ProvisionerContext context , DataprocClient client , DataprocConf conf ,
528+ ClusterStatus previousStatus , String clusterName ) {
529+ String opType = getOpType (previousStatus );
530+ if (opType == null ) {
531+ return ;
532+ }
533+
534+ try {
535+ Optional <Operation > opOpt =
536+ client .getLatestOperation (clusterName , opType );
537+
538+ if (!opOpt .isPresent () || !opOpt .get ().getDone ()) {
539+ return ;
540+ }
541+
542+ Operation op = opOpt .get ();
543+ String metricName = "provisioner.operation.response.count" ;
544+ String imageVersion = getImageVersion (context , conf );
545+
546+ DataprocMetric .Builder builder = DataprocMetric .builder (metricName )
547+ .setRegion (conf .getRegion ())
548+ .setImageVersion (imageVersion )
549+ .setOpType (opType );
550+
551+ if (op .hasError ()) {
552+ builder .setException (createApiException (op .getError ()));
553+ }
554+
555+ DataprocUtils .emitMetric (context , builder .build ());
556+
557+ } catch (Exception ex ) {
558+ LOG .warn ("Failed to retrieve LRO operation metadata for metric emission on cluster {}" , clusterName , ex );
559+ }
560+ }
561+
562+ @ Nullable
563+ private String getOpType (ClusterStatus status ) {
564+ if (status == ClusterStatus .CREATING ) {
565+ return "CREATE" ;
566+ }
567+ if (status == ClusterStatus .DELETING ) {
568+ return "DELETE" ;
569+ }
570+ return null ;
571+ }
572+
573+ private Exception createApiException (Status error ) {
574+ int rpcCode = error .getCode ();
575+
576+ Code grpcCode = Code .INTERNAL ;
577+
578+ if (rpcCode >= 0 && rpcCode < io .grpc .Status .Code .values ().length ) {
579+ grpcCode = Code .values ()[rpcCode ];
580+ }
581+
582+ StatusCode gaxStatusCode =
583+ GrpcStatusCode .of (grpcCode );
584+
585+ ApiException apiException = new ApiException (
586+ new Exception (error .getMessage ()), gaxStatusCode , false );
587+
588+ return new Exception (apiException );
589+ }
590+
492591 @ Override
493592 public String getClusterFailureMsg (ProvisionerContext context , Cluster cluster ) throws Exception {
494593 DataprocConf conf = DataprocConf .create (createContextProperties (context ));
0 commit comments