PregelLoop: Superstep-Schleifentreiber
Verantwortung
PregelLoop ist der «Taktgeber» der LangGraph-Laufzeit. Pregel selbst ist nur für das Zusammenbauen der Ressourcen und das Bereitstellen der Eingänge invoke / astream zuständig; was den Graphen Runde für Runde tatsächlich treibt, ist eine PregelLoop-Instanz: Sie hält den aktuellen Checkpoint (checkpoint), die Kanal-Menge (channel), pending_writes, den step-Zähler, die status-Zustandsmaschine sowie ein tasks-Dictionary — all das ist der gesamte Kontext innerhalb eines einzelnen Superstep (superstep). Darauf baut die äußere Form while loop.tick(): ... loop.after_tick() auf.
Ihre beiden Kernmethoden sind tick(tick:599) und after_tick(after_tick:683). Ersteres übernimmt das «vor dem Lauf»: prüfen, ob die Schrittzahlgrenze überschritten ist, prepare_next_tasks aufrufen, um die in diesem Schritt auszuführenden Knoten in self.tasks zu laden, Interrupts prüfen, drain_requested behandeln und die von einer vorherigen Wiederaufnahme übrig gebliebenen pending_writes wieder an erfolgreiche Knoten zurückheften. Letzteres übernimmt das «nach dem Lauf»: alle task.writes einsammeln, apply_writes aufrufen, um sie in die Kanäle zu übernehmen, Pending leeren, Checkpoint schreiben und schließlich entscheiden, ob ein interrupt_after nötig ist. Die beiden flankieren den BSP-Zyklus «Eingabe lesen → dispatchen → ausführen → zurückschreiben → persistieren» vorne und hinten.
Anders gesagt: PregelLoop führt Knoten nicht selbst aus — das ist die Aufgabe von PregelRunner; es entscheidet nur, «wer in diesem Schritt drankommt, wie nach dem Lauf aufgeräumt wird und ob es einen weiteren Schritt braucht».
Entwurfsmotivation
Warum die Schleifenlogik in tick und after_tick aufteilen statt mit einer einzigen step()-Methode zu erledigen?
- Ausführung an den Aufrufer übergeben: Nachdem
tickTruezurückgegeben hat, treibt der Aufrufer (Implementierungskörper vonPregel.astream, siehesync main loop:2964-2984) selbstrunner.tick, um diese Task-Charge abzuarbeiten, und ruft danachloop.after_tickauf. So mussPregelLoopkeine Referenz aufPregelRunnerhalten; beide Objekte arbeiten lose gekoppelt zusammen, statt dass eines im anderen schachtelt. - Explizite Zustandsmaschine: Das
status-Feld hat sieben Ausprägungen:"input" / "pending" / "done" / "draining" / "interrupt_before" / "interrupt_after" / "out_of_steps"(status Literal:256-264). Jeder Zustand entspricht einem Austrittsgrund; externer Code kann daran entscheiden, ob resume, raise oder break nötig ist, ohne die Rückgabewert-Semantik raten zu müssen. - Klare Wiederaufnahme-Semantik:
is_replayingist nur beim ersten tick wahr — es bedeutet, dass dieser Schritt das «Replay» der Tasks ist, die vor dem letzten Interrupt bereits fertig waren; sobaldafter_tickdurchlaufen ist, wird es ausgeschaltet(is_replaying = False:716). Zusammen mit_reapply_writes_to_succeeded_nodes, das die im Checkpoint verbliebenen erfolgreichen Writes an In-Memory-Tasks zurückheftet, werden erfolgreiche Knoten nicht erneut ausgeführt, während fehlgeschlagene in den Error-Handler gehen. - Drain als Eingriff von außen:
RunControl.drain_requestedwird vom Stream-Konsumenten gesetzt (z. B. wenn der Client mittendrin anhalten möchte);ticktritt bei Erkennung in dendraining-Zustand ein und gibtFalsezurück, sodass die äußerewhile-Schleife natürlich endet, statt Knoten mittendrin hart abzuwürgen. - Exit-mode delta writes und Checkpoint-Zeitplanung:
_delta_write_futs(_delta_write_futs:207) sammelt dieput_writes-Futures aller Delta-Kanäle;_checkpointer_put_after_previousmuss sie erst drainen, bevor der nächste Checkpoint geschrieben wird — das garantiert die kausale Reihenfolge «Writes erzeugen den Checkpoint, nicht der Checkpoint überdeckt die Writes».
Schlüsseldateien
class PregelLoop:158— Klassendefinition; Felder sind fast alle nackte Attribute ohne Property-Verpackung, damit die UnterklassenSyncPregelLoop/AsyncPregelLoopdirekt überschreiben können.status Literal:256-264— Sieben-Zustände-Enumeration;"out_of_steps"wird bei Überschreitung der Schrittzahlgrenze ausgelöst,"draining"bei externem Stoppwunsch,"done"bei normalem Ende.tick method:599-681— Die gesamte Logik eines vollständigen tick: Schrittzahlprüfung,prepare_next_tasks, Done-Entscheidung bei leeren Tasks, Drain,_reapply_writes_to_succeeded_nodes,should_interrupt.out_of_steps:607-609— Tritt in denout_of_steps-Zustand ein und gibtFalsezurück, wennself.step > self.stop; die äußerewhile-Schleife beendet sich.done branch:653-655— Setzt dendone-Zustand, wennself.tasksleer ist — das ist der «natürliche Tod» des Graphen.draining branch:657-659— Setzt dendraining-Zustand, wenncontrol.drain_requestedwahr ist.reapply + resume handlers:662-664— Heftet nach der Wiederaufnahme zuerst die pending writes an erfolgreiche Knoten zurück und ruft dann_resume_error_handlers_if_applicableauf, um für fehlgeschlagene Knoten Handler-Tasks vorzubereiten.interrupt_before:667-671— Ruftshould_interruptauf, um zu prüfen, ob Knoten in derinterrupt_before-Liste liegen; bei Treffer wirdGraphInterruptgeworfen.after_tick method:683-726— Writes einsammeln →apply_writes→ values emitten → Pending leeren →_put_checkpoint→interrupt_after-Prüfung._reapply_writes_to_succeeded_nodes:736-749— Heftet pending writes an In-Memory-Tasks zurück, überspringt aber die vier SteuersignaleERROR / ERROR_SOURCE_NODE / INTERRUPT / RESUME._resume_error_handlers_if_applicable:751-816— Scannt dieERROR_SOURCE_NODE-Markierungen, konstruiert für fehlgeschlagene Knoten Error-Handler-Tasks und lässt den Runner die ursprünglichen Tasks überspringen und direkt die Handler ausführen.put_writes:415-508— Eingang für die von Tasks produzierten Writes; dedupliziert Steuerkanäle, akkumuliert Null-Tasks und startet dieput_writes-Future des Checkpointers.SyncPregelLoop:1469/AsyncPregelLoop:1722— Die beiden Unterklassen; überschreiben die synchron/asynchron unterschiedlichen Teile wie__enter__/__exit__,accept_push,schedule_error_handler.
Datenfluss
Der Eingang von tick prüft zuerst die Schrittzahlgrenze und ruft dann prepare_next_tasks auf, um zu berechnen, «welche Tasks im nächsten Schritt laufen». Dieser Schritt ist der Samen des gesamten Supersteps — alle nachfolgenden Aktionen verarbeiten diese Task-Charge.
def tick(self) -> bool:
"""Execute a single iteration of the Pregel loop.
Returns:
True if more iterations are needed.
"""
# check if iteration limit is reached
if self.step > self.stop:
self.status = "out_of_steps"
return False
# prepare next tasks
self.tasks = prepare_next_tasks(
self.checkpoint,
self.checkpoint_pending_writes,
self.nodes,
self.channels,
self.managed,
self.config,
self.step,
self.stop,
for_execution=True,
manager=self.manager,
store=self.store,
checkpointer=self.checkpointer,
trigger_to_nodes=self.trigger_to_nodes,
updated_channels=self.updated_channels,
retry_policy=self.retry_policy,
cache_policy=self.cache_policy,
)Das stammt aus tick 头部:599-629. self.step > self.stop ist die Austrittsbedingung «Schrittzahlgrenze überschritten» — stop wird in PregelLoop.__init__ auf recursion_limit gesetzt; wenn die Rekursionstiefe überschritten ist, endet man bei out_of_steps. prepare_next_tasks nimmt den aktuellen Checkpoint, pending_writes, die Knoten- und Kanaltabellen und berechnet, welche Tasks in diesem Schritt auszuführen sind; sie werden in self.tasks gesteckt. for_execution=True bedeutet, dass diese Tasks wirklich laufen sollen (nicht ein Dry-Run für Streaming-Vorschau).
Es folgen drei parallele Austrittsbedingungen — leere Tasks / Drain / Wiederaufnahme mit zurückgehefteten Writes — und danach should_interrupt:
# if no more tasks, we're done
if not self.tasks:
self.status = "done"
return False
if self.control is not None and self.control.drain_requested:
self.status = "draining"
return False
# if there are pending writes from a previous loop, apply them
if not self.is_replaying and self.checkpoint_pending_writes:
self._reapply_writes_to_succeeded_nodes(self.tasks)
self._resume_error_handlers_if_applicable()
# before execution, check if we should interrupt
if self.interrupt_before and should_interrupt(
self.checkpoint, self.interrupt_before, self.tasks.values()
):
self.status = "interrupt_before"
raise GraphInterrupt()Das stammt aus tick 中段:652-671. Die interrupt_before-Prüfung erfolgt erst nach der Wiederaufnahme mit pending_writes — denn bei der Wiederaufnahme spiegeln die Kanalversionsnummern bereits die letzten Schreibvorgänge, sodass should_interrupt korrekt beurteilen kann, «ob es seit dem letzten Interrupt neue Updates gab».
after_tick wird aufgerufen, nachdem der Runner alle Tasks abgearbeitet hat, und übernimmt den Abschluss:
def after_tick(self) -> None:
# finish superstep
writes = [w for t in self.tasks.values() for w in t.writes]
self._delta_channels_with_overwrite.update(
ch
for ch, v in writes
if isinstance(self.specs.get(ch), DeltaChannel) and _get_overwrite(v)[0]
)
# all tasks have finished
self.updated_channels = apply_writes(
self.checkpoint,
self.channels,
self.tasks.values(),
self.checkpointer_get_next_version,
self.trigger_to_nodes,
)Das stammt aus after_tick 头部:683-698. Zuerst werden alle task.writes eingeebnet und nebenbei erfasst, welche Delta-Kanäle ein Overwrite erlitten haben (relevant für den Startwert beim Sparse Replay); dann ruft apply_writes diese Writes tatsächlich in die Kanäle ein. Der Rückgabewert updated_channels wird im nächsten Schritt von prepare_next_tasks genutzt, um das «Finden des als nächstes auszulösenden Knotens» zu beschleunigen. Am Ende von after_tick wird zudem checkpoint_pending_writes geleert, is_replaying auf False gesetzt, der Checkpoint über _put_checkpoint({"source": "loop"}) geschrieben und schließlich die interrupt_after-Prüfung ausgeführt(after_tick 尾部:714-724).
Der Lebenszyklus eines ganzen Supersteps lässt sich so darstellen:
Grenzen und Fehler
out_of_stepsist kein Fehler:step > stopgibt direktFalsezurück;statuswird aufout_of_stepsgesetzt, die äußerewhile-Schleife endet natürlich.Pregelentscheidet anhand von status, ob eineRecursionErrorgeworfen oder still abgeschlossen wird — das ist eine «Soft-Limit»-Semantik, kein Ausnahmepfad(out_of_steps:607-609).drainingdarf keine Lifecycle-Events emitten:_emit_graph_lifecycle_eventlehnt Aufrufe imdraining-Zustand explizit ab(draining guard:384-385), da Drain ein aktiver Benutzerstopp ist und nicht als interrupt-/resume-Semantik zählt.- Bei Wiederaufnahme müssen Steuersignale in pending writes übersprungen werden:
_reapply_writes_to_succeeded_nodesmussERROR / ERROR_SOURCE_NODE / INTERRUPT / RESUMEüberspringen(skip control signals:746-747), da sonst fehlgeschlagene Tasks als erfolgreich behandelt und bereits gelaufene Handler erneut getriggert würden. - Error-Handler bei der Wiederaufnahme neu schedulen:
_resume_error_handlers_if_applicablewirkt nur auf Knoten miterror_handler_node(handler_node check:791-793); fehlgeschlagene Knoten ohne Handler bleiben unangetastet und gehen über_should_stop_othersdes Runners den Panic-Pfad. put_writes-Akkumulationssemantik für Null-Tasks: Writes mitNULL_TASK_IDwerden nicht überschrieben, sondern akkumuliert(null task accumulate:422-431), verwendet für Input-Writes, die «zu keinem Knoten gehören, aber in den Checkpoint müssen».- Futures der Delta-Kanäle müssen vor dem nächsten Checkpoint flüssig gemacht werden:
_delta_write_futssammelt dieput_writes-Futures aller Delta-Kanäle; vor dem Schreiben des nächsten Checkpoints werden sie zuerst drainet(_delta_write_futs:201-207) — umgekehrte Reihenfolge würde beim Sparse Replay die Writes verbergen, die den Checkpoint erzeugt haben.
Zusammenfassung
PregelLoop ist ein zustandsmaschinengetriebener Superstep-Taktgeber, der die drei Phasen «Tasks vorbereiten → laufen lassen → aufräumen» um PregelRunner herum aufspannt, ohne selbst die Ausführung anzufassen. Wer tick / after_tick, die sieben status-Zustände sowie den Wiederaufnahmemechanismus aus is_replaying + _reapply_writes_to_succeeded_nodes verstanden hat, hat die Hauptachse des LangGraph-Laufzeitmodells erfasst. Wie der Runner die Tasks tatsächlich zum Laufen bringt, steht in /pregel/runner; die internen Details der Algorithmusfunktionen prepare_next_tasks / apply_writes / should_interrupt in /pregel/algo; wie das Ganze zusammengesetzt wird, in /pregel/pregel. Die Schreibzurück-Semantik der Kanäle steht in /channel/base-channel.
Siehe offizielle Dokumentation: LangGraph-Dokumentation · README.