All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hudi.timeline.service.RequestHandler Maven / Gradle / Ivy

The newest version!
/*
 * 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.hudi.timeline.service;

import org.apache.hudi.common.engine.HoodieEngineContext;
import org.apache.hudi.common.metrics.Registry;
import org.apache.hudi.common.table.marker.MarkerOperation;
import org.apache.hudi.common.table.timeline.HoodieInstant;
import org.apache.hudi.common.table.timeline.HoodieTimeline;
import org.apache.hudi.common.table.timeline.dto.BaseFileDTO;
import org.apache.hudi.common.table.timeline.dto.ClusteringOpDTO;
import org.apache.hudi.common.table.timeline.dto.CompactionOpDTO;
import org.apache.hudi.common.table.timeline.dto.FileGroupDTO;
import org.apache.hudi.common.table.timeline.dto.FileSliceDTO;
import org.apache.hudi.common.table.timeline.dto.InstantDTO;
import org.apache.hudi.common.table.timeline.dto.InstantStateDTO;
import org.apache.hudi.common.table.timeline.dto.TimelineDTO;
import org.apache.hudi.common.table.view.FileSystemViewManager;
import org.apache.hudi.common.table.view.RemoteHoodieTableFileSystemView;
import org.apache.hudi.common.table.view.SyncableFileSystemView;
import org.apache.hudi.common.util.HoodieTimer;
import org.apache.hudi.common.util.Option;
import org.apache.hudi.exception.HoodieException;
import org.apache.hudi.exception.HoodieIOException;
import org.apache.hudi.storage.HoodieStorage;
import org.apache.hudi.storage.StorageConfiguration;
import org.apache.hudi.timeline.service.handlers.BaseFileHandler;
import org.apache.hudi.timeline.service.handlers.FileSliceHandler;
import org.apache.hudi.timeline.service.handlers.InstantStateHandler;
import org.apache.hudi.timeline.service.handlers.MarkerHandler;
import org.apache.hudi.timeline.service.handlers.TimelineHandler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.afterburner.AfterburnerModule;
import io.javalin.Javalin;
import io.javalin.http.BadRequestResponse;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import org.apache.hadoop.security.UserGroupInformation;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

/**
 * Main REST Handler class that handles and delegates calls to timeline relevant handlers.
 */
public class RequestHandler {

  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new AfterburnerModule());
  private static final Logger LOG = LoggerFactory.getLogger(RequestHandler.class);
  private static final TypeReference> LIST_TYPE_REFERENCE = new TypeReference>() {
  };

  private final TimelineService.Config timelineServiceConfig;
  private final FileSystemViewManager viewManager;
  private final Javalin app;
  private final TimelineHandler instantHandler;
  private final FileSliceHandler sliceHandler;
  private final BaseFileHandler dataFileHandler;
  private final MarkerHandler markerHandler;
  private final InstantStateHandler instantStateHandler;
  private final Registry metricsRegistry = Registry.getRegistry("TimelineService");
  private final ScheduledExecutorService asyncResultService;

  public RequestHandler(Javalin app, StorageConfiguration conf, TimelineService.Config timelineServiceConfig,
                        HoodieEngineContext hoodieEngineContext, HoodieStorage storage,
                        FileSystemViewManager viewManager) throws IOException {
    this.timelineServiceConfig = timelineServiceConfig;
    this.viewManager = viewManager;
    this.app = app;
    this.instantHandler = new TimelineHandler(conf, timelineServiceConfig, storage, viewManager);
    this.sliceHandler = new FileSliceHandler(conf, timelineServiceConfig, storage, viewManager);
    this.dataFileHandler = new BaseFileHandler(conf, timelineServiceConfig, storage, viewManager);
    if (timelineServiceConfig.enableMarkerRequests) {
      this.markerHandler = new MarkerHandler(
          conf, timelineServiceConfig, hoodieEngineContext, storage, viewManager, metricsRegistry);
    } else {
      this.markerHandler = null;
    }
    if (timelineServiceConfig.enableInstantStateRequests) {
      this.instantStateHandler = new InstantStateHandler(conf, timelineServiceConfig, storage, viewManager);
    } else {
      this.instantStateHandler = null;
    }
    if (timelineServiceConfig.async) {
      this.asyncResultService = Executors.newSingleThreadScheduledExecutor();
    } else {
      this.asyncResultService = null;
    }
  }

  /**
   * Serializes the result into JSON String.
   *
   * @param ctx             Javalin context
   * @param obj             object to serialize
   * @param metricsRegistry {@code Registry} instance for storing metrics
   * @param objectMapper    JSON object mapper
   * @param logger          {@code Logger} instance
   * @return JSON String from the input object
   * @throws JsonProcessingException
   */
  public static String jsonifyResult(
      Context ctx, Object obj, Registry metricsRegistry, ObjectMapper objectMapper, Logger logger)
      throws JsonProcessingException {
    HoodieTimer timer = HoodieTimer.start();
    boolean prettyPrint = ctx.queryParam("pretty") != null;
    String result =
        prettyPrint ? objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj)
            : objectMapper.writeValueAsString(obj);
    final long jsonifyTime = timer.endTimer();
    metricsRegistry.add("WRITE_VALUE_CNT", 1);
    metricsRegistry.add("WRITE_VALUE_TIME", jsonifyTime);
    if (logger.isDebugEnabled()) {
      logger.debug("Jsonify TimeTaken={}", jsonifyTime);
    }
    return result;
  }

  private static String getBasePathParam(Context ctx) {
    return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.BASEPATH_PARAM, String.class).getOrThrow(e -> new HoodieException("Basepath is invalid"));
  }

  private static String getPartitionParam(Context ctx) {
    return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.PARTITION_PARAM, String.class).getOrDefault("");
  }

  private static String getFileIdParam(Context ctx) {
    return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.FILEID_PARAM, String.class).getOrThrow(e -> new HoodieException("FILEID is invalid"));
  }

  private static List getInstantsParam(Context ctx) {
    return Arrays.asList(ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.INSTANTS_PARAM, String.class).getOrThrow(e -> new HoodieException("INSTANTS_PARAM is invalid"))
        .split(RemoteHoodieTableFileSystemView.MULTI_VALUE_SEPARATOR));
  }

  private static String getMaxInstantParamMandatory(Context ctx) {
    return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.MAX_INSTANT_PARAM, String.class).getOrThrow(e -> new HoodieException("MAX_INSTANT_PARAM is invalid"));
  }

  private static String getMaxInstantParamOptional(Context ctx) {
    return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.MAX_INSTANT_PARAM, String.class).getOrDefault("");
  }

  private static String getMinInstantParam(Context ctx) {
    return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.MIN_INSTANT_PARAM, String.class).getOrDefault("");
  }

  private static String getMarkerDirParam(Context ctx) {
    return ctx.queryParamAsClass(MarkerOperation.MARKER_DIR_PATH_PARAM, String.class).getOrDefault("");
  }

  private static boolean getIncludeFilesInPendingCompactionParam(Context ctx) {
    return Boolean.parseBoolean(
        ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.INCLUDE_FILES_IN_PENDING_COMPACTION_PARAM, String.class)
            .getOrThrow(e -> new HoodieException("INCLUDE_FILES_IN_PENDING_COMPACTION_PARAM is invalid")));
  }

  private static String getInstantStateDirPathParam(Context ctx) {
    return ctx.queryParam(InstantStateHandler.INSTANT_STATE_DIR_PATH_PARAM);
  }

  public void register() {
    registerDataFilesAPI();
    registerFileSlicesAPI();
    registerTimelineAPI();
    if (markerHandler != null) {
      registerMarkerAPI();
    }
    if (instantStateHandler != null) {
      registerInstantStateAPI();
    }
  }

  public void stop() {
    if (markerHandler != null) {
      markerHandler.stop();
    }
    if (asyncResultService != null) {
      asyncResultService.shutdown();
    }
  }

  private void writeValueAsString(Context ctx, Object obj) throws JsonProcessingException {
    if (timelineServiceConfig.async) {
      writeValueAsStringAsync(ctx, obj);
    } else {
      writeValueAsStringSync(ctx, obj);
    }
  }

  private void writeValueAsStringSync(Context ctx, Object obj) throws JsonProcessingException {
    String result = jsonifyResult(ctx, obj, metricsRegistry, OBJECT_MAPPER, LOG);
    ctx.result(result);
  }

  private void writeValueAsStringAsync(Context ctx, Object obj) {
    ctx.future(CompletableFuture.supplyAsync(() -> {
      try {
        return jsonifyResult(ctx, obj, metricsRegistry, OBJECT_MAPPER, LOG);
      } catch (JsonProcessingException e) {
        throw new HoodieException("Failed to JSON encode the value", e);
      }
    }, asyncResultService));
  }

  /**
   * Register Timeline API calls.
   */
  private void registerTimelineAPI() {
    app.get(RemoteHoodieTableFileSystemView.LAST_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LAST_INSTANT", 1);
      List dtos = instantHandler.getLastInstant(getBasePathParam(ctx));
      writeValueAsString(ctx, dtos);
    }, false));

    app.get(RemoteHoodieTableFileSystemView.TIMELINE_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("TIMELINE", 1);
      TimelineDTO dto = instantHandler.getTimeline(getBasePathParam(ctx));
      writeValueAsString(ctx, dto);
    }, false));
  }

  /**
   * Register Data-Files API calls.
   */
  private void registerDataFilesAPI() {
    app.get(RemoteHoodieTableFileSystemView.LATEST_PARTITION_DATA_FILES_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_PARTITION_DATA_FILES", 1);
      List dtos = dataFileHandler.getLatestDataFiles(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_PARTITION_DATA_FILE_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_PARTITION_DATA_FILE", 1);
      List dtos = dataFileHandler.getLatestDataFile(
          getBasePathParam(ctx),
          getPartitionParam(ctx),
          getFileIdParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_ALL_DATA_FILES_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_ALL_DATA_FILES", 1);
      List dtos = dataFileHandler.getLatestDataFiles(getBasePathParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_DATA_FILES_BEFORE_ON_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_DATA_FILES_BEFORE_ON_INSTANT", 1);
      List dtos = dataFileHandler.getLatestDataFilesBeforeOrOn(
          getBasePathParam(ctx),
          getPartitionParam(ctx),
          getMaxInstantParamMandatory(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_LATEST_BASE_FILES_BEFORE_ON_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_LATEST_BASE_FILES_BEFORE_ON_INSTANT", 1);
      Map> dtos = dataFileHandler.getAllLatestDataFilesBeforeOrOn(
          getBasePathParam(ctx),
          getMaxInstantParamMandatory(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_DATA_FILE_ON_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_DATA_FILE_ON_INSTANT", 1);
      List dtos = dataFileHandler.getLatestDataFileOn(
          getBasePathParam(ctx),
          getPartitionParam(ctx),
          ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.INSTANT_PARAM, String.class).get(),
          getFileIdParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_DATA_FILES_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_DATA_FILES", 1);
      List dtos = dataFileHandler.getAllDataFiles(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_DATA_FILES_RANGE_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_DATA_FILES_RANGE_INSTANT", 1);
      List dtos = dataFileHandler.getLatestDataFilesInRange(
          getBasePathParam(ctx),
          getInstantsParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));
  }

  /**
   * Register File Slices API calls.
   */
  private void registerFileSlicesAPI() {
    app.get(RemoteHoodieTableFileSystemView.LATEST_PARTITION_SLICES_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_PARTITION_SLICES", 1);
      List dtos = sliceHandler.getLatestFileSlices(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_PARTITION_SLICES_INFLIGHT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_PARTITION_SLICES_INFLIGHT", 1);
      List dtos = sliceHandler.getLatestFileSlicesIncludingInflight(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_PARTITION_SLICES_STATELESS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_PARTITION_SLICES_STATELESS", 1);
      List dtos = sliceHandler.getLatestFileSlicesStateless(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_PARTITION_SLICE_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_PARTITION_SLICE", 1);
      List dtos = sliceHandler.getLatestFileSlice(
          getBasePathParam(ctx),
          getPartitionParam(ctx),
          getFileIdParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_PARTITION_UNCOMPACTED_SLICES_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_PARTITION_UNCOMPACTED_SLICES", 1);
      List dtos = sliceHandler.getLatestUnCompactedFileSlices(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_SLICES_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_SLICES", 1);
      List dtos = sliceHandler.getAllFileSlices(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_SLICES_RANGE_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_SLICE_RANGE_INSTANT", 1);
      List dtos = sliceHandler.getLatestFileSliceInRange(
          getBasePathParam(ctx),
          getInstantsParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_SLICES_MERGED_BEFORE_ON_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_SLICES_MERGED_BEFORE_ON_INSTANT", 1);
      List dtos = sliceHandler.getLatestMergedFileSlicesBeforeOrOn(
          getBasePathParam(ctx),
          getPartitionParam(ctx),
          getMaxInstantParamMandatory(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.LATEST_SLICES_BEFORE_ON_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LATEST_SLICES_BEFORE_ON_INSTANT", 1);
      List dtos = sliceHandler.getLatestFileSlicesBeforeOrOn(
          getBasePathParam(ctx),
          getPartitionParam(ctx),
          getMaxInstantParamMandatory(ctx),
          getIncludeFilesInPendingCompactionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_LATEST_SLICES_BEFORE_ON_INSTANT_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_LATEST_SLICES_BEFORE_ON_INSTANT", 1);
      Map> dtos = sliceHandler.getAllLatestFileSlicesBeforeOrOn(
          getBasePathParam(ctx),
          getMaxInstantParamMandatory(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.PENDING_COMPACTION_OPS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("PEDING_COMPACTION_OPS", 1);
      List dtos = sliceHandler.getPendingCompactionOperations(getBasePathParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.PENDING_LOG_COMPACTION_OPS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("PEDING_LOG_COMPACTION_OPS", 1);
      List dtos = sliceHandler.getPendingLogCompactionOperations(getBasePathParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_FILEGROUPS_FOR_PARTITION_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_FILEGROUPS_FOR_PARTITION", 1);
      List dtos = sliceHandler.getAllFileGroups(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_FILEGROUPS_FOR_PARTITION_STATELESS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_FILEGROUPS_FOR_PARTITION_STATELESS", 1);
      List dtos = sliceHandler.getAllFileGroupsStateless(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.post(RemoteHoodieTableFileSystemView.REFRESH_TABLE_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("REFRESH_TABLE", 1);
      boolean success = sliceHandler.refreshTable(getBasePathParam(ctx));
      writeValueAsString(ctx, success);
    }, false));

    app.post(RemoteHoodieTableFileSystemView.LOAD_PARTITIONS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LOAD_PARTITIONS", 1);
      String basePath = getBasePathParam(ctx);
      try {
        List partitionPaths = OBJECT_MAPPER.readValue(ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.PARTITIONS_PARAM, String.class)
            .getOrThrow(e -> new HoodieException("Partitions param is invalid")), LIST_TYPE_REFERENCE);
        boolean success = sliceHandler.loadPartitions(basePath, partitionPaths);
        writeValueAsString(ctx, success);
      } catch (IOException e) {
        throw new HoodieIOException("Failed to parse request parameter", e);
      }
    }, false));

    app.post(RemoteHoodieTableFileSystemView.LOAD_ALL_PARTITIONS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("LOAD_ALL_PARTITIONS", 1);
      boolean success = sliceHandler.loadAllPartitions(getBasePathParam(ctx));
      writeValueAsString(ctx, success);
    }, false));

    app.get(RemoteHoodieTableFileSystemView.ALL_REPLACED_FILEGROUPS_BEFORE_OR_ON_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_REPLACED_FILEGROUPS_BEFORE_OR_ON", 1);
      List dtos = sliceHandler.getReplacedFileGroupsBeforeOrOn(
          getBasePathParam(ctx),
          getMaxInstantParamOptional(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_REPLACED_FILEGROUPS_BEFORE_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_REPLACED_FILEGROUPS_BEFORE", 1);
      List dtos = sliceHandler.getReplacedFileGroupsBefore(
          getBasePathParam(ctx),
          getMaxInstantParamOptional(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_REPLACED_FILEGROUPS_AFTER_OR_ON_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_REPLACED_FILEGROUPS_AFTER_OR_ON", 1);
      List dtos = sliceHandler.getReplacedFileGroupsAfterOrOn(
          getBasePathParam(ctx),
          getMinInstantParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.ALL_REPLACED_FILEGROUPS_PARTITION_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_REPLACED_FILEGROUPS_PARTITION", 1);
      List dtos = sliceHandler.getAllReplacedFileGroups(
          getBasePathParam(ctx),
          getPartitionParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));

    app.get(RemoteHoodieTableFileSystemView.PENDING_CLUSTERING_FILEGROUPS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("PENDING_CLUSTERING_FILEGROUPS", 1);
      List dtos = sliceHandler.getFileGroupsInPendingClustering(getBasePathParam(ctx));
      writeValueAsString(ctx, dtos);
    }, true));
  }

  private void registerMarkerAPI() {
    app.get(MarkerOperation.ALL_MARKERS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_MARKERS", 1);
      Set markers = markerHandler.getAllMarkers(getMarkerDirParam(ctx));
      writeValueAsString(ctx, markers);
    }, false));

    app.get(MarkerOperation.CREATE_AND_MERGE_MARKERS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("CREATE_AND_MERGE_MARKERS", 1);
      Set markers = markerHandler.getCreateAndMergeMarkers(getMarkerDirParam(ctx));
      writeValueAsString(ctx, markers);
    }, false));

    app.get(MarkerOperation.MARKERS_DIR_EXISTS_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("MARKERS_DIR_EXISTS", 1);
      boolean exist = markerHandler.doesMarkerDirExist(getMarkerDirParam(ctx));
      writeValueAsString(ctx, exist);
    }, false));

    app.post(MarkerOperation.CREATE_MARKER_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("CREATE_MARKER", 1);
      ctx.future(markerHandler.createMarker(
          ctx,
          getMarkerDirParam(ctx),
          ctx.queryParamAsClass(MarkerOperation.MARKER_NAME_PARAM, String.class).getOrDefault(""),
          ctx.queryParamAsClass(MarkerOperation.MARKER_BASEPATH_PARAM, String.class).getOrDefault("")));
    }, false));

    app.post(MarkerOperation.DELETE_MARKER_DIR_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("DELETE_MARKER_DIR", 1);
      boolean success = markerHandler.deleteMarkers(getMarkerDirParam(ctx));
      writeValueAsString(ctx, success);
    }, false));
  }

  private void registerInstantStateAPI() {
    app.get(InstantStateHandler.ALL_INSTANT_STATE_URL, new ViewHandler(ctx -> {
      metricsRegistry.add("ALL_INSTANT_STATE", 1);
      List instantStates = instantStateHandler.getAllInstantStates(getInstantStateDirPathParam(ctx));
      writeValueAsString(ctx, instantStates);
    }, false));

    app.post(InstantStateHandler.REFRESH_INSTANT_STATE, new ViewHandler(ctx -> {
      metricsRegistry.add("REFRESH_INSTANT_STATE", 1);
      boolean success = instantStateHandler.refresh(getInstantStateDirPathParam(ctx));
      writeValueAsString(ctx, success);
    }, false));
  }

  /**
   * Used for logging and performing refresh check.
   */
  private class ViewHandler implements Handler {

    private final Handler handler;
    private final boolean performRefreshCheck;
    private final UserGroupInformation ugi;

    ViewHandler(Handler handler, boolean performRefreshCheck) {
      this.handler = handler;
      this.performRefreshCheck = performRefreshCheck;
      try {
        ugi = UserGroupInformation.getCurrentUser();
      } catch (Exception e) {
        LOG.warn("Fail to get ugi", e);
        throw new HoodieException(e);
      }
    }

    @Override
    public void handle(@NotNull Context context) throws Exception {
      ugi.doAs((PrivilegedExceptionAction) () -> {
        boolean success = true;
        long beginTs = System.currentTimeMillis();
        boolean synced = false;
        boolean refreshCheck = performRefreshCheck && !isRefreshCheckDisabledInQuery(context);
        long refreshCheckTimeTaken = 0;
        long handleTimeTaken = 0;
        long finalCheckTimeTaken = 0;
        try {
          if (refreshCheck) {
            long beginRefreshCheck = System.currentTimeMillis();
            synced = syncIfLocalViewBehind(context);
            long endRefreshCheck = System.currentTimeMillis();
            refreshCheckTimeTaken = endRefreshCheck - beginRefreshCheck;
          }

          long handleBeginMs = System.currentTimeMillis();
          handler.handle(context);
          long handleEndMs = System.currentTimeMillis();
          handleTimeTaken = handleEndMs - handleBeginMs;

          if (refreshCheck) {
            long beginFinalCheck = System.currentTimeMillis();
            if (isLocalViewBehind(context)) {
              String lastKnownInstantFromClient = getLastInstantTsParam(context);
              String timelineHashFromClient = getTimelineHashParam(context);
              HoodieTimeline localTimeline =
                  viewManager.getFileSystemView(context.queryParam(RemoteHoodieTableFileSystemView.BASEPATH_PARAM)).getTimeline();
              if (shouldThrowExceptionIfLocalViewBehind(localTimeline, timelineHashFromClient)) {
                String errMsg = String.format("Last known instant from client was %s but server has the following timeline %s",
                        lastKnownInstantFromClient, localTimeline.getInstants());
                throw new BadRequestResponse(errMsg);
              }
            }
            long endFinalCheck = System.currentTimeMillis();
            finalCheckTimeTaken = endFinalCheck - beginFinalCheck;
          }
        } catch (RuntimeException re) {
          success = false;
          if (re instanceof BadRequestResponse) {
            LOG.warn("Bad request response due to client view behind server view. {}", re.getMessage());
          } else {
            LOG.error(String.format("Got runtime exception servicing request %s", context.queryString()), re);
          }
          throw re;
        } finally {
          long endTs = System.currentTimeMillis();
          long timeTakenMillis = endTs - beginTs;
          metricsRegistry.add("TOTAL_API_TIME", timeTakenMillis);
          metricsRegistry.add("TOTAL_REFRESH_TIME", refreshCheckTimeTaken);
          metricsRegistry.add("TOTAL_HANDLE_TIME", handleTimeTaken);
          metricsRegistry.add("TOTAL_CHECK_TIME", finalCheckTimeTaken);
          metricsRegistry.add("TOTAL_API_CALLS", 1);

          if (LOG.isDebugEnabled()) {
            LOG.debug("TimeTakenMillis[Total={}, Refresh={}, handle={}, Check={}], Success={}, Query={}, Host={}, synced={}",
                    timeTakenMillis, refreshCheckTimeTaken, handleTimeTaken, finalCheckTimeTaken, success, context.queryString(), context.host(), synced);
          }
        }
        return null;
      });
    }

    /**
     * Determines if local view of table's timeline is behind that of client's view.
     */
    private boolean isLocalViewBehind(Context ctx) {
      String basePath = ctx.queryParam(RemoteHoodieTableFileSystemView.BASEPATH_PARAM);
      String lastKnownInstantFromClient = getLastInstantTsParam(ctx);
      String timelineHashFromClient = getTimelineHashParam(ctx);
      HoodieTimeline localTimeline =
          viewManager.getFileSystemView(basePath).getTimeline().filterCompletedOrMajorOrMinorCompactionInstants();
      if (LOG.isDebugEnabled()) {
        LOG.debug("Client [ LastTs={}, TimelineHash={}], localTimeline={}",lastKnownInstantFromClient, timelineHashFromClient, localTimeline.getInstants());
      }

      if ((!localTimeline.getInstantsAsStream().findAny().isPresent())
          && HoodieTimeline.INVALID_INSTANT_TS.equals(lastKnownInstantFromClient)) {
        return false;
      }

      String localTimelineHash = localTimeline.getTimelineHash();
      // refresh if timeline hash mismatches
      if (!localTimelineHash.equals(timelineHashFromClient)) {
        return true;
      }

      // As a safety check, even if hash is same, ensure instant is present
      return !localTimeline.containsOrBeforeTimelineStarts(lastKnownInstantFromClient);
    }

    /**
     * Syncs data-set view if local view is behind.
     */
    private boolean syncIfLocalViewBehind(Context ctx) {
      String basePath = ctx.queryParam(RemoteHoodieTableFileSystemView.BASEPATH_PARAM);
      SyncableFileSystemView view = viewManager.getFileSystemView(basePath);
      synchronized (view) {
        if (isLocalViewBehind(ctx)) {
          String lastKnownInstantFromClient = getLastInstantTsParam(ctx);
          HoodieTimeline localTimeline = viewManager.getFileSystemView(basePath).getTimeline();
          if (LOG.isInfoEnabled()) {
            LOG.info("Syncing view as client passed last known instant {} as last known instant but server has the following last instant on timeline: {}",
                lastKnownInstantFromClient, localTimeline.lastInstant());
          }
          view.sync();
          return true;
        }
      }
      return false;
    }

    /**
     * Determine whether to throw an exception when local view of table's timeline is behind that of client's view.
     */
    private boolean shouldThrowExceptionIfLocalViewBehind(HoodieTimeline localTimeline, String timelineHashFromClient) {
      Option lastInstant = localTimeline.lastInstant();
      // When performing async clean, we may have one more .clean.completed after lastInstantTs.
      // In this case, we do not need to throw an exception.
      return !lastInstant.isPresent() || !lastInstant.get().getAction().equals(HoodieTimeline.CLEAN_ACTION)
          || !localTimeline.findInstantsBefore(lastInstant.get().requestedTime()).getTimelineHash().equals(timelineHashFromClient);
    }

    private boolean isRefreshCheckDisabledInQuery(Context ctx) {
      return Boolean.parseBoolean(ctx.queryParam(RemoteHoodieTableFileSystemView.REFRESH_OFF));
    }

    private String getLastInstantTsParam(Context ctx) {
      return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.LAST_INSTANT_TS, String.class).getOrDefault(HoodieTimeline.INVALID_INSTANT_TS);
    }

    private String getTimelineHashParam(Context ctx) {
      return ctx.queryParamAsClass(RemoteHoodieTableFileSystemView.TIMELINE_HASH, String.class).getOrDefault("");
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy