Skyframe StateMachines 가이드

<ph type="x-smartling-placeholder"></ph> 문제 신고 소스 보기 1박 · 7.3 · 7.2 · 7.1 · 7.0 · 6.5

개요

스카이프레임 StateMachine는 다음 위치에 있는 해체된 함수 객체입니다. 합니다. 또한 유연한 평가와 중복성 없는 평가를 지원합니다1. 필수 값을 즉시 사용할 수는 없지만 비동기식으로 계산됩니다. 이 StateMachine는 대기 중에 스레드 리소스를 연결할 수 없지만 대신 연결해야 합니다. 정지 및 재개될 수 있습니다 따라서 해체는 명시적인 재진입을 노출합니다. 이전 계산을 건너뛸 수 있습니다.

StateMachine는 시퀀스, 브랜치, 구조화된 논리 시퀀스를 표현하는 데 사용할 수 있음 Skyframe 상호작용에 특별히 맞춤화되었습니다. StateMachine를 더 큰 StateMachine로 구성하여 공유할 수 있습니다. 하위 StateMachine개 동시 실행은 항상 구성별로 계층적임 논리적이지요. 모든 동시 하위 태스크가 단일 공유 상위 태스크에서 실행됨 SkyFunction 스레드입니다.

소개

이 섹션에서는StateMachine java.com.google.devtools.build.skyframe.state 패키지에서 찾을 수 있습니다.

Skyframe 재시작에 관한 간략한 소개

Skyframe은 종속 항목 그래프의 병렬 평가를 수행하는 프레임워크입니다. 그래프의 각 노드는 SkyKey는 매개변수를 지정하고 SkyValue는 결과를 지정합니다. 이 SkyFunction은 SkyKey로 SkyValues를 조회할 수 있으며 추가 SkyFunctions의 반복적, 병렬 평가를 트리거합니다. 대신 요청된 SkyValue가 아직 요청이 준비되지 않은 경우 계산의 일부 하위 그래프가 SkyFunction에서 null getValue 응답을 관찰하며 null를 반환해야 합니다. 이는 입력 누락으로 인해 불완전하다는 신호입니다. 이전에 요청된 모든 SkyValues가 있으면 스카이프레임이 SkyFunctions를 다시 시작함 확인할 수 있습니다

SkyKeyComputeState가 도입되기 전에는 계산을 완전히 재실행하는 것이었습니다. 이것은 이차적, 이런 식으로 작성된 함수는 최종적으로 완성됩니다. 매번 재실행되므로 조회가 더 적을수록 null을 반환합니다. SkyKeyComputeState를 사용하면 다음 작업을 할 수 있습니다. 직접 지정한 체크포인트 데이터를 SkyFunction과 연결하여 있습니다.

StateMachineSkyKeyComputeState 내에 상주하며 SkyFunction이 재시작되면 거의 모든 재계산이 이루어집니다( SkyKeyComputeState가 캐시에서 사라지지 않음) 정지 및 재개를 노출하여 실행할 수 있습니다

SkyKeyComputeState 내부의 스테이트풀(Stateful) 계산

객체 지향 설계의 관점에서 볼 때, 객체 지향적, SkyKeyComputeState 내부의 계산 객체를 생성할 수 있습니다. Java에서 객체를 전달하는 동작에 대한 최소한의 설명은 기능 인터페이스로만 충분하며 StateMachine에는 다음이 포함됩니다. 호기심을 자극하는 다음과 같은 정의2.

@FunctionalInterface
public interface StateMachine {
  StateMachine step(Tasks tasks) throws InterruptedException;
}

Tasks 인터페이스는 SkyFunction.Environment와 비슷하지만 비동기성을 위해 설계되었으며 논리적으로 동시 실행되는 하위 태스크를 지원합니다3.

step의 반환 값은 또 다른 StateMachine로, 사양을 단계 시퀀스의 반복을 말합니다. step는 다음과 같은 경우 DONE를 반환합니다. StateMachine이(가) 완료되었습니다. 예를 들면 다음과 같습니다.

class HelloWorld implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    System.out.println("hello");
    return this::step2;  // The next step is HelloWorld.step2.
  }

  private StateMachine step2(Tasks tasks) {
     System.out.println("world");
     // DONE is special value defined in the `StateMachine` interface signaling
     // that the computation is done.
     return DONE;
  }
}

다음 출력을 사용하여 StateMachine를 설명합니다.

hello
world

메서드 참조 this::step2도 다음과 같은 이유로 StateMachine입니다. StateMachine의 기능 인터페이스 정의를 충족하는 step2입니다. 방법 참조는 객체의 다음 상태를 지정하는 가장 일반적인 방법입니다. StateMachine

정지 및 재개

직관적으로 계산을 다음 단계가 아닌 StateMachine 단계로 나눕니다. 모놀리식 함수: 인스턴스를 정지하고 재개하는 데 필요한 후크를 제공합니다. 있습니다. StateMachine.step가 반환되면 명시적인 정지가 발생합니다. 있습니다. 반환된 StateMachine 값으로 지정된 연속은 명시적인 재개 지점. 따라서 재계산을 피할 수 있습니다. 정확히 중단한 지점부터 계산을 시작할 수 있습니다.

콜백, 연속, 비동기 계산

기술적으로 StateMachine연속 역할을 하여 후속 계산을 실행하는 것입니다. StateMachine는 차단하는 대신 이전 코드를 전송하는 step 함수에서 반환하여 자발적으로 정지 Driver 인스턴스로 다시 제어합니다. Driver에서 할 수 있는 작업 그런 다음 준비된 StateMachine로 전환하거나 제어를 다시 Skyframe으로 포기합니다.

일반적으로 콜백연속은 하나의 개념으로 합쳐집니다. 그러나 StateMachine는 둘 사이에 차이를 유지합니다.

  • 콜백 - 비동기식 결과를 저장할 위치를 설명합니다. 있습니다.
  • Continuation - 다음 실행 상태를 지정합니다.

콜백은 비동기 작업을 호출할 때 필요합니다. 즉, 다음과 같이 메서드를 호출하자마자 실제 작업이 발생하지 않습니다. SkyValue 조회의 경우에 해당합니다. 콜백은 최대한 간단하게 유지해야 합니다.

연속StateMachineStateMachine 모든 비동기식 코드 뒤에 뒤따르는 복잡한 실행을 모든 계산이 해결되기 때문입니다. 이 구조적인 접근 방식은 API의 복잡성을 관리할 수 있습니다

작업

Tasks 인터페이스는 SkyValues를 조회하는 API를 StateMachine에 제공합니다. 동시 하위 작업을 예약할 수 있습니다.

interface Tasks {
  void enqueue(StateMachine subtask);

  void lookUp(SkyKey key, Consumer<SkyValue> sink);

  <E extends Exception>
  void lookUp(SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);

  // lookUp overloads for 2 and 3 exception types exist, but are elided here.
}

SkyValue 조회

StateMachineTasks.lookUp 오버로드를 사용하여 SkyValues를 찾습니다. 그들은 SkyFunction.Environment.getValueSkyFunction.Environment.getValueOrThrow이며 유사한 예외 처리가 있습니다. 의미합니다 구현은 즉시 조회를 수행하지는 않지만 그렇게 하기 전에 최대한 많은 조회를 일괄 처리4합니다. 값은 즉시 사용하지 못할 수도 있습니다(예: Skyframe 재시작, 호출자는 콜백을 사용하여 결과 값으로 무엇을 해야 하는지 지정합니다.

StateMachine 프로세서 (Driver 및 SkyFrame)은 다음 전에 값을 사용할 수 있음을 보장합니다. 다음 상태가 시작됩니다. 예를 들면 다음과 같습니다.

class DoesLookup implements StateMachine, Consumer<SkyValue> {
  private Value value;

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.lookUp(new Key(), (Consumer<SkyValue>) this);
    return this::processValue;
  }

  // The `lookUp` call in `step` causes this to be called before `processValue`.
  @Override  // Implementation of Consumer<SkyValue>.
  public void accept(SkyValue value) {
    this.value = (Value)value;
  }

  private StateMachine processValue(Tasks tasks) {
    System.out.println(value);  // Prints the string representation of `value`.
    return DONE;
  }
}

위의 예에서 첫 번째 단계는 new Key()를 조회하여 다음 값을 전달합니다. this: 소비자 이는 DoesLookupConsumer<SkyValue>입니다.

계약에 따라 다음 주 DoesLookup.processValue이(가) 시작되기 전에 모든 DoesLookup.step 조회가 완료되었습니다. 따라서 value는 다음 경우에 사용할 수 있습니다. processValue에서 액세스합니다.

하위 할 일

Tasks.enqueue는 논리적으로 동시 실행되는 하위 태스크의 실행을 요청합니다. 하위 할 일도 StateMachine이며 일반적인 StateMachine 작업을 할 수 있습니다. 재귀적으로 더 많은 하위 할 일을 만들거나 SkyValues를 찾는 등의 작업을 수행할 수 있습니다. lookUp와 마찬가지로 상태 머신 드라이버는 모든 하위 태스크가 완료해야 합니다. 예를 들면 다음과 같습니다.

class Subtasks implements StateMachine {
  private int i = 0;

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.enqueue(new Subtask1());
    tasks.enqueue(new Subtask2());
    // The next step is Subtasks.processResults. It won't be called until both
    // Subtask1 and Subtask 2 are complete.
    return this::processResults;
  }

  private StateMachine processResults(Tasks tasks) {
    System.out.println(i);  // Prints "3".
    return DONE;  // Subtasks is done.
  }

  private class Subtask1 implements StateMachine {
    @Override
    public StateMachine step(Tasks tasks) {
      i += 1;
      return DONE;  // Subtask1 is done.
    }
  }

  private class Subtask2 implements StateMachine {
    @Override
    public StateMachine step(Tasks tasks) {
      i += 2;
      return DONE;  // Subtask2 is done.
    }
  }
}

Subtask1Subtask2는 논리적으로 동시적이지만 모든 것이 따라서 '동시' 스레드가 i의 업데이트에 아무것도 필요하지 않습니다. 동기화를 지원합니다.

구조화된 동시 실행

모든 lookUpenqueue는 다음으로 진행하기 전에 확인되어야 합니다. 즉, 동시 실행이 자연적으로 트리 구조로 제한된다는 의미입니다. 그것은 다음 예와 같이 계층적5 동시 실행을 만들 수 있음 예로 들 수 있습니다

구조화된 동시 실행

동시 실행 구조가 트리를 형성한다고 UML에서 판단하기는 어렵습니다. 대체 보기를 사용하면 생각해야 합니다

구조화되지 않은 동시 실행

구조화된 동시 실행은 추론하기 훨씬 쉽습니다.

컴포지션 및 제어 흐름 패턴

이 섹션에서는 여러 StateMachine를 구성할 수 있는 방법의 예를 보여줍니다. 특정 제어 흐름 문제에 대한 해결책을 찾을 수 있습니다.

순차적 상태

가장 일반적이고 간단한 제어 흐름 패턴입니다. 예시 스테이트풀(Stateful) 컴퓨팅 내부의 스테이트풀(Stateful) 계산에 나와 있습니다. SkyKeyComputeState

분기형

StateMachine의 브랜치 상태는 다양한 일반 Java 제어 흐름을 사용하여 값을 반환할 수 있습니다.

class Branch implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    // Returns different state machines, depending on condition.
    if (shouldUseA()) {
      return this::performA;
    }
    return this::performB;
  }
  …
}

특정 브랜치는 조기 완료를 위해 DONE를 반환하는 것이 매우 일반적입니다.

고급 순차 컴포지션

StateMachine 제어 구조는 메모리가 없으므로 StateMachine을 공유합니다. 하위 할 일로 정의하는 것이 어색할 수 있습니다 M1M2StateMachine StateMachine, S을 공유하는 인스턴스여야 합니다. 여기서 M1M2<A, S, B> 수열이고 <X, S, Y>를 각각 반환합니다. 문제는 S가 완료 후 B 또는 Y로 계속 이동하고 StateMachine는 호출 스택을 표시합니다. 이 섹션에서는 이를 위한 몇 가지 기법을 검토합니다.

터미널 시퀀스 요소로서의 StateMachine

이로 인해 제기된 초기 문제가 해결되지는 않습니다. 순차적 데이터만 볼 수 있습니다 컴포지션의 StateMachine이 시퀀스에서 종결되기만 하면 됩니다.

// S is the shared state machine.
class S implements StateMachine { … }

class M1 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performA();
    return new S();
  }
}

class M2 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performX();
    return new S();
  }
}

이는 S 자체가 복잡한 상태 머신인 경우에도 작동합니다.

순차 컴포지션의 하위 태스크

큐에 추가된 하위 태스크는 다음 상태 전에 완료되는 것이 보장되므로 하위 작업 메커니즘을 약간 악용6할 수도 있습니다.

class M1 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performA();
    // S starts after `step` returns and by contract must complete before `doB`
    // begins. It is effectively sequential, inducing the sequence < A, S, B >.
    tasks.enqueue(new S());
    return this::doB;
  }

  private StateMachine doB(Tasks tasks) {
    performB();
    return DONE;
  }
}

class M2 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performX();
    // Similarly, this induces the sequence < X, S, Y>.
    tasks.enqueue(new S());
    return this::doY;
  }

  private StateMachine doY(Tasks tasks) {
    performY();
    return DONE;
  }
}

runAfter 주사

때때로 Tasks.enqueue를 악용하는 것은 불가능할 수도 있습니다. 왜냐하면 S 전에 완료해야 하는 병렬 하위 작업 또는 Tasks.lookUp 호출 실행됩니다 이 경우 runAfter 매개변수를 S에 삽입하여 다음을 실행할 수 있습니다. S에게 다음으로 할 일을 알려줍니다.

class S implements StateMachine {
  // Specifies what to run after S completes.
  private final StateMachine runAfter;

  @Override
  public StateMachine step(Tasks tasks) {
    … // Performs some computations.
    return this::processResults;
  }

  @Nullable
  private StateMachine processResults(Tasks tasks) {
    … // Does some additional processing.

    // Executes the state machine defined by `runAfter` after S completes.
    return runAfter;
  }
}

class M1 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performA();
    // Passes `this::doB` as the `runAfter` parameter of S, resulting in the
    // sequence < A, S, B >.
    return new S(/* runAfter= */ this::doB);
  }

  private StateMachine doB(Tasks tasks) {
    performB();
    return DONE;
  }
}

class M2 implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks) {
    performX();
    // Passes `this::doY` as the `runAfter` parameter of S, resulting in the
    // sequence < X, S, Y >.
    return new S(/* runAfter= */ this::doY);
  }

  private StateMachine doY(Tasks tasks) {
    performY();
    return DONE;
  }
}

이 방법은 하위 할 일을 악용하는 것보다 더 깔끔합니다. 그러나 이것도 예를 들어 여러 StateMachinerunAfter로 중첩하면 콜백 헬로 향하는 길입니다. 순차적으로 나누는 것이 더 좋음 대신 일반 순차 상태가 있는 runAfter를 사용합니다.

  return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))

다음으로 대체할 수 있습니다.

  private StateMachine step1(Tasks tasks) {
     doStep1();
     return new S(/* runAfter= */ this::intermediateStep);
  }

  private StateMachine intermediateStep(Tasks tasks) {
    return new T(/* runAfter= */ this::nextStep);
  }

금지됨 대안: runAfterUnlessError

이전 초안에서는 취소될 runAfterUnlessError를 고려했습니다. 초기 오류 해결. 이는 오류가 보통 두 번 확인합니다. runAfter 참조가 있는 StateMachine에 의해 한 번 확인되고 runAfter 머신 자체에서 한 번 반복됩니다.

약간의 고민 끝에 우리는 코드의 균일성이 더 중요할 수 있습니다 만약 runAfter 메커니즘이 tasks.enqueue 메커니즘으로, 항상 오류 검사가 필요합니다.

직접 위임

공식적인 상태 전환이 있을 때마다 기본 Driver 루프가 진행됩니다. 계약에 따라 상태 선발은 이전에 SkyValue의 대기열에 추가되었음을 의미합니다. 조회 및 하위 태스크가 다음 상태가 실행되기 전에 해결됩니다. 때때로 논리는 위임 StateMachine의 경우 단계 진행이 불필요하거나 오히려 해로울 수 있습니다. 예를 들어 위임의 첫 번째 step가 위임 상태 조회와 동시에 처리할 수 있는 SkyKey 조회 단계 진행으로 인해 순차적이 됩니다. 아래 예와 같이 직접 위임을 수행합니다.

class Parent implements StateMachine {
  @Override
  public StateMachine step(Tasks tasks ) {
    tasks.lookUp(new Key1(), this);
    // Directly delegates to `Delegate`.
    //
    // The (valid) alternative:
    //   return new Delegate(this::afterDelegation);
    // would cause `Delegate.step` to execute after `step` completes which would
    // cause lookups of `Key1` and `Key2` to be sequential instead of parallel.
    return new Delegate(this::afterDelegation).step(tasks);
  }

  private StateMachine afterDelegation(Tasks tasks) {
    …
  }
}

class Delegate implements StateMachine {
  private final StateMachine runAfter;

  Delegate(StateMachine runAfter) {
    this.runAfter = runAfter;
  }

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.lookUp(new Key2(), this);
    return …;
  }

  // Rest of implementation.
  …

  private StateMachine complete(Tasks tasks) {
    …
    return runAfter;
  }
}

데이터 흐름

이전 논의에서는 제어 흐름 관리에 초점을 맞췄습니다. 이 섹션은 데이터 값의 전파에 대해 설명합니다.

Tasks.lookUp 콜백 구현

SkyValue에서 Tasks.lookUp 콜백을 구현하는 예 조회를 참조하세요. 이 섹션에서는 이러한 주장을 뒷받침하는 근거와 여러 SkyValues를 처리하는 접근 방식을 제공합니다.

콜백 Tasks.lookUp

Tasks.lookUp 메서드는 콜백 sink를 매개변수로 사용합니다.

  void lookUp(SkyKey key, Consumer<SkyValue> sink);

관용적인 접근 방식은 Java 람다를 사용하여 이를 구현하는 것입니다.

  tasks.lookUp(key, value -> myValue = (MyValueClass)value);

여기서 myValue는 다음을 실행하는 StateMachine 인스턴스의 멤버 변수입니다. 조회할 수 있습니다. 그러나 람다는 StateMachine에서 Consumer<SkyValue> 인터페이스 구현 있습니다. 람다는 여러 조회가 있는 경우에도 여전히 유용합니다. 모호합니다.

Tasks.lookUp의 오류 처리 오버로드도 있습니다. 이는 SkyFunction.Environment.getValueOrThrow입니다.

  <E extends Exception> void lookUp(
      SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);

  interface ValueOrExceptionSink<E extends Exception> {
    void acceptValueOrException(@Nullable SkyValue value, @Nullable E exception);
  }

다음은 구현의 예입니다.

class PerformLookupWithError extends StateMachine, ValueOrExceptionSink<MyException> {
  private MyValue value;
  private MyException error;

  @Override
  public StateMachine step(Tasks tasks) {
    tasks.lookUp(new MyKey(), MyException.class, ValueOrExceptionSink<MyException>) this);
    return this::processResult;
  }

  @Override
  public acceptValueOrException(@Nullable SkyValue value, @Nullable MyException exception) {
    if (value != null) {
      this.value = (MyValue)value;
      return;
    }
    if (exception != null) {
      this.error = exception;
      return;
    }
    throw new IllegalArgumentException("Both parameters were unexpectedly null.");
  }

  private StateMachine processResult(Tasks tasks) {
    if (exception != null) {
      // Handles the error.
      …
      return DONE;
    }
    // Processes `value`, which is non-null.
    …
  }
}

오류를 처리하지 않는 조회와 마찬가지로 StateMachine 클래스를 직접 보유함 콜백을 구현하면 람다의 메모리 할당을 절약할 수 있습니다.

오류 처리가 좀 더 자세히 설명하지만 기본적으로 오류 전파와 일반 값 사이에 큰 차이가 없습니다.

여러 SkyValue 사용

종종 SkyValue를 여러 번 조회해야 합니다. 한 가지 접근 방식에서 SkyValue의 유형을 켜는 것입니다. 다음은 프로토타입 프로덕션 코드로부터 단순화되었습니다.

  @Nullable
  private StateMachine fetchConfigurationAndPackage(Tasks tasks) {
    var configurationKey = configuredTarget.getConfigurationKey();
    if (configurationKey != null) {
      tasks.lookUp(configurationKey, (Consumer<SkyValue>) this);
    }

    var packageId = configuredTarget.getLabel().getPackageIdentifier();
    tasks.lookUp(PackageValue.key(packageId), (Consumer<SkyValue>) this);

    return this::constructResult;
  }

  @Override  // Implementation of `Consumer<SkyValue>`.
  public void accept(SkyValue value) {
    if (value instanceof BuildConfigurationValue) {
      this.configurationValue = (BuildConfigurationValue) value;
      return;
    }
    if (value instanceof PackageValue) {
      this.pkg = ((PackageValue) value).getPackage();
      return;
    }
    throw new IllegalArgumentException("unexpected value: " + value);
  }

Consumer<SkyValue> 콜백 구현을 명확하게 공유할 수 있음 값이 다르기 때문입니다. 그렇지 않은 경우 람다 기반 구현이나 전체 내부 클래스 인스턴스를 구현하는 적절한 콜백을 사용해야 합니다.

StateMachine 간 값 전파

지금까지 이 문서에서는 하위 작업에서 작업을 정렬하는 방법만 설명했지만 또한 호출자에게 값을 보고해야 합니다. 하위 할 일이 비동기식으로 처리하면 결과가 호출자에게 다시 전달됩니다. 콜백 이 작업을 수행하기 위해 하위 작업은 삽입됩니다.

class BarProducer implements StateMachine {
  // Callers of BarProducer implement the following interface to accept its
  // results. Exactly one of the two methods will be called by the time
  // BarProducer completes.
  interface ResultSink {
    void acceptBarValue(Bar value);
    void acceptBarError(BarException exception);
  }

  private final ResultSink sink;

  BarProducer(ResultSink sink) {
     this.sink = sink;
  }

  … // StateMachine steps that end with this::complete.

  private StateMachine complete(Tasks tasks) {
    if (hasError()) {
      sink.acceptBarError(getError());
      return DONE;
    }
    sink.acceptBarValue(getValue());
    return DONE;
  }
}

그러면 호출자 StateMachine은 다음과 같이 표시됩니다.

class Caller implements StateMachine, BarProducer.ResultSink {
  interface ResultSink {
    void acceptCallerValue(Bar value);
    void acceptCallerError(BarException error);
  }

  private final ResultSink sink;

  private Bar value;

  Caller(ResultSink sink) {
    this.sink = sink;
  }

  @Override
  @Nullable
  public StateMachine step(Tasks tasks) {
    tasks.enqueue(new BarProducer((BarProducer.ResultSink) this));
    return this::processResult;
  }

  @Override
  public void acceptBarValue(Bar value) {
    this.value = value;
  }

  @Override
  public void acceptBarError(BarException error) {
    sink.acceptCallerError(error);
  }

  private StateMachine processResult(Tasks tasks) {
    // Since all enqueued subtasks resolve before `processResult` starts, one of
    // the `BarResultSink` callbacks must have been called by this point.
    if (value == null) {
      return DONE;  // There was a previously reported error.
    }
    var finalResult = computeResult(value);
    sink.acceptCallerValue(finalResult);
    return DONE;
  }
}

앞의 예는 몇 가지를 보여줍니다. Caller는 자체 Caller.ResultSink를 정의합니다. Caller는 다음을 구현합니다. BarProducer.ResultSink 콜백. 재개 시 processResult는 다음을 확인합니다. value는 null로, 오류가 발생했는지 확인합니다. 이는 Ad Exchange 계정에서 하위 태스크 또는 SkyValue 조회로부터 출력을 수락한 후의 패턴입니다.

acceptBarError 구현은 결과를 빠르게 전달합니다. 오류 버블링에 필요한 Caller.ResultSink

최상위 StateMachine의 대안은 Driver 및 SkyFunctions와 연계합니다.

오류 처리

Tasks.lookUp에는 이미 몇 가지 오류 처리 예시가 있습니다. StateMachines 예외(다음 제외: InterruptedException가 발생하지 않고 대신 다음을 통해 전달됩니다. 콜백을 값으로 사용합니다. 이러한 콜백은 배타적 또는 의미 체계를 갖는 경우가 많으며, 정확히 하나만 전달되는 값 또는 오류입니다.

다음 섹션에서는 스카이프레임과의 미묘하지만 중요한 상호작용에 대해 설명합니다. 있습니다.

도움말 풍선 오류 (--nokeep_going)

오류 버블링 중에는 일부 요청이 없어도 SkyFunction을 다시 시작할 수 있습니다. SkyValues도 이용할 수 있습니다. 이러한 경우 후속 상태는 Tasks API 계약으로 인해 한도에 도달했습니다. 하지만 StateMachine는 다음과 같이 작동해야 합니다. 여전히 예외를 전파합니다

다음 상태에 도달하든 관계없이 전파가 발생해야 하므로 오류 처리 콜백이 이 작업을 수행해야 합니다. 내부 StateMachine의 경우 상위 콜백을 호출하면 됩니다.

SkyFunction과 상호작용하는 최상위 StateMachine에서 다음을 할 수 있습니다. ValueOrExceptionProducersetException 메서드를 호출하면 됩니다. 그러면 ValueOrExceptionProducer.tryProduceValue에서 예외가 발생합니다. 찾을 수 있습니다.

Driver를 직접 사용하는 경우 오류가 발생할 수 있습니다. 가장 적합합니다

이벤트 처리

이벤트를 내보내야 하는 SkyFunctions의 경우 StoredEventHandler가 삽입됩니다. SkyKeyComputeState에 추가하고 StateMachine 있습니다. 이전에는 Skyframe 감소로 인해 StoredEventHandler가 필요했습니다. 특정 이벤트가 다시 실행되지 않는 한 발생하지만 이 문제는 이후에 수정되었습니다. StoredEventHandler 삽입은 오류 처리 콜백에서 내보낸 이벤트의 구현입니다.

Driver 및 SkyFunctions로 연결

DriverStateMachine의 실행을 관리합니다. 지정된 루트 StateMachine로 시작합니다. StateMachine에서 할 수 있는 작업 하위 태스크 StateMachine를 재귀적으로 큐에 추가하면 단일 Driver로 하위 할 일이 많습니다. 이러한 하위 태스크는 구조화된 동시 실행. Driver는 SkyValue를 일괄 처리합니다. 하위 태스크 전체를 조회하여 효율성을 높입니다.

Driver를 중심으로 다음 API를 사용하여 빌드된 여러 클래스가 있습니다.

public final class Driver {
  public Driver(StateMachine root);
  public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}

Driver는 단일 루트 StateMachine를 매개변수로 사용합니다. 전화 거는 중 Driver.driveStateMachine 스카이프레임 다시 시작 StateMachine가 완료되면 true를 반환하고 false를 반환합니다. 이는 일부 값을 사용할 수 없음을 나타냅니다.

DriverStateMachine의 동시 상태를 유지하며 문제 없음 SkyKeyComputeState에 삽입하는 데 적합합니다.

직접 Driver 인스턴스화

StateMachine 구현은 일반적으로 다음을 통해 결과를 전달합니다. 있습니다. 다음 예와 같이 Driver를 직접 인스턴스화할 수 있습니다. 다음 예를 참고하세요.

Driver는 다음과 함께 SkyKeyComputeState 구현에 삽입됩니다. 좀 더 자세히 정의할 해당 ResultSink의 구현 감소합니다. 최상위 수준에서 State 객체는 Driver보다 오래 지속됩니다.

class State implements SkyKeyComputeState, ResultProducer.ResultSink {
  // The `Driver` instance, containing the full tree of all `StateMachine`
  // states. Responsible for calling `StateMachine.step` implementations when
  // asynchronous values are available and performing batched SkyFrame lookups.
  //
  // Non-null while `result` is being computed.
  private Driver resultProducer;

  // Variable for storing the result of the `StateMachine`
  //
  // Will be non-null after the computation completes.
  //
  private ResultType result;

  // Implements `ResultProducer.ResultSink`.
  //
  // `ResultProducer` propagates its final value through a callback that is
  // implemented here.
  @Override
  public void acceptResult(ResultType result) {
    this.result = result;
  }
}

아래 코드는 ResultProducer를 스케치합니다.

class ResultProducer implements StateMachine {
  interface ResultSink {
    void acceptResult(ResultType value);
  }

  private final Parameters parameters;
  private final ResultSink sink;

  … // Other internal state.

  ResultProducer(Parameters parameters, ResultSink sink) {
    this.parameters = parameters;
    this.sink = sink;
  }

  @Override
  public StateMachine step(Tasks tasks) {
    …  // Implementation.
    return this::complete;
  }

  private StateMachine complete(Tasks tasks) {
    sink.acceptResult(getResult());
    return DONE;
  }
}

결과를 지연 계산하기 위한 코드는 다음과 같을 수 있습니다.

@Nullable
private Result computeResult(State state, Skyfunction.Environment env)
    throws InterruptedException {
  if (state.result != null) {
    return state.result;
  }
  if (state.resultProducer == null) {
    state.resultProducer = new Driver(new ResultProducer(
      new Parameters(), (ResultProducer.ResultSink)state));
  }
  if (state.resultProducer.drive(env)) {
    // Clears the `Driver` instance as it is no longer needed.
    state.resultProducer = null;
  }
  return state.result;
}

Driver 삽입

StateMachine가 값을 생성하고 예외가 발생하지 않으면 임베딩 Driver는 다음 예와 같이 가능한 또 다른 구현입니다.

class ResultProducer implements StateMachine {
  private final Parameters parameters;
  private final Driver driver;

  private ResultType result;

  ResultProducer(Parameters parameters) {
    this.parameters = parameters;
    this.driver = new Driver(this);
  }

  @Nullable  // Null when a Skyframe restart is needed.
  public ResultType tryProduceValue( SkyFunction.Environment env)
      throws InterruptedException {
    if (!driver.drive(env)) {
      return null;
    }
    return result;
  }

  @Override
  public StateMachine step(Tasks tasks) {
    …  // Implementation.
}

SkyFunction에는 다음과 같은 코드가 있을 수 있습니다 (여기서 State는 함수별 SkyKeyComputeState)

@Nullable  // Null when a Skyframe restart is needed.
Result computeResult(SkyFunction.Environment env, State state)
    throws InterruptedException {
  if (state.result != null) {
    return state.result;
  }
  if (state.resultProducer == null) {
    state.resultProducer = new ResultProducer(new Parameters());
  }
  var result = state.resultProducer.tryProduceValue(env);
  if (result == null) {
    return null;
  }
  state.resultProducer = null;
  return state.result = result;
}

StateMachine 구현에 Driver를 삽입하는 것이 스카이프레임의 동기 코딩 스타일

예외를 생성할 수 있는 StateMachine

그 외의 경우에는 SkyKeyComputeState 삽입 가능한 ValueOrExceptionProducer가 있습니다. 일치시킬 동기 API가 있는 ValueOrException2Producer 클래스 동기 SkyFunction 코드입니다.

ValueOrExceptionProducer 추상 클래스에는 다음 메서드가 포함되어 있습니다.

public abstract class ValueOrExceptionProducer<V, E extends Exception>
    implements StateMachine {
  @Nullable
  public final V tryProduceValue(Environment env)
      throws InterruptedException, E {
    …  // Implementation.
  }

  protected final void setValue(V value)  {  … // Implementation. }
  protected final void setException(E exception) {  … // Implementation. }
}

이 인스턴스는 삽입된 Driver 인스턴스를 포함하고 있으며 이 인스턴스는 Embedding 드라이버 및 인터페이스의 ResultProducer 클래스 SkyFunction을 비슷한 방식으로 사용합니다. ResultSink를 정의하는 대신 구현은 둘 중 하나가 발생하면 setValue 또는 setException를 호출합니다. 둘 다 발생하면 예외가 우선 적용됩니다. tryProduceValue 메서드 비동기 콜백 코드를 동기 코드에 연결하여 예외를 둘 수 있습니다.

앞서 언급했듯이 오류 버블링 중에 오류가 발생할 수 있으며 일부 입력을 사용할 수 없으므로 머신이 아직 완료되지 않았더라도 말이죠. 받는사람 따라서 tryProduceValue는 끝났습니다.

에필로그: 최종적으로 콜백 삭제

StateMachine는 매우 효율적이지만 상용구 집약적인 실행 방법입니다. 살펴보겠습니다 연속 (특히 Runnable 형식) ListenableFuture에 전달됨)는 Bazel 코드의 특정 부분에서 광범위하게 사용됩니다. SkyFunctions 분석에서는 자주 사용되지 않습니다. 분석은 대부분 CPU의 제약을 받으며 디스크 I/O를 위한 효율적인 비동기 API는 없습니다. 결국에는 학습 곡선이 있고 방해 요소가기 때문에 원정 콜백에 최적화하면 좋습니다. 있습니다.

가장 유망한 대안 중 하나는 Java 가상 스레드입니다. 대신 콜백을 작성해야 하므로 모든 것이 동기식 및 차단 해제로 대체됩니다. 있습니다. 이렇게 할 수 있는 이유는 단일 스레드가 아닌 가상 스레드 리소스를 저렴해야 하기 때문입니다. 그러나 가상 스레드를 사용하더라도 간단한 동기 작업을 스레드 생성 및 동기화로 대체 너무 비쌉니다. StateMachine에서 Java 가상 스레드는 엄청나게 느려서 엔드 투 엔드 분석 지연 시간이 거의 3배 증가합니다. 가상 스레드는 미리보기 기능이므로 한 번에 나중에 실적이 개선되는 날짜를 기준으로 할 수 있습니다.

고려해야 할 또 다른 접근 방식은 Loom 코루틴을 기다리는 것입니다. 확인할 수 있습니다 이 경우의 장점은 동기화 오버헤드를 줄이는 것이 목표입니다.

다른 모든 방법이 실패하면 하위 수준의 바이트 코드 재작성도 실행 가능할 수 있습니다. 대안입니다. 최적화가 충분하면 성능을 향상시키는 데 도움이 됩니다.

부록

콜백 헬

콜백 헬은 콜백을 사용하는 비동기 코드의 악명 높은 문제입니다. 후속 단계의 연속이 중첩되고 살펴봤습니다 단계가 많은 경우 이 중첩은 매우 깊이 파고듭니다. 제어 흐름과 결합되면 코드를 관리할 수 없게 됩니다.

class CallbackHell implements StateMachine {
  @Override
  public StateMachine step(Tasks task) {
    doA();
    return (t, l) -> {
      doB();
      return (t1, l2) -> {
        doC();
        return DONE;
      };
    };
  }
}

중첩 구현의 장점 중 하나는 보존할 수 있습니다 자바에서 캡처된 람다 변수는 최종 버전이 결정되므로 이러한 변수를 사용하는 것이 번거로울 수 있습니다. 딥 중첩은 람다가 아니라 메서드 참조를 연속으로 반환하여 피합니다. 참조하세요.

class CallbackHellAvoided implements StateMachine {
  @Override
  public StateMachine step(Tasks task) {
    doA();
    return this::step2;
  }

  private StateMachine step2(Tasks tasks) {
    doB();
    return this::step3;
  }

  private StateMachine step3(Tasks tasks) {
    doC();
    return DONE;
  }
}

runAfter 삽입이 패턴은 너무 조밀하게 사용되지만, 인젝션을 여기저기 흩어 놓으면 순차적으로 전달될 수 있습니다

예: 체이닝된 SkyValue 조회

애플리케이션 로직에 종속 항목 체인이 필요한 예를 들어 두 번째 SkyKey가 첫 번째 SkyValue에 종속되는 경우 SkyValue는 조회를 수행합니다. 단순하게 생각해보면 복잡하고 깊이 중첩된 합니다.

private ValueType1 value1;
private ValueType2 value2;

private StateMachine step1(...) {
  tasks.lookUp(key1, (Consumer<SkyValue>) this);  // key1 has type KeyType1.
  return this::step2;
}

@Override
public void accept(SkyValue value) {
  this.value1 = (ValueType1) value;
}

private StateMachine step2(...) {
  KeyType2 key2 = computeKey(value1);
  tasks.lookup(key2, this::acceptValueType2);
  return this::step3;
}

private void acceptValueType2(SkyValue value) {
  this.value2 = (ValueType2) value;
}

그러나 연속은 메서드 참조로 지정되므로 코드는 상태 전환 절차: step2step1를 따릅니다. 여기서 람다는 value2를 할당하는 데 사용됩니다. 이렇게 하면 코드의 순서가 가장 낮은 순위로 계산되는 것을 가리킵니다.

기타 도움말

가독성: 실행 순서 지정

가독성을 높이려면 StateMachine.step 구현을 유지하도록 노력하세요. 실행 순서 및 콜백 구현의 바로 그 뒤에 가 코드에 전달됩니다. 제어 흐름의 제어 흐름에 따라 항상 브랜치. 이러한 경우 추가 의견이 도움이 될 수 있습니다.

예: 체이닝된 SkyValue 조회에서 중간 메서드 참조가 생성됩니다. 이렇게 하면 가독성을 위해 성능을 크게 개선해야 합니다.

세대 가설

수명이 중간인 Java 객체는 Java의 세대 기반 가설을 깨고 가비지 컬렉터는 매우 오래 지속되는 객체를 처리하도록 고안되었습니다. 영원히 사는 물건을 예로 들 수 있습니다 정의상 SkyKeyComputeState개는 가설을 위반합니다. 이러한 객체는 Driver에 뿌리를 두고 여전히 실행 중인 모든 StateMachine의 구성된 트리가 정지되는 동안 중간 수명을 유지하여 비동기 계산을 대기함 완료합니다.

JDK19에서는 덜 나빠 보일 수 있지만 StateMachine를 사용할 때 가끔 기존 메모리의 비중이 급격하게 감소하더라도 GC 시간이 있습니다. StateMachine의 수명이 중간이므로 세대로 승급할 수 있어 더 빨리 채워질 수 있었고, 정리를 위해 더 많은 비용이 드는 주 GC 또는 전체 GC가 필요합니다.

초기 예방 조치는 StateMachine 변수의 사용을 최소화하는 것이지만 항상 실행 가능한 것은 아닙니다. 예를 들어 값이 여러 있습니다. 가능한 경우 로컬 스택 step 변수는 새로운 세대입니다. 효율적으로 GC'd를 처리합니다.

StateMachine 변수의 경우 하위 작업으로 분류하고 따라가기 두 값 사이에 값을 전파하기 위해 권장되는 StateMachine도 유용합니다. 포드의 패턴을 따르면 하위 StateMachine만 상위 요소에 대한 참조를 가집니다. StateMachine와 같아야 하며 그 반대의 경우도 마찬가지입니다. 즉, 아이들이 결과 콜백을 사용하여 상위 요소를 업데이트하면 하위 요소가 GC 자격요건을 충족하게 됩니다.

마지막으로 일부 경우에는 이전 상태에서 StateMachine 변수가 필요합니다. 나중의 상태는 아닙니다. 인코더-디코더 아키텍처를 기반으로 하는 대규모 객체를 삭제할 수 있습니다.

상태 이름 지정

메서드 이름을 지정할 때 일반적으로 동작에 대한 메서드 이름을 지정할 수 있습니다. 확인할 수 있습니다. 이 작업을 수행하는 방법은 StateMachine. 스택이 없기 때문입니다. 예를 들어 foo 메서드가 있다고 가정해 보겠습니다. 하위 메서드 bar를 호출합니다. StateMachine에서 이는 다음으로 변환될 수 있습니다. 상태 시퀀스 foo로 뒤이어 bar가 나옵니다. foo에 더 이상 동작이 포함되지 않습니다. bar입니다. 따라서 상태의 메서드 이름은 범위가 더 좁아지는 경향이 있습니다. 잠재적인 지역 행동이 반영될 수 있습니다.

동시 실행 트리 다이어그램

다음은 구조화된 동시 실행을 지원합니다. 블록은 작은 트리를 형성합니다.

구조화된 동시 실행 3D


  1. 즉, Skyframe의 규칙과 대조적으로 값을 사용할 수 없습니다. 

  2. stepInterruptedException을 발생시킬 수 있지만 예에서는 생략합니다. Bazel 코드에는 이 예외는 Driver까지 전파됩니다. 나중에 설명하겠습니다. - StateMachine를 실행하는 오류가 발생할 경우 있습니다.

  3. 동시 하위 태스크는 ConfiguredTargetFunction에서 동기를 부여했습니다. 는 각 종속 항목에 대해 독립적인 작업을 수행합니다. 인코더-디코더 아키텍처를 모든 종속 항목을 한 번에 처리하는 복잡한 데이터 구조인 각 종속 항목은 서로 독립적이며 StateMachine

  4. 한 단계에 있는 여러 개의 tasks.lookUp 호출은 함께 일괄 처리됩니다. 추가 일괄 처리는 동시 실행 내에서 발생하는 조회를 통해 생성될 수 있습니다. 하위 할 일 목록이 표시됩니다. 

  5. Java의 구조화된 동시 실행과 개념적으로 유사합니다. jeps/428에 연결합니다. 

  6. 이는 스레드를 생성하고 조인하여 순차 합성을 사용합니다.