Skyframe StateMachines का इस्तेमाल करने के लिए गाइड

खास जानकारी

Skyframe StateMachine, एक डीकंस्ट्रक्टेड फ़ंक्शन-ऑब्जेक्ट है, जो हीप पर मौजूद होता है. जब ज़रूरी वैल्यू तुरंत उपलब्ध नहीं होती हैं, लेकिन उन्हें एसिंक्रोनस तरीके से कंप्यूट किया जाता है, तब यह फ़्लेक्सिबल और बिना किसी रुकावट के आकलन करने की सुविधा देता है1. इंतज़ार करते समय, StateMachine थ्रेड रिसॉर्स को बांध नहीं सकता. इसके बजाय, इसे निलंबित और फिर से शुरू करना होगा. इसलिए, डीकंस्ट्रक्शन से साफ़ तौर पर री-एंट्री पॉइंट का पता चलता है, ताकि पिछली गणनाओं को स्किप किया जा सके.

StateMachine का इस्तेमाल, क्रम, ब्रांचिंग, स्ट्रक्चर्ड लॉजिकल कंकरेंसी को दिखाने के लिए किया जा सकता है. इन्हें खास तौर पर Skyframe इंटरैक्शन के लिए बनाया गया है. StateMachine को बड़े StateMachine में कंपोज़ किया जा सकता है और सब-StateMachine शेयर किए जा सकते हैं. कॉन्करेंसी हमेशा हैरारकल होती है और पूरी तरह से लॉजिकल होती है. एक साथ चलने वाले हर सबटास्क को, शेयर की गई एक ही पैरंट SkyFunction थ्रेड में चलाया जाता है.

परिचय

इस सेक्शन में, java.com.google.devtools.build.skyframe.state पैकेज में मौजूद StateMachine के बारे में कम शब्दों में बताया गया है.

Skyframe को रीस्टार्ट करने के बारे में खास जानकारी

Skyframe एक ऐसा फ़्रेमवर्क है जो डिपेंडेंसी ग्राफ़ का पैरलल तरीके से आकलन करता है. ग्राफ़ में मौजूद हर नोड, SkyFunction के आकलन से जुड़ा होता है. इसमें SkyKey, पैरामीटर के बारे में बताता है और SkyValue, नतीजे के बारे में बताता है. कंप्यूटेशनल मॉडल ऐसा होता है कि SkyFunction, SkyKey की मदद से SkyValues को ढूंढ सकता है. इससे अतिरिक्त SkyFunctions का रिकर्सिव और पैरलल तरीके से आकलन ट्रिगर होता है. अनुरोध किए गए SkyValue के तैयार न होने पर, थ्रेड को ब्लॉक करने के बजाय, अनुरोध करने वाला SkyFunction, null getValue रिस्पॉन्स देखता है. ऐसा तब होता है, जब कंप्यूटेशन का कुछ सबग्राफ़ पूरा नहीं होता. इसके बाद, उसे SkyValue के बजाय null दिखाना चाहिए. इससे पता चलता है कि इनपुट मौजूद न होने की वजह से, यह पूरा नहीं हुआ है. जब पहले से अनुरोध की गई सभी SkyValues उपलब्ध हो जाती हैं, तब Skyframe, SkyFunctions को रीस्टार्ट करता है.

SkyKeyComputeState को लागू करने से पहले, रीस्टार्ट को मैनेज करने का पारंपरिक तरीका यह था कि कंप्यूटेशन को पूरी तरह से फिर से चलाया जाए. हालांकि, इसमें क्वाड्रेटिक कॉम्प्लेक्सिटी होती है, लेकिन इस तरह से लिखे गए फ़ंक्शन आखिर में पूरे हो जाते हैं. ऐसा इसलिए होता है, क्योंकि हर बार फिर से चलाने पर, कम लुकअप null दिखाते हैं. SkyKeyComputeState की मदद से, हाथ से तय किए गए चेक-पॉइंट डेटा को SkyFunction से जोड़ा जा सकता है. इससे, दोबारा कैलकुलेट करने की ज़रूरत नहीं पड़ती.

StateMachine ऐसे ऑब्जेक्ट होते हैं जो SkyKeyComputeState में मौजूद होते हैं. ये SkyFunction के रीस्टार्ट होने पर, लगभग सभी रीकंप्यूटेशन को हटा देते हैं. हालांकि, ऐसा तब होता है, जब SkyKeyComputeState कैश मेमोरी से बाहर न हो. इसके लिए, ये सस्पेंड और रिज़्यूम एक्ज़ीक्यूशन हुक को दिखाते हैं.

SkyKeyComputeState के अंदर स्टेटफ़ुल कंप्यूटेशन

ऑब्जेक्ट ओरिएंटेड डिज़ाइन के हिसाब से, SkyKeyComputeState में सिर्फ़ डेटा वैल्यू के बजाय, कंप्यूटेशनल ऑब्जेक्ट सेव करना बेहतर होता है. Java में, किसी ऑब्जेक्ट के व्यवहार के बारे में कम से कम जानकारी देने वाला इंटरफ़ेस, फ़ंक्शनल इंटरफ़ेस होता है. यह जानकारी काफ़ी होती है. StateMachine की परिभाषा, कुछ इस तरह से दी गई है2.

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

Tasks इंटरफ़ेस, SkyFunction.Environment से मिलता-जुलता है. हालांकि, इसे एसिंक्रोनस के लिए डिज़ाइन किया गया है. साथ ही, यह लॉजिकल तौर पर एक साथ होने वाले सबटास्क के साथ काम करता है3.

step की रिटर्न वैल्यू, एक और StateMachine होती है. इससे, चरणों के क्रम को इंडक्टिव तरीके से तय किया जा सकता है. StateMachine पूरा होने पर, step DONE दिखाता है. उदाहरण के लिए:

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

ध्यान दें कि StateMachine के फ़ंक्शनल इंटरफ़ेस की परिभाषा को पूरा करने की वजह से, step2 के साथ-साथ this::step2 भी एक StateMachine है. StateMachine में अगली स्थिति तय करने के लिए, मेथड रेफ़रंस का इस्तेमाल सबसे ज़्यादा किया जाता है.

निलंबित करना और फिर से शुरू करना

किसी भी कंप्यूटेशन को एक बड़े फ़ंक्शन के बजाय StateMachine चरणों में बांटने से, निलंबित और फिर से शुरू करने के लिए ज़रूरी हुक मिलते हैं. StateMachine.step के वापस आने पर, निलंबन का एक पॉइंट होता है. रिटर्न की गई StateMachine वैल्यू से तय किया गया कंटीन्यूएशन, एक साफ़ तौर पर बताया गया फिर से शुरू करें पॉइंट है. इसलिए, फिर से हिसाब लगाने की ज़रूरत नहीं पड़ती, क्योंकि हिसाब लगाने की प्रोसेस को वहीं से शुरू किया जा सकता है जहां उसे छोड़ा गया था.

कॉलबैक, जारी रखने की सुविधा, और एसिंक्रोनस कंप्यूटेशन

तकनीकी तौर पर, StateMachine एक जारी रखने वाला फ़ंक्शन है. यह तय करता है कि इसके बाद कौनसी गणना की जाएगी. ब्लॉक करने के बजाय, StateMachine suspend कर सकता है. इसके लिए, उसे step फ़ंक्शन से वापस आना होगा. इससे कंट्रोल वापस Driver इंस्टेंस को मिल जाता है. इसके बाद, Driver को StateMachine के लिए तैयार किया जा सकता है या कंट्रोल वापस Skyframe को दिया जा सकता है.

आम तौर पर, कॉलबैक और कंटिन्यूएशन को एक ही कॉन्सेप्ट माना जाता है. हालांकि, StateMachines इन दोनों के बीच अंतर बनाए रखते हैं.

  • कॉलबैक - इससे पता चलता है कि एसिंक्रोनस कंप्यूटेशन के नतीजे को कहां सेव करना है.
  • जारी रखना - इससे, अगले चरण की स्थिति के बारे में पता चलता है.

एसिंक्रोनस ऑपरेशन शुरू करने के लिए, कॉलबैक की ज़रूरत होती है. इसका मतलब है कि SkyValue लुकअप के मामले में, तरीका कॉल करने पर तुरंत ऑपरेशन नहीं होता है. कॉलबैक को जितना हो सके उतना आसान रखना चाहिए.

कंटिन्यूएशन, StateMachine के StateMachine रिटर्न वैल्यू होते हैं. साथ ही, ये सभी एसिंक्रोनस कंप्यूटेशन के हल हो जाने के बाद, जटिल एक्ज़ीक्यूशन को शामिल करते हैं. इस स्ट्रक्चर्ड अप्रोच से, कॉल बैक की जटिलता को मैनेज करने में मदद मिलती है.

Tasks

Tasks इंटरफ़ेस, StateMachines को SkyKey के हिसाब से SkyValues देखने और एक साथ कई सबटास्क शेड्यूल करने के लिए एपीआई उपलब्ध कराता है.

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 लुकअप

StateMachine, SkyValues को खोजने के लिए Tasks.lookUp ओवरलोड का इस्तेमाल करते हैं. ये SkyFunction.Environment.getValue और SkyFunction.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 को उपभोक्ता के तौर पर पास किया जाता है. ऐसा इसलिए हो सकता है, क्योंकि DoesLookup Consumer<SkyValue> को लागू करता है.

अनुबंध के मुताबिक, अगली स्थिति DoesLookup.processValue शुरू होने से पहले, DoesLookup.step के सभी लुकअप पूरे हो जाते हैं. इसलिए, processValue में ऐक्सेस करने पर value उपलब्ध होता है.

सबटास्क

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.
    }
  }
}

हालांकि, Subtask1 और Subtask2 एक साथ काम करते हैं, लेकिन सब कुछ एक ही थ्रेड में चलता है. इसलिए, i के "एक साथ" अपडेट के लिए किसी भी तरह के सिंक्रनाइज़ेशन की ज़रूरत नहीं होती.

स्ट्रक्चर्ड कॉनकरेंसी

हर lookUp और enqueue को अगली स्थिति पर जाने से पहले हल करना ज़रूरी है. इसका मतलब है कि कॉन्करेंसी, ट्री-स्ट्रक्चर तक ही सीमित है. यहां दिए गए उदाहरण में दिखाया गया है कि क्रम के हिसाब से 5 कंकरेंसी बनाई जा सकती है.

स्ट्रक्चर्ड कॉनकरेंसी

यूएमएल से यह पता लगाना मुश्किल है कि कॉंकुरेंसी स्ट्रक्चर, ट्री बनाता है. दूसरा व्यू भी उपलब्ध है, जिसमें ट्री स्ट्रक्चर को बेहतर तरीके से दिखाया गया है.

अनस्ट्रक्चर्ड कॉनकरेंसी

स्ट्रक्चर्ड कंकरेंसी को समझना बहुत आसान है.

कंपोज़िशन और कंट्रोल फ़्लो पैटर्न

इस सेक्शन में, एक से ज़्यादा StateMachine को कंपोज़ करने के तरीके के उदाहरण दिए गए हैं. साथ ही, कंट्रोल फ़्लो से जुड़ी कुछ समस्याओं के समाधान भी दिए गए हैं.

क्रम से लागू होने वाली स्थितियां

यह कंट्रोल फ़्लो का सबसे सामान्य और आसान पैटर्न है. इसका एक उदाहरण 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 की परिभाषाओं को सबटास्क के तौर पर शेयर करना कभी-कभी मुश्किल हो सकता है. मान लें कि M1 और M2, StateMachine के ऐसे इंस्टेंस हैं जो StateMachine, S को शेयर करते हैं. साथ ही, M1 और M2, क्रम से <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 कॉल पूरे होने चाहिए. इस मामले में, S में runAfter पैरामीटर इंजेक्ट करके, 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;
  }
}

यह तरीका, सबटास्क का गलत इस्तेमाल करने से बेहतर है. हालांकि, इसका बहुत ज़्यादा इस्तेमाल करने से कॉलबैक हैल की समस्या हो सकती है. उदाहरण के लिए, runAfter के साथ कई StateMachine नेस्ट करना. इसलिए, बेहतर होगा कि आप क्रम से चलने वाली 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 पर विचार किया था, जो गड़बड़ियों के मामले में तुरंत बंद हो जाएगा. ऐसा इसलिए किया गया है, क्योंकि गड़बड़ियों की जांच अक्सर दो बार की जाती है. पहली बार, StateMachine से की जाती है, जिसमें runAfter का रेफ़रंस होता है. दूसरी बार, runAfter मशीन से की जाती है.

कुछ समय तक विचार-विमर्श करने के बाद, हमने यह फ़ैसला किया कि कोड में एकरूपता होना, गड़बड़ी की जांच करने वाले कोड को डुप्लीकेट होने से बचाने से ज़्यादा ज़रूरी है. अगर runAfter मैकेनिज़्म, tasks.enqueue मैकेनिज़्म के साथ लगातार काम नहीं करता है, तो यह भ्रम की स्थिति पैदा कर सकता है. 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 lookups में Tasks.lookUp कॉलबैक लागू करने का उदाहरण यहां दिया गया है. इस सेक्शन में, एक से ज़्यादा SkyValue को मैनेज करने के तरीकों के बारे में बताया गया है. साथ ही, इसके पीछे की वजह भी बताई गई है.

Tasks.lookUp कॉलबैक

Tasks.lookUp वाला तरीका, कॉलबैक sink को पैरामीटर के तौर पर लेता है.

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

इस सुविधा को लागू करने के लिए, Java लैंबडा का इस्तेमाल करना सबसे सही तरीका होगा:

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

यहां myValue, StateMachine इंस्टेंस का सदस्य वैरिएबल है, जो लुकअप कर रहा है. हालांकि, 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 शून्य है, ताकि यह पता चल सके कि कोई गड़बड़ी हुई है या नहीं. यह एक सामान्य व्यवहार है. ऐसा तब होता है, जब कोई उपयोगकर्ता किसी सबटास्क या SkyValue लुकअप से मिले आउटपुट को स्वीकार करता है.

ध्यान दें कि acceptBarError, गड़बड़ी की जानकारी आगे बढ़ाने की सुविधा की ज़रूरत के मुताबिक, नतीजे को तुरंत Caller.ResultSink को भेज देता है.

टॉप-लेवल StateMachine के विकल्पों के बारे में Driver और SkyFunctions पर स्विच करना लेख में बताया गया है.

गड़बड़ी ठीक करना

Tasks.lookUp callbacks और Propagating values between StateMachines में, गड़बड़ी ठीक करने के कुछ उदाहरण पहले से मौजूद हैं. InterruptedException के अलावा, अन्य अपवाद नहीं दिखाए जाते. हालांकि, इन्हें वैल्यू के तौर पर कॉलबैक के ज़रिए पास किया जाता है. इस तरह के कॉलबैक में अक्सर एक्सक्लूसिव-ओआर सिमैंटिक्स होते हैं. इनमें वैल्यू या गड़बड़ी में से सिर्फ़ एक को पास किया जाता है.

अगले सेक्शन में, Skyframe की गड़बड़ी ठीक करने की सुविधा के साथ होने वाले एक छोटे, लेकिन अहम इंटरैक्शन के बारे में बताया गया है.

Error bubbling (--nokeep_going)

गड़बड़ी की जानकारी देने के दौरान, SkyFunction को फिर से शुरू किया जा सकता है. भले ही, अनुरोध किए गए सभी SkyValues उपलब्ध न हों. ऐसे मामलों में, Tasks API के कानूनी समझौते की वजह से, अगली स्थिति कभी नहीं पहुंच पाएगी. हालांकि, StateMachine को अब भी अपवाद को आगे बढ़ाना चाहिए.

गड़बड़ी की जानकारी को आगे बढ़ाना ज़रूरी है. भले ही, अगली स्थिति तक पहुंचा गया हो या नहीं. इसलिए, गड़बड़ी को ठीक करने वाले कॉलबैक को यह टास्क पूरा करना होगा. इनर StateMachine के लिए, पैरंट कॉलबैक को लागू करके ऐसा किया जाता है.

टॉप-लेवल StateMachine, जो SkyFunction के साथ इंटरफ़ेस करता है, ऐसा ValueOrExceptionProducer के setException तरीके को कॉल करके किया जा सकता है. इसके बाद, ValueOrExceptionProducer.tryProduceValue अपवाद दिखाएगा. भले ही, SkyValues मौजूद न हों.

अगर Driver का इस्तेमाल सीधे तौर पर किया जा रहा है, तो SkyFunction से मिली गड़बड़ियों की जांच करना ज़रूरी है. भले ही, मशीन ने प्रोसेसिंग पूरी न की हो.

इवेंट हैंडलिंग

जिन SkyFunctions को इवेंट भेजने होते हैं उनके लिए, StoredEventHandler को SkyKeyComputeState में इंजेक्ट किया जाता है. इसके बाद, इसे उन StateMachines में इंजेक्ट किया जाता है जिनके लिए इनकी ज़रूरत होती है. पहले, Skyframe के कुछ इवेंट को फिर से चलाने के लिए StoredEventHandler की ज़रूरत होती थी. हालांकि, बाद में इस समस्या को ठीक कर दिया गया. StoredEventHandler इंजेक्शन को बनाए रखा जाता है, क्योंकि इससे गड़बड़ी ठीक करने वाले कॉलबैक से जनरेट होने वाले इवेंट को लागू करना आसान हो जाता है.

Driver और SkyFunctions पर ब्रिज करना

Driver, StateMachine के एक्ज़ीक्यूशन को मैनेज करने के लिए ज़िम्मेदार होता है. यह काम, तय किए गए रूट StateMachine से शुरू होता है. StateMachine, सबटास्क StateMachine को बार-बार कतार में लगा सकते हैं. इसलिए, एक Driver कई सबटास्क मैनेज कर सकता है. ये सब-टास्क, ट्री स्ट्रक्चर बनाते हैं. यह स्ट्रक्चर्ड कंकरेंसी का नतीजा है. Driver, बेहतर परफ़ॉर्मेंस के लिए सभी उप-टास्क में SkyValue लुकअप को बैच करता है.

Driver के आस-पास कई क्लास बनाई गई हैं. इनमें ये एपीआई शामिल हैं.

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

Driver, एक पैरामीटर के तौर पर एक रूट StateMachine लेता है. कॉल करने पर, Driver.drive को Skyframe रीस्टार्ट किए बिना जितना हो सके उतना काम करना होता है.StateMachine StateMachine पूरा होने पर, यह फ़ंक्शन सही वैल्यू दिखाता है. ऐसा न होने पर, यह गलत वैल्यू दिखाता है. इससे पता चलता है कि सभी वैल्यू उपलब्ध नहीं थीं.

Driver, StateMachine के मौजूदा स्टेटस को बनाए रखता है. साथ ही, इसे 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 को StateMachine लागू करने के तरीके में एम्बेड करना, Skyframe की सिंक्रोनस कोडिंग स्टाइल के लिए ज़्यादा सही है.Driver

ऐसी स्टेट मशीनें जो अपवाद जनरेट कर सकती हैं

इसके अलावा, SkyKeyComputeState-embeddable ValueOrExceptionProducer और 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 driver में मौजूद ResultProducer क्लास से मिलता-जुलता होता है. यह SkyFunction के साथ भी इसी तरह इंटरफ़ेस करता है. ResultSink को तय करने के बजाय, लागू करने वाले फ़ंक्शन, setValue या setException को कॉल करते हैं. अगर दोनों एक साथ होते हैं, तो अपवाद को प्राथमिकता दी जाती है. tryProduceValue तरीका, एसिंक्रोनस कॉलबैक कोड को सिंक्रोनस कोड से जोड़ता है. साथ ही, जब कोई कोड सेट किया जाता है, तो एक अपवाद दिखाता है.

जैसा कि पहले बताया गया है, गड़बड़ी की जानकारी देने के दौरान, गड़बड़ी हो सकती है. ऐसा तब होता है, जब मशीन ने सभी इनपुट नहीं लिए होते हैं. इस सुविधा के लिए, tryProduceValue सेट की गई किसी भी गड़बड़ी को ठीक करता है. भले ही, मशीन का काम पूरा न हुआ हो.

उपसंहार: कॉल बैक की सुविधा को आखिर में हटाना

StateMachines, एसिंक्रोनस कंप्यूटेशन करने का एक बहुत ही असरदार तरीका है. हालांकि, इसमें बॉयलरप्लेट कोड का इस्तेमाल ज़्यादा होता है. Bazel कोड के कुछ हिस्सों में, खास तौर पर Runnables passed to ListenableFuture के तौर पर, कॉन्टिन्यूएशन का इस्तेमाल किया जाता है. हालांकि, विश्लेषण करने वाली SkyFunctions में इनका इस्तेमाल नहीं किया जाता है. विश्लेषण ज़्यादातर सीपीयू पर निर्भर करता है और डिस्क I/O के लिए कोई असरदार एसिंक्रोनस एपीआई नहीं है. आखिरकार, कॉलबैक को ऑप्टिमाइज़ करना बेहतर होगा, क्योंकि इन्हें समझने में समय लगता है और ये कोड को पढ़ने में मुश्किल पैदा करते हैं.

Java वर्चुअल थ्रेड, सबसे बेहतर विकल्पों में से एक है. कॉलबैक लिखने के बजाय, हर चीज़ को सिंक्रोनस, ब्लॉकिंग कॉल से बदल दिया जाता है. ऐसा इसलिए हो सकता है, क्योंकि वर्चुअल थ्रेड रिसॉर्स को प्लैटफ़ॉर्म थ्रेड की तरह बांधना महंगा नहीं होता. हालांकि, वर्चुअल थ्रेड के साथ भी, थ्रेड बनाने और सिंक्रनाइज़ेशन प्रिमिटिव के साथ सामान्य सिंक्रोनस कार्रवाइयों को बदलना बहुत महंगा है. हमने StateMachines से Java वर्चुअल थ्रेड पर माइग्रेट किया. हालांकि, ये थ्रेड बहुत धीमी थीं. इस वजह से, एंड-टू-एंड विश्लेषण में लगने वाला समय करीब तीन गुना बढ़ गया. वर्चुअल थ्रेड की सुविधा अभी भी पूर्वावलोकन के तौर पर उपलब्ध है. इसलिए, ऐसा हो सकता है कि परफ़ॉर्मेंस बेहतर होने के बाद, इस माइग्रेशन को बाद की तारीख में किया जा सके.

अगर Loom कोरूटीन कभी उपलब्ध होते हैं, तो उनका इस्तेमाल किया जा सकता है. इसका फ़ायदा यह है कि कोऑपरेटिव मल्टीटास्किंग का इस्तेमाल करके, सिंक्रनाइज़ेशन ओवरहेड को कम किया जा सकता है.

अगर इन सभी तरीकों से भी समस्या हल नहीं होती है, तो लो-लेवल बाइटकोड को फिर से लिखने का तरीका भी आज़माया जा सकता है. बेहतर ऑप्टिमाइज़ेशन की मदद से, हाथ से लिखे गए कॉलबैक कोड के बराबर परफ़ॉर्मेंस हासिल की जा सकती है.

अपेंडिक्स

कॉलबैक हेल

कॉलबैक हेल, एसिंक्रोनस कोड में एक कुख्यात समस्या है. यह समस्या, कॉलबैक का इस्तेमाल करने वाले कोड में होती है. ऐसा इसलिए होता है, क्योंकि अगले चरण को पूरा करने के लिए, पिछले चरण को पूरा करना ज़रूरी होता है. अगर कई चरण हैं, तो यह नेस्टिंग बहुत ज़्यादा डीप हो सकती है. अगर इसे कंट्रोल फ़्लो के साथ जोड़ा जाता है, तो कोड को मैनेज करना मुश्किल हो जाता है.

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

नेस्ट किए गए लागू करने के तरीकों का एक फ़ायदा यह है कि आउटर स्टेप के स्टैक फ़्रेम को सुरक्षित रखा जा सकता है. Java में, कैप्चर किए गए लैम्डा वैरिएबल, फ़ाइनल होने चाहिए. इसलिए, ऐसे वैरिएबल का इस्तेमाल करना मुश्किल हो सकता है. डीप नेस्टिंग से बचने के लिए, लैम्ब्डा के बजाय कंटीन्यूएशन के तौर पर मेथड रेफ़रंस दिखाए जाते हैं. इन्हें इस तरह दिखाया जाता है.

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 इंजेक्शन पैटर्न का इस्तेमाल बहुत ज़्यादा किया जाता है, तो कॉलबैक हेल की समस्या भी हो सकती है. हालांकि, इंजेक्शन को क्रमवार चरणों के साथ मिलाकर इस समस्या से बचा जा सकता है.

उदाहरण: Chained SkyValue lookups

ऐसा अक्सर होता है कि ऐप्लिकेशन लॉजिक के लिए, SkyValue लुकअप की निर्भर चेन की ज़रूरत होती है. उदाहरण के लिए, अगर कोई दूसरा SkyKey, पहले 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;
}

हालांकि, कंटीन्यूएशन को मेथड रेफ़रंस के तौर पर तय किया जाता है. इसलिए, स्टेट ट्रांज़िशन के दौरान कोड, प्रोसीजरल दिखता है: step2 के बाद step1 आता है. ध्यान दें कि यहां value2 असाइन करने के लिए, लैम्डा का इस्तेमाल किया गया है. इससे कोड का क्रम, ऊपर से नीचे तक की गणना के क्रम से मैच हो जाता है.

अन्य सुझाव

रीडेबिलिटी: एक्ज़ीक्यूशन का क्रम

कोड को आसानी से पढ़ने के लिए, StateMachine.step को लागू करने के क्रम में रखें. साथ ही, कॉलबैक को लागू करने के क्रम में, कोड में पास किए जाने के तुरंत बाद रखें. कंट्रोल फ़्लो के ब्रांच होने पर, ऐसा हमेशा नहीं किया जा सकता. ऐसे मामलों में, अतिरिक्त टिप्पणियां मददगार हो सकती हैं.

उदाहरण: SkyValue लुकअप को चेन करना में, इसे हासिल करने के लिए इंटरमीडिएट तरीके का रेफ़रंस बनाया जाता है. इससे परफ़ॉर्मेंस में थोड़ी कमी आती है, लेकिन कोड को पढ़ना आसान हो जाता है. इस मामले में, यह फ़ायदेमंद हो सकता है.

जनरेशनल हाइपोथीसिस

मीडियम-लिव्ड Java ऑब्जेक्ट, Java के गार्बेज कलेक्टर की जनरेशनल हाइपोथेसिस को तोड़ते हैं. इस हाइपोथेसिस को ऐसे ऑब्जेक्ट को हैंडल करने के लिए डिज़ाइन किया गया है जो बहुत कम समय के लिए मौजूद रहते हैं या हमेशा के लिए मौजूद रहते हैं. परिभाषा के हिसाब से, SkyKeyComputeState में मौजूद ऑब्जेक्ट इस हाइपोथेसिस का उल्लंघन करते हैं. इस तरह के ऑब्जेक्ट में, अब भी चल रहे सभी StateMachine का बनाया गया ट्री होता है. इसकी जड़ Driver होती है. इनकी लाइफ़स्पैन कुछ समय के लिए होती है, क्योंकि ये एसिंक्रोनस कंप्यूटेशन के पूरा होने का इंतज़ार करते समय निलंबित हो जाते हैं.

JDK19 में यह समस्या कम है. हालांकि, StateMachines का इस्तेमाल करने पर, कभी-कभी GC में लगने वाले समय में बढ़ोतरी देखी जा सकती है. भले ही, जनरेट किए गए कचरे में काफ़ी कमी आई हो. StateMachines की लाइफ़स्पैन कम होती है. इसलिए, इन्हें पुरानी जनरेशन में प्रमोट किया जा सकता है. इससे यह जनरेशन ज़्यादा तेज़ी से भर जाती है. इसलिए, इसे खाली करने के लिए ज़्यादा महंगे मेजर या फ़ुल जीसी की ज़रूरत होती है.

शुरुआत में, StateMachine वैरिएबल का इस्तेमाल कम से कम करना चाहिए. हालांकि, ऐसा हमेशा नहीं किया जा सकता. उदाहरण के लिए, अगर किसी वैल्यू की ज़रूरत कई राज्यों में है. जहां मुमकिन होता है वहां लोकल स्टैक step वैरिएबल, नई जनरेशन के वैरिएबल होते हैं. साथ ही, इन्हें बेहतर तरीके से GC किया जाता है.

StateMachine वैरिएबल के लिए, टास्क को सबटास्क में बांटना और StateMachine के बीच वैल्यू ट्रांसफ़र करने के लिए सुझाए गए पैटर्न का पालन करना भी मददगार होता है. ध्यान दें कि इस पैटर्न को फ़ॉलो करते समय, सिर्फ़ चाइल्ड StateMachine के पास पैरंट StateMachine के रेफ़रंस होते हैं. इसके उलट नहीं. इसका मतलब है कि जैसे-जैसे बच्चे, माता-पिता के लिए उपलब्ध कराए गए टास्क पूरे करते हैं और नतीजों के बारे में उन्हें सूचना देते हैं वैसे-वैसे वे टास्क की सूची से हटते जाते हैं. इसके बाद, वे टास्क, माता-पिता के लिए उपलब्ध हो जाते हैं.

आखिर में, कुछ मामलों में पहले की स्थितियों में StateMachine वैरिएबल की ज़रूरत होती है, लेकिन बाद की स्थितियों में नहीं. जब यह पता चल जाए कि बड़े ऑब्जेक्ट की अब ज़रूरत नहीं है, तो उनके रेफ़रंस को शून्य कर देना फ़ायदेमंद हो सकता है.

स्थितियों के नाम रखना

किसी तरीके का नाम रखते समय, आम तौर पर उस तरीके के अंदर होने वाले व्यवहार के हिसाब से उसका नाम रखा जा सकता है. StateMachine में ऐसा कैसे किया जाए, यह कम साफ़ तौर पर बताया गया है, क्योंकि इसमें कोई स्टैक नहीं है. उदाहरण के लिए, मान लें कि foo तरीके से bar सब-तरीके को कॉल किया जाता है. StateMachine में, इसे foo के बाद bar के तौर पर ट्रांसलेट किया जा सकता है. foo में अब bar शामिल नहीं है. इस वजह से, राज्यों के लिए तरीके के नाम, स्कोप में छोटे होते हैं. इससे स्थानीय व्यवहार का पता चल सकता है.

कॉनकरेंसी ट्री डायग्राम

यहां स्ट्रक्चर्ड कंकरेंसी में दिए गए डायग्राम का एक दूसरा व्यू दिया गया है. इसमें ट्री स्ट्रक्चर को बेहतर तरीके से दिखाया गया है. ब्लॉक से एक छोटा पेड़ बनता है.

स्ट्रक्चर्ड कॉनकरेंसी 3D


  1. इसके उलट, Skyframe में वैल्यू उपलब्ध न होने पर, शुरू से ही प्रोसेस शुरू हो जाती है. 

  2. ध्यान दें कि step को InterruptedException थ्रो करने की अनुमति है, लेकिन उदाहरणों में इसे शामिल नहीं किया गया है. Bazel कोड में कुछ ऐसे तरीके हैं जिनसे यह अपवाद दिखता है. यह Driver तक पहुंचता है. इसके बारे में बाद में बताया जाएगा. यह StateMachine को चलाता है. ज़रूरत न होने पर, इसे थ्रो किए जाने का एलान न करना ठीक है. 

  3. एक साथ कई उपटास्क चलाने की सुविधा, ConfiguredTargetFunction की वजह से मिली है. यह हर डिपेंडेंसी के लिए अलग से काम करता है. जटिल डेटा स्ट्रक्चर में बदलाव करने के बजाय, हर डिपेंडेंसी का अपना इंडिपेंडेंट StateMachine होता है. जटिल डेटा स्ट्रक्चर, सभी डिपेंडेंसी को एक साथ प्रोसेस करते हैं, जिससे काम करने में समस्याएं आती हैं. 

  4. एक ही चरण में कई tasks.lookUp कॉल को एक साथ बैच किया जाता है. एक साथ चलने वाले उपटास्क में होने वाले लुकअप से, अतिरिक्त बैचिंग बनाई जा सकती है. 

  5. यह कॉन्सेप्ट, Java के स्ट्रक्चर्ड कॉंकुरेंसी jeps/428 से मिलता-जुलता है. 

  6. ऐसा करना, किसी थ्रेड को स्पॉन करने और उसे क्रमवार कंपोज़िशन बनाने के लिए शामिल करने जैसा है.