/*
 * 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.
 */

package org.apache.beam.runners.dataflow.worker.fn.control;

import static org.apache.beam.runners.dataflow.worker.fn.control.RegisterAndProcessBundleOperation.encodeAndConcat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableTable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.apache.beam.model.fnexecution.v1.BeamFnApi;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.InstructionRequest;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.InstructionRequest.RequestCase;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.InstructionResponse;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateAppendRequest;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateClearRequest;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateGetRequest;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateKey;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateRequest;
import org.apache.beam.model.fnexecution.v1.BeamFnApi.StateResponse;
import org.apache.beam.runners.core.InMemoryMultimapSideInputView;
import org.apache.beam.runners.core.InMemoryStateInternals;
import org.apache.beam.runners.core.SideInputReader;
import org.apache.beam.runners.dataflow.harness.util.ThrowingRunnable;
import org.apache.beam.runners.dataflow.worker.DataflowExecutionContext.DataflowStepContext;
import org.apache.beam.runners.dataflow.worker.DataflowPortabilityPCollectionView;
import org.apache.beam.runners.dataflow.worker.fn.IdGenerator;
import org.apache.beam.runners.dataflow.worker.util.common.worker.OperationContext;
import org.apache.beam.runners.fnexecution.control.InstructionRequestHandler;
import org.apache.beam.runners.fnexecution.state.StateDelegator;
import org.apache.beam.runners.fnexecution.state.StateRequestHandler;
import org.apache.beam.sdk.coders.ByteArrayCoder;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
import org.apache.beam.sdk.transforms.windowing.GlobalWindow;
import org.apache.beam.sdk.util.CoderUtils;
import org.apache.beam.sdk.util.MoreFutures;
import org.apache.beam.sdk.util.WindowedValue.FullWindowedValueCoder;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollectionView;
import org.apache.beam.sdk.values.TupleTag;
import org.apache.beam.sdk.values.ValueInSingleWindow.Coder;
import org.apache.beam.vendor.protobuf.v3.com.google.protobuf.ByteString;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

/** Tests for {@link RegisterAndProcessBundleOperation}. */
@RunWith(JUnit4.class)
@SuppressWarnings("FutureReturnValueIgnored")
public class RegisterAndProcessBundleOperationTest {
  private static final BeamFnApi.RegisterRequest REGISTER_REQUEST =
      BeamFnApi.RegisterRequest.newBuilder()
          .addProcessBundleDescriptor(BeamFnApi.ProcessBundleDescriptor.newBuilder().setId("555"))
          .build();
  @Mock private OperationContext mockContext;
  @Mock private StateDelegator mockBeamFnStateDelegator;

  @Captor private ArgumentCaptor<StateRequestHandler> stateHandlerCaptor;

  private AtomicInteger stateServiceRegisterCounter;
  private AtomicInteger stateServiceDeregisterCounter;
  private AtomicInteger stateServiceAbortCounter;

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    stateServiceRegisterCounter = new AtomicInteger();
    stateServiceDeregisterCounter = new AtomicInteger();
    stateServiceAbortCounter = new AtomicInteger();
    when(mockBeamFnStateDelegator.registerForProcessBundleInstructionId(
            any(String.class), any(StateRequestHandler.class)))
        .thenAnswer(
            new Answer<StateDelegator.Registration>() {
              @Override
              public StateDelegator.Registration answer(InvocationOnMock invocationOnMock)
                  throws Throwable {
                stateServiceRegisterCounter.incrementAndGet();
                return new StateDelegator.Registration() {

                  @Override
                  public void deregister() {
                    stateServiceDeregisterCounter.incrementAndGet();
                  }

                  @Override
                  public void abort() {
                    stateServiceAbortCounter.incrementAndGet();
                  }
                };
              }
            });
  }

  private Supplier<String> makeIdGeneratorStartingFrom(long initialValue) {
    AtomicLong longIdGenerator = new AtomicLong(initialValue);
    return () -> Long.toString(longIdGenerator.getAndIncrement());
  }

  @Test
  public void testSupportsRestart() {
    new RegisterAndProcessBundleOperation(
            IdGenerator::generate,
            new InstructionRequestHandler() {
              @Override
              public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
                CompletableFuture<InstructionResponse> responseFuture = new CompletableFuture<>();
                completeFuture(request, responseFuture);
                return responseFuture;
              }

              @Override
              public void close() {}
            },
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext)
        .supportsRestart();
  }

  @Test
  public void testRegisterOnlyOnFirstBundle() throws Exception {
    List<BeamFnApi.InstructionRequest> requests = new ArrayList<>();
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);
    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            new InstructionRequestHandler() {
              @Override
              public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
                requests.add(request);
                switch (request.getRequestCase()) {
                  case REGISTER:
                    return CompletableFuture.completedFuture(
                        responseFor(request)
                            .setRegister(BeamFnApi.RegisterResponse.getDefaultInstance())
                            .build());
                  case PROCESS_BUNDLE:
                    return CompletableFuture.completedFuture(
                        responseFor(request)
                            .setProcessBundle(BeamFnApi.ProcessBundleResponse.getDefaultInstance())
                            .build());
                  default:
                    // block forever on other requests
                    return new CompletableFuture<>();
                }
              }

              @Override
              public void close() {}
            },
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext);

    // Ensure that the first time we start we send the register and process bundle requests
    assertThat(requests, empty());
    operation.start();
    assertEquals(
        requests.get(0),
        BeamFnApi.InstructionRequest.newBuilder()
            .setInstructionId("777")
            .setRegister(REGISTER_REQUEST)
            .build());
    assertEquals(
        requests.get(1),
        BeamFnApi.InstructionRequest.newBuilder()
            .setInstructionId("778")
            .setProcessBundle(
                BeamFnApi.ProcessBundleRequest.newBuilder()
                    .setProcessBundleDescriptorReference("555"))
            .build());
    operation.finish();

    // Ensure on restart that we only send the process bundle request
    operation.start();
    assertEquals(
        requests.get(2),
        BeamFnApi.InstructionRequest.newBuilder()
            .setInstructionId("779")
            .setProcessBundle(
                BeamFnApi.ProcessBundleRequest.newBuilder()
                    .setProcessBundleDescriptorReference("555"))
            .build());
    operation.finish();
  }

  @Test
  public void testTentativeUserMetrics() throws Exception {
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);

    CountDownLatch processBundleLatch = new CountDownLatch(1);

    final String stepName = "fakeStepNameWithUserMetrics";
    final String namespace = "sdk/whatever";
    final String name = "someCounter";
    final long counterValue = 42;

    final BeamFnApi.Metrics.User.MetricName metricName =
        BeamFnApi.Metrics.User.MetricName.newBuilder()
            .setNamespace(namespace)
            .setName(name)
            .build();

    InstructionRequestHandler instructionRequestHandler =
        new InstructionRequestHandler() {
          @Override
          public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
            switch (request.getRequestCase()) {
              case REGISTER:
                return CompletableFuture.completedFuture(responseFor(request).build());
              case PROCESS_BUNDLE:
                return MoreFutures.supplyAsync(
                    () -> {
                      processBundleLatch.await();
                      return responseFor(request)
                          .setProcessBundle(BeamFnApi.ProcessBundleResponse.getDefaultInstance())
                          .build();
                    });
              case PROCESS_BUNDLE_PROGRESS:
                return CompletableFuture.completedFuture(
                    responseFor(request)
                        .setProcessBundleProgress(
                            BeamFnApi.ProcessBundleProgressResponse.newBuilder()
                                .setMetrics(
                                    BeamFnApi.Metrics.newBuilder()
                                        .putPtransforms(
                                            stepName,
                                            BeamFnApi.Metrics.PTransform.newBuilder()
                                                .addUser(
                                                    BeamFnApi.Metrics.User.newBuilder()
                                                        .setMetricName(metricName)
                                                        .setCounterData(
                                                            BeamFnApi.Metrics.User.CounterData
                                                                .newBuilder()
                                                                .setValue(counterValue)))
                                                .build())))
                        .build());
              default:
                // block forever
                return new CompletableFuture<>();
            }
          }

          @Override
          public void close() {}
        };

    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            instructionRequestHandler,
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext);

    operation.start();

    BeamFnApi.Metrics metrics = MoreFutures.get(operation.getMetrics());
    assertThat(metrics.getPtransformsOrThrow(stepName).getUserCount(), equalTo(1));

    BeamFnApi.Metrics.User userMetric = metrics.getPtransformsOrThrow(stepName).getUser(0);
    assertThat(userMetric.getMetricName(), equalTo(metricName));
    assertThat(userMetric.getCounterData().getValue(), equalTo(counterValue));

    processBundleLatch.countDown();
    operation.finish();
  }

  @Test
  public void testFinalUserMetrics() throws Exception {
    List<BeamFnApi.InstructionRequest> requests = new ArrayList<>();
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);
    ExecutorService executorService = Executors.newCachedThreadPool();

    CountDownLatch processBundleLatch = new CountDownLatch(1);

    final String stepName = "fakeStepNameWithUserMetrics";
    final String namespace = "sdk/whatever";
    final String name = "someCounter";
    final long counterValue = 42;
    final long finalCounterValue = 77;

    final BeamFnApi.Metrics.User.MetricName metricName =
        BeamFnApi.Metrics.User.MetricName.newBuilder()
            .setNamespace(namespace)
            .setName(name)
            .build();

    InstructionRequestHandler instructionRequestHandler =
        new InstructionRequestHandler() {
          @Override
          public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
            switch (request.getRequestCase()) {
              case REGISTER:
                return CompletableFuture.completedFuture(responseFor(request).build());
              case PROCESS_BUNDLE:
                return MoreFutures.supplyAsync(
                    () -> {
                      processBundleLatch.await();
                      return responseFor(request)
                          .setProcessBundle(
                              BeamFnApi.ProcessBundleResponse.newBuilder()
                                  .setMetrics(
                                      BeamFnApi.Metrics.newBuilder()
                                          .putPtransforms(
                                              stepName,
                                              BeamFnApi.Metrics.PTransform.newBuilder()
                                                  .addUser(
                                                      BeamFnApi.Metrics.User.newBuilder()
                                                          .setMetricName(metricName)
                                                          .setCounterData(
                                                              BeamFnApi.Metrics.User.CounterData
                                                                  .newBuilder()
                                                                  .setValue(finalCounterValue)))
                                                  .build())))
                          .build();
                    });
              case PROCESS_BUNDLE_PROGRESS:
                return CompletableFuture.completedFuture(
                    responseFor(request)
                        .setProcessBundleProgress(
                            BeamFnApi.ProcessBundleProgressResponse.newBuilder()
                                .setMetrics(
                                    BeamFnApi.Metrics.newBuilder()
                                        .putPtransforms(
                                            stepName,
                                            BeamFnApi.Metrics.PTransform.newBuilder()
                                                .addUser(
                                                    BeamFnApi.Metrics.User.newBuilder()
                                                        .setMetricName(metricName)
                                                        .setCounterData(
                                                            BeamFnApi.Metrics.User.CounterData
                                                                .newBuilder()
                                                                .setValue(counterValue)))
                                                .build())))
                        .build());
              default:
                // block forever
                return new CompletableFuture<>();
            }
          }

          @Override
          public void close() {}
        };

    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            instructionRequestHandler,
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext);

    operation.start();

    // Force some intermediate metrics to test crosstalk is not introduced
    BeamFnApi.Metrics metrics = MoreFutures.get(operation.getMetrics());
    BeamFnApi.Metrics.User userMetric = metrics.getPtransformsOrThrow(stepName).getUser(0);
    assertThat(userMetric.getMetricName(), equalTo(metricName));
    assertThat(userMetric.getCounterData().getValue(), not(equalTo(finalCounterValue)));

    processBundleLatch.countDown();
    operation.finish();

    metrics = MoreFutures.get(operation.getFinalMetrics());

    userMetric = metrics.getPtransformsOrThrow(stepName).getUser(0);
    assertThat(userMetric.getMetricName(), equalTo(metricName));
    assertThat(userMetric.getCounterData().getValue(), equalTo(finalCounterValue));
  }

  @Test
  public void testProcessingBundleBlocksOnFinish() throws Exception {
    List<BeamFnApi.InstructionRequest> requests = new ArrayList<>();
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);
    ExecutorService executorService = Executors.newCachedThreadPool();
    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            new InstructionRequestHandler() {
              @Override
              public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
                requests.add(request);
                switch (request.getRequestCase()) {
                  case REGISTER:
                    return CompletableFuture.completedFuture(
                        InstructionResponse.newBuilder()
                            .setInstructionId(request.getInstructionId())
                            .build());
                  case PROCESS_BUNDLE:
                    CompletableFuture<InstructionResponse> responseFuture =
                        new CompletableFuture<>();
                    executorService.submit(
                        () -> {
                          // Purposefully sleep simulating SDK harness doing work
                          Thread.sleep(100);
                          responseFuture.complete(
                              InstructionResponse.newBuilder()
                                  .setInstructionId(request.getInstructionId())
                                  .setProcessBundle(
                                      BeamFnApi.ProcessBundleResponse.getDefaultInstance())
                                  .build());
                          completeFuture(request, responseFuture);
                          return null;
                        });
                    return responseFuture;
                  default:
                    // Anything else hangs; nothing else should be blocking
                    return new CompletableFuture<>();
                }
              }

              @Override
              public void close() {}
            },
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext);

    operation.start();
    // This method blocks till the requests are completed
    operation.finish();

    // Ensure that the messages were received
    assertEquals(
        requests.get(0),
        BeamFnApi.InstructionRequest.newBuilder()
            .setInstructionId("777")
            .setRegister(REGISTER_REQUEST)
            .build());
    assertEquals(
        requests.get(1),
        BeamFnApi.InstructionRequest.newBuilder()
            .setInstructionId("778")
            .setProcessBundle(
                BeamFnApi.ProcessBundleRequest.newBuilder()
                    .setProcessBundleDescriptorReference("555"))
            .build());
  }

  @Test
  public void testProcessingBundleHandlesUserStateRequests() throws Exception {
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);
    ExecutorService executorService = Executors.newCachedThreadPool();

    InMemoryStateInternals<ByteString> stateInternals =
        InMemoryStateInternals.forKey(ByteString.EMPTY);
    DataflowStepContext mockStepContext = mock(DataflowStepContext.class);
    DataflowStepContext mockUserStepContext = mock(DataflowStepContext.class);
    when(mockStepContext.namespacedToUser()).thenReturn(mockUserStepContext);
    when(mockUserStepContext.stateInternals()).thenReturn(stateInternals);

    InstructionRequestHandler instructionRequestHandler =
        new InstructionRequestHandler() {
          @Override
          public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
            switch (request.getRequestCase()) {
              case REGISTER:
                return CompletableFuture.completedFuture(responseFor(request).build());
              case PROCESS_BUNDLE:
                return MoreFutures.supplyAsync(
                    () -> {
                      StateRequest partialRequest =
                          StateRequest.newBuilder()
                              .setStateKey(
                                  StateKey.newBuilder()
                                      .setBagUserState(
                                          StateKey.BagUserState.newBuilder()
                                              .setPtransformId("testPTransformId")
                                              .setWindow(ByteString.EMPTY)
                                              .setUserStateId("testUserStateId")))
                              .buildPartial();
                      StateRequest get =
                          partialRequest
                              .toBuilder()
                              .setGet(StateGetRequest.getDefaultInstance())
                              .build();
                      StateRequest clear =
                          partialRequest
                              .toBuilder()
                              .setClear(StateClearRequest.getDefaultInstance())
                              .build();
                      StateRequest append =
                          partialRequest
                              .toBuilder()
                              .setAppend(
                                  StateAppendRequest.newBuilder()
                                      .setData(ByteString.copyFromUtf8("ABC")))
                              .build();

                      StateRequestHandler stateHandler = stateHandlerCaptor.getValue();

                      StateResponse.Builder getWhenEmptyResponse =
                          MoreFutures.get(stateHandler.handle(get));
                      assertEquals(ByteString.EMPTY, getWhenEmptyResponse.getGet().getData());

                      StateResponse.Builder appendWhenEmptyResponse =
                          MoreFutures.get(stateHandler.handle(append));
                      assertNotNull(appendWhenEmptyResponse);
                      StateResponse.Builder appendWhenEmptyResponse2 =
                          MoreFutures.get(stateHandler.handle(append));
                      assertNotNull(appendWhenEmptyResponse2);

                      StateResponse.Builder getWhenHasValueResponse =
                          MoreFutures.get(stateHandler.handle(get));
                      assertEquals(
                          ByteString.copyFromUtf8("ABC").concat(ByteString.copyFromUtf8("ABC")),
                          getWhenHasValueResponse.getGet().getData());

                      StateResponse.Builder clearResponse =
                          MoreFutures.get(stateHandler.handle(clear));
                      assertNotNull(clearResponse);

                      return responseFor(request)
                          .setProcessBundle(BeamFnApi.ProcessBundleResponse.getDefaultInstance())
                          .build();
                    });
              default:
                // block forever
                return new CompletableFuture<>();
            }
          }

          @Override
          public void close() {}
        };

    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            instructionRequestHandler,
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of("testPTransformId", mockStepContext),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext);

    operation.start();
    verify(mockBeamFnStateDelegator)
        .registerForProcessBundleInstructionId(eq("778"), stateHandlerCaptor.capture());

    // This method blocks till the requests are completed
    operation.finish();
    // Ensure that the number of reigstrations matches the number of deregistrations
    assertEquals(stateServiceRegisterCounter.get(), stateServiceDeregisterCounter.get());
    assertEquals(0, stateServiceAbortCounter.get());
  }

  @Test
  public void testProcessingBundleHandlesMultimapSideInputRequests() throws Exception {
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);
    ExecutorService executorService = Executors.newCachedThreadPool();

    DataflowStepContext mockStepContext = mock(DataflowStepContext.class);
    DataflowStepContext mockUserStepContext = mock(DataflowStepContext.class);
    when(mockStepContext.namespacedToUser()).thenReturn(mockUserStepContext);

    // Issues state calls to the Runner after a process bundle request is sent.
    InstructionRequestHandler fakeClient =
        new InstructionRequestHandler() {
          @Override
          public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
            switch (request.getRequestCase()) {
              case REGISTER:
                return CompletableFuture.completedFuture(responseFor(request).build());
              case PROCESS_BUNDLE:
                return MoreFutures.supplyAsync(
                    () -> {
                      StateKey getKey =
                          StateKey.newBuilder()
                              .setMultimapSideInput(
                                  StateKey.MultimapSideInput.newBuilder()
                                      .setPtransformId("testPTransformId")
                                      .setSideInputId("testSideInputId")
                                      .setWindow(
                                          ByteString.copyFrom(
                                              CoderUtils.encodeToByteArray(
                                                  GlobalWindow.Coder.INSTANCE,
                                                  GlobalWindow.INSTANCE)))
                                      .setKey(
                                          ByteString.copyFrom(
                                              CoderUtils.encodeToByteArray(
                                                  ByteArrayCoder.of(),
                                                  "ABC".getBytes(StandardCharsets.UTF_8),
                                                  Coder.Context.NESTED))))
                              .build();

                      StateRequest getRequest =
                          StateRequest.newBuilder()
                              .setStateKey(getKey)
                              .setGet(StateGetRequest.getDefaultInstance())
                              .build();

                      StateRequestHandler stateHandler = stateHandlerCaptor.getValue();

                      StateResponse.Builder getResponse =
                          MoreFutures.get(stateHandler.handle(getRequest));

                      assertEquals(
                          encodeAndConcat(Arrays.asList("X", "Y", "Z"), StringUtf8Coder.of()),
                          getResponse.getGet().getData());

                      return responseFor(request)
                          .setProcessBundle(BeamFnApi.ProcessBundleResponse.getDefaultInstance())
                          .build();
                    });
              default:
                // block forever on other request types
                return new CompletableFuture<>();
            }
          }

          @Override
          public void close() {}
        };

    SideInputReader fakeSideInputReader =
        new SideInputReader() {
          @Nullable
          @Override
          public <T> T get(PCollectionView<T> view, BoundedWindow window) {
            assertEquals(GlobalWindow.INSTANCE, window);
            assertEquals("testSideInputId", view.getTagInternal().getId());
            return (T)
                InMemoryMultimapSideInputView.fromIterable(
                    ByteArrayCoder.of(),
                    ImmutableList.of(
                        KV.of("ABC".getBytes(StandardCharsets.UTF_8), "X"),
                        KV.of("ABC".getBytes(StandardCharsets.UTF_8), "Y"),
                        KV.of("ABC".getBytes(StandardCharsets.UTF_8), "Z")));
          }

          @Override
          public <T> boolean contains(PCollectionView<T> view) {
            return "testSideInputId".equals(view.getTagInternal().getId());
          }

          @Override
          public boolean isEmpty() {
            return false;
          }
        };

    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            fakeClient,
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of("testPTransformId", mockStepContext),
            ImmutableMap.of("testPTransformId", fakeSideInputReader),
            ImmutableTable.of(
                "testPTransformId",
                "testSideInputId",
                DataflowPortabilityPCollectionView.with(
                    new TupleTag<>("testSideInputId"),
                    FullWindowedValueCoder.of(
                        KvCoder.of(ByteArrayCoder.of(), StringUtf8Coder.of()),
                        GlobalWindow.Coder.INSTANCE))),
            mockContext);

    operation.start();
    verify(mockBeamFnStateDelegator)
        .registerForProcessBundleInstructionId(eq("778"), stateHandlerCaptor.capture());

    // This method blocks till the requests are completed
    operation.finish();
    // Ensure that the number of reigstrations matches the number of deregistrations
    assertEquals(stateServiceRegisterCounter.get(), stateServiceDeregisterCounter.get());
    assertEquals(0, stateServiceAbortCounter.get());
  }

  @Test
  public void testAbortCancelsAndCleansUpDuringRegister() throws Exception {
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);
    ExecutorService executorService = Executors.newCachedThreadPool();

    CountDownLatch waitForAbortToComplete = new CountDownLatch(1);
    AtomicReference<ThrowingRunnable> abortReference = new AtomicReference<>();
    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            new InstructionRequestHandler() {
              @Override
              public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
                CompletableFuture<InstructionResponse> responseFuture = new CompletableFuture<>();
                if (request.getRequestCase() == RequestCase.PROCESS_BUNDLE) {
                  executorService.submit(
                      (Callable<Void>)
                          () -> {
                            abortReference.get().run();
                            waitForAbortToComplete.countDown();
                            return null;
                          });
                } else {
                  completeFuture(request, responseFuture);
                }
                return responseFuture;
              }

              @Override
              public void close() {}
            },
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext);
    abortReference.set(operation::abort);
    operation.start();

    waitForAbortToComplete.await();
    // Ensure that the number of registrations matches the number of aborts
    assertEquals(stateServiceRegisterCounter.get(), stateServiceAbortCounter.get());
    assertEquals(0, stateServiceDeregisterCounter.get());
  }

  @Test
  public void testAbortCancelsAndCleansUpDuringProcessBundle() throws Exception {
    Supplier<String> idGenerator = makeIdGeneratorStartingFrom(777L);
    ExecutorService executorService = Executors.newCachedThreadPool();

    CountDownLatch waitForAbortToComplete = new CountDownLatch(1);
    AtomicReference<ThrowingRunnable> abortReference = new AtomicReference<>();
    RegisterAndProcessBundleOperation operation =
        new RegisterAndProcessBundleOperation(
            idGenerator,
            new InstructionRequestHandler() {
              @Override
              public CompletionStage<InstructionResponse> handle(InstructionRequest request) {
                CompletableFuture<InstructionResponse> responseFuture = new CompletableFuture<>();
                if (request.getRequestCase() == RequestCase.PROCESS_BUNDLE) {
                  executorService.submit(
                      (Callable<Void>)
                          () -> {
                            abortReference.get().run();
                            waitForAbortToComplete.countDown();
                            return null;
                          });
                } else {
                  completeFuture(request, responseFuture);
                }
                return responseFuture;
              }

              @Override
              public void close() {}
            },
            mockBeamFnStateDelegator,
            REGISTER_REQUEST,
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableMap.of(),
            ImmutableTable.of(),
            mockContext);
    abortReference.set(operation::abort);
    operation.start();

    waitForAbortToComplete.await();
    // Ensure that the number of registrations matches the number of aborts
    assertEquals(stateServiceRegisterCounter.get(), stateServiceAbortCounter.get());
    assertEquals(0, stateServiceDeregisterCounter.get());
  }

  private InstructionResponse.Builder responseFor(BeamFnApi.InstructionRequest request) {
    return BeamFnApi.InstructionResponse.newBuilder().setInstructionId(request.getInstructionId());
  }

  private void completeFuture(
      BeamFnApi.InstructionRequest request, CompletableFuture<InstructionResponse> response) {
    response.complete(
        BeamFnApi.InstructionResponse.newBuilder()
            .setInstructionId(request.getInstructionId())
            .build());
  }
}
