Repository: open-telemetry/opentelemetry-java
File: sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/export/PeriodicMetricReader.java (Java)

Code before:
  public Aggregation getDefaultAggregation(InstrumentType instrumentType) {
    return exporter.getDefaultAggregation(instrumentType);
  }

  @Override
  public MemoryMode getMemoryMode() {
    return exporter.getMemoryMode();
  }

  @Override
  public CompletableResultCode forceFlush() {
    CompletableResultCode result = new CompletableResultCode();
    CompletableResultCode flushResult = scheduled.doRun();
    flushResult.whenComplete(
        () ->
            exporter
                .flush()
                .whenComplete(
                    () -> {
                      if (!flushResult.isSuccess()) {
                        result.fail();
                      } else {
                        result.succeed();
                      }
                    }));
    return result;
  }

  @Override
  public CompletableResultCode shutdown() {
    CompletableResultCode result = new CompletableResultCode();
    ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
    if (scheduledFuture != null) {
      scheduledFuture.cancel(false);
    }
    scheduler.shutdown();
    try {
      scheduler.awaitTermination(5, TimeUnit.SECONDS);
      CompletableResultCode flushResult = scheduled.doRun();
      flushResult.join(5, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      // force a shutdown if the export hasn't finished.
      scheduler.shutdownNow();
      // reset the interrupted status
      Thread.currentThread().interrupt();
    } finally {
      CompletableResultCode shutdownResult = scheduled.shutdown();
      shutdownResult.whenComplete(
          () -> {
            if (!shutdownResult.isSuccess()) {
              result.fail();

Review comment (suggestion):
I've seen this pattern in enough places I wonder if it would be worth adding `CompletableResultCode.whenComplete(Callable<CompletableResultCode>)` that handles this logic in one place.

Code after:
  public Aggregation getDefaultAggregation(InstrumentType instrumentType) {
    return exporter.getDefaultAggregation(instrumentType);
  }

  @Override
  public MemoryMode getMemoryMode() {
    return exporter.getMemoryMode();
  }

  @Override
  public CompletableResultCode forceFlush() {
    CompletableResultCode result = new CompletableResultCode();
    CompletableResultCode doRunResult = scheduled.doRun();
    doRunResult.whenComplete(
        () -> {
          CompletableResultCode flushResult = exporter.flush();
          flushResult.whenComplete(
              () -> {
                if (doRunResult.isSuccess() && flushResult.isSuccess()) {
                  result.succeed();
                } else {
                  result.fail();
                }
              });
        });
    return result;
  }

  @Override
  public CompletableResultCode shutdown() {
    CompletableResultCode result = new CompletableResultCode();
    ScheduledFuture<?> scheduledFuture = this.scheduledFuture;
    if (scheduledFuture != null) {
      scheduledFuture.cancel(false);
    }
    scheduler.shutdown();
    try {
      scheduler.awaitTermination(5, TimeUnit.SECONDS);
      CompletableResultCode flushResult = scheduled.doRun();
      flushResult.join(5, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      // force a shutdown if the export hasn't finished.
      scheduler.shutdownNow();
      // reset the interrupted status
      Thread.currentThread().interrupt();
    } finally {
      CompletableResultCode shutdownResult = scheduled.shutdown();
      shutdownResult.whenComplete(
          () -> {
            if (!shutdownResult.isSuccess()) {
              result.fail();