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

io.camunda.zeebe.scheduler.ActorThread Maven / Gradle / Ivy

The newest version!
/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Licensed under the Camunda License 1.0. You may not use this file
 * except in compliance with the Camunda License 1.0.
 */
package io.camunda.zeebe.scheduler;

import io.camunda.zeebe.scheduler.ActorScheduler.ActorSchedulerBuilder;
import io.camunda.zeebe.scheduler.clock.ActorClock;
import io.camunda.zeebe.scheduler.clock.DefaultActorClock;
import io.camunda.zeebe.util.Loggers;
import io.camunda.zeebe.util.error.FatalErrorHandler;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Consumer;
import org.agrona.concurrent.IdleStrategy;
import org.agrona.concurrent.ManyToManyConcurrentArrayQueue;
import org.slf4j.Logger;
import org.slf4j.MDC;

public class ActorThread extends Thread implements Consumer {
  private static final Logger LOG = Loggers.ACTOR_LOGGER;
  private static final FatalErrorHandler FATAL_ERROR_HANDLER = FatalErrorHandler.withLogger(LOG);
  private static final VarHandle STATE_HANDLE;

  static {
    try {
      STATE_HANDLE =
          MethodHandles.lookup().findVarHandle(ActorThread.class, "state", ActorThreadState.class);
    } catch (final NoSuchFieldException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  public final ManyToManyConcurrentArrayQueue submittedCallbacks =
      new ManyToManyConcurrentArrayQueue<>(1024 * 24);
  protected final ActorTimerQueue timerJobQueue;
  protected ActorTaskRunnerIdleStrategy idleStrategy;
  ActorTask currentTask;
  private final ActorMetrics actorMetrics;
  private final CompletableFuture terminationFuture = new CompletableFuture<>();
  private final ActorClock clock;
  private final int threadId;
  private final TaskScheduler taskScheduler;
  private final BoundedArrayQueue jobs = new BoundedArrayQueue<>(2048);
  private final ActorThreadGroup actorThreadGroup;
  private volatile ActorThreadState state;

  public ActorThread(
      final String name,
      final int id,
      final ActorThreadGroup threadGroup,
      final TaskScheduler taskScheduler,
      final ActorClock clock,
      final ActorTimerQueue timerQueue,
      final boolean metricsEnabled) {
    this(
        name,
        id,
        threadGroup,
        taskScheduler,
        clock,
        timerQueue,
        metricsEnabled,
        ActorSchedulerBuilder.defaultIdleStrategySupplier());
  }

  public ActorThread(
      final String name,
      final int id,
      final ActorThreadGroup threadGroup,
      final TaskScheduler taskScheduler,
      final ActorClock clock,
      final ActorTimerQueue timerQueue,
      final boolean metricsEnabled,
      final IdleStrategy idleStrategy) {
    setName(name);
    state = ActorThreadState.NEW;
    threadId = id;
    this.clock = clock != null ? clock : new DefaultActorClock();
    timerJobQueue = timerQueue != null ? timerQueue : new ActorTimerQueue(this.clock);
    actorThreadGroup = threadGroup;
    this.taskScheduler = taskScheduler;
    actorMetrics = new ActorMetrics(metricsEnabled);
    this.idleStrategy = new ActorTaskRunnerIdleStrategy(idleStrategy);
  }

  ActorMetrics getActorMetrics() {
    return actorMetrics;
  }

  private void doWork() {
    submittedCallbacks.drain(this);

    if (clock.update()) {
      timerJobQueue.processExpiredTimers(clock);
    }

    currentTask = taskScheduler.getNextTask();

    if (currentTask != null) {
      final var actorName = currentTask.actor.getName();
      try (final var timer = actorMetrics.startExecutionTimer(actorName)) {
        executeCurrentTask();
      }
      if (actorMetrics.isEnabled()) {
        actorMetrics.updateJobQueueLength(actorName, currentTask.estimateQueueLength());
        actorMetrics.countExecution(actorName);
      }
    } else {
      idleStrategy.onIdle();
    }
  }

  private void executeCurrentTask() {
    final var properties = currentTask.getActor().getContext();
    boolean resubmit = false;

    for (final var property : properties.entrySet()) {
      MDC.put(property.getKey(), property.getValue());
    }

    idleStrategy.onTaskExecuted();

    try {
      resubmit = currentTask.execute(this);
    } catch (final Throwable e) {
      FATAL_ERROR_HANDLER.handleError(e);
      LOG.error("Unexpected error occurred in task {}", currentTask, e);
    } finally {
      clock.update();
      properties.keySet().forEach(MDC::remove);
    }

    if (resubmit) {
      currentTask.resubmit();
    }
  }

  public void hintWorkAvailable() {
    idleStrategy.hintWorkAvailable();
  }

  /** Must be called from this thread, schedules a job to be run later. */
  public void scheduleTimer(final TimerSubscription timer) {
    timerJobQueue.schedule(timer, clock);
  }

  /** Must be called from this thread, remove a scheduled job. */
  public void removeTimer(final TimerSubscription timer) {
    timerJobQueue.remove(timer);
  }

  /**
   * Returns the current {@link ActorThread} or null if the current thread is not an {@link
   * ActorThread}.
   *
   * @return the current {@link ActorThread} or null
   */
  public static ActorThread current() {
    /*
     * Yes, we could work with a thread-local. Except thread locals are slow as f***
     * since they are kept in a map datastructure on the current thread.
     * This implementation takes advantage of the fact that ActorTaskRunner extends Thread
     * itself. If we can cast down, the current thread is the current ActorTaskRunner.
     */
    return Thread.currentThread() instanceof ActorThread
        ? (ActorThread) Thread.currentThread()
        : null;
  }

  public static ActorThread ensureCalledFromActorThread(final String methodName) {
    final ActorThread thread = ActorThread.current();

    if (thread == null) {
      throw new UnsupportedOperationException(
          "Incorrect usage of actor. " + methodName + ": must be called from actor thread");
    }

    return thread;
  }

  public static boolean isCalledFromActorThread() {
    final ActorThread thread = ActorThread.current();
    return thread != null;
  }

  public ActorJob newJob() {
    ActorJob job = jobs.poll();

    if (job == null) {
      job = new ActorJob();
    }

    return job;
  }

  void recycleJob(final ActorJob j) {
    j.reset();
    jobs.offer(j);
  }

  public int getRunnerId() {
    return threadId;
  }

  @Override
  public synchronized void start() {
    if (STATE_HANDLE.compareAndSet(this, ActorThreadState.NEW, ActorThreadState.RUNNING)) {
      super.start();
    } else {
      throw new IllegalStateException("Cannot start runner, not in state 'NEW'.");
    }
  }

  @Override
  public void run() {
    idleStrategy.init();
    MDC.put("actor-scheduler", actorThreadGroup.getSchedulerName());

    while (state == ActorThreadState.RUNNING) {
      try {
        doWork();
      } catch (final Exception e) {
        LOG.error("Unexpected error occurred while in the actor thread {}", getName(), e);
      }
    }

    state = ActorThreadState.TERMINATED;

    terminationFuture.complete(null);
  }

  public CompletableFuture close() {
    if (STATE_HANDLE.compareAndSet(this, ActorThreadState.RUNNING, ActorThreadState.TERMINATING)) {
      return terminationFuture;
    } else {
      throw new IllegalStateException("Cannot stop runner, not in state 'RUNNING'.");
    }
  }

  public ActorJob getCurrentJob() {
    final ActorTask task = getCurrentTask();

    if (task != null) {
      return task.currentJob;
    }

    return null;
  }

  public ActorTask getCurrentTask() {
    return currentTask;
  }

  public ActorClock getClock() {
    return clock;
  }

  public ActorThreadGroup getActorThreadGroup() {
    return actorThreadGroup;
  }

  @Override
  public void accept(final Runnable t) {
    t.run();
  }

  public enum ActorThreadState {
    NEW,
    RUNNING,
    TERMINATING,
    TERMINATED // runner is not reusable
  }

  protected class ActorTaskRunnerIdleStrategy {
    private final IdleStrategy idleStrategy;
    private boolean isIdle;

    protected ActorTaskRunnerIdleStrategy(final IdleStrategy idleStrategy) {
      this.idleStrategy = idleStrategy;
    }

    void init() {
      isIdle = true;
    }

    public void hintWorkAvailable() {
      LockSupport.unpark(ActorThread.this);
    }

    protected void onIdle() {
      if (!isIdle) {
        clock.update();
        isIdle = true;
      }

      idleStrategy.idle();
    }

    protected void onTaskExecuted() {
      idleStrategy.reset();
      isIdle = false;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy