Die Engine, die täglich Millionen von Okta Workflows -Ausführungen antreibt, war ein Early Adopter des asynchronen Rust -Ökosystems. Während Tokio heute die natürliche Wahl für eine asynchrone Laufzeitumgebung ist, haben wir Workflows gestartet , bevor die Version Tokio 1.0veröffentlicht wurde und lange bevor sich die Best Practices und Muster für asynchrones Rust wirklich gefestigt hatten.
In diesem Artikel wird beschrieben, wie wir die Workflows Engine, eine Codebasis von über 100.000 Quellcodezeilen, von Continuations-basiertem Tokio 0.1 auf async / await Tokio 1.0 migriert haben, ohne Serviceunterbrechungen oder Pausierung der Feature-Entwicklung.
Continuations-basierte Futures
In den Anfängen wurde die asynchrone Ausführung von Rust-Futures durch die Übergabe von Funktionen erreicht, die endgültige Werte darstellen, die als Kontinuationen bekannt sind. Diese Kontinuationen wurden miteinander verkettet und jeweils asynchron von einer Laufzeitumgebung wie Tokio ausgeführt und führten schließlich zu dem gewünschten Wert. Vor der Migration hatten wir Hunderttausende von Codezeilen, die ungefähr so aussahen:

Dies war der Stand von Async-Rust, bis die async / await -Syntax stabilisiert wurde. Für Workflows bedeutete die frühe Einführung, dass wir weit vor der Stabilisierung von async / await ausliefern mussten, und obwohl diese Art von Code schwer zu lesen und zu warten ist, haben wir ihn so weit wie möglich ausgereizt.
Es hat uns gute Dienste geleistet und es uns ermöglicht, Okta Workflows zu dem zu entwickeln, was es heute ist. Schnellvorlauf bis vor etwa einem Jahr, als die Risse unerträglich wurden und es notwendig wurde, endlich mit der Migration zur modernen async / await Syntax und Tokio 1.0 zu beginnen.
Async / Await-basierte Futures
Mit Tokio 1.0 konnte der asynchrone Kontrollfluss mit der (jetzt seit langem stabilen) async / await Syntax geschrieben werden, was den Code erheblich vereinfacht und Boilerplate-Code reduziert. Das obige Beispiel könnte so geschrieben werden.

Selbst wenn Sie nicht viel mit Rust vertraut sind, sind die Vorteile der Lesbarkeit und Einfachheit der Verwendung von async / await klar. Für diejenigen, die mit Rust vertraut sind, ermöglicht die Fähigkeit von async / await, über await-Punkte hinweg zu borgen, dass wir nicht mehr das Eigentum des Flow -Objekts übergeben müssen, sodass wir es nicht mehr im FlowError speichern mussten. Dies war entscheidend, um Fehler an unsere Kunden weiterzuleiten, und eine Quelle für viele Fehler, wenn ein Codepfad zwangsläufig vergisst, den Flow in einem Fehler zu speichern. Dies wäre nur eine Klasse von Fehlern, die durch die Migration zu async / await behoben werden könnte.
Der große Aufwand
Obwohl die Vorteile der Migration zu Tokio 1.0 klar waren, wäre dies eine gewaltige Aufgabe. Die Migration ist nicht trivial und würde eine vollständige Neufassung der Workflows Engine erfordern. Eine komplette Neufassung, die die Welt anhält, ist sowohl aus geschäftlicher Sicht als auch im Hinblick auf die Kundenverlässlichkeit unpraktisch und riskant. Wir konnten die Produktentwicklung nicht unterbrechen, um eine vollständige Neufassung durchzuführen; dies wäre in der Tat ein fataler Fehler für jedes Softwareprodukt gewesen.
Erschwerend kam hinzu, dass wir um die Zeit unserer Untersuchung einer Neufassung herum neue Funktionen in der Pipeline hatten. Zum Beispiel würden wir nur wenige Monate später, zeitgleich mit dieser Migration auf Tokio 1.0, die Möglichkeit, einen Workflow abzubrechen ausliefern. Eine Neufassung dieser Größenordnung müsste Stück für Stück erfolgen, so als würde man die Räder eines fahrenden Zuges austauschen.
Kompatibilitätsschichten und Feature Flags
Okta Workflows verwendet die Hyper HTTP-Bibliothek, um mit dem Web zu kommunizieren und externe Dienste wie Google Drive oder Amazon S3 aufzurufen – all das macht Workflows so leistungsstark. Diese grundlegende Bibliothek war eine der ersten, die ersetzt wurde: Wir haben den mit Tokio 0.1 betriebenen Hyper 0.12 gegen Hyper 0.14 ausgetauscht, der auf Tokio 1.0 und async / await basierte. Dies war einer der ersten Teile der Workflows Engine, der von Tokio 0.1 auf Tokio 1.0 portiert wurde, und die Erfahrung trug dazu bei, viele unserer Ansätze und architektonischen Entscheidungen während des Migrationsprozesses zu beeinflussen.
Die Futures-Bibliothek, die den größten Teil des Async-Ökosystems von Rust untermauert, verfügt über eine Reihe von Kompatibilitäts-Shims, die bei der Migration von Continuations-basierten Futures zu async / await -basierten Futures helfen. Diese Shims können jedoch Laufzeitunterschiede nicht berücksichtigen. Unser Continuations-basierter Legacy-Code konnte nur auf der Tokio 0.1-Runtime ausgeführt werden, aber dieser neue HTTP-Verarbeitungscode müsste auf der Tokio 1.0-Runtime ausgeführt werden, und niemals treffen sich die beiden: Tokio 1.0 hängt für immer, wenn ihm ein Kompatibilitäts-Future übergeben wird, das für die Ausführung auf einem Tokio 0.1-Executor bestimmt ist.
Daher war ein zusätzlicher Indirektionsschritt erforderlich. In den frühen Tagen der „Asyncification“, als eine HTTP-Konnektor-Karte in Workflows verwendet wurde, erfolgte die Flow-Verarbeitung zunächst in einem Tokio 0.1-Kontext. Beim Absetzen der HTTP-Anfrage rief Tokio 0.1 einen Shim auf, der die Anfrage in einer Tokio 1.0-Runtime auslöste, die in einem separaten Thread lief, und wartete darauf, dass die Anfrage über einen Kanal zurückgegeben wurde. Auf diese Weise konnten wir vermeiden, dass Futures für eine Runtime mit einer anderen vermischt werden. Wir sollten diesen Ansatz später bis an seine Grenzen bringen, wobei jeder Flow-Funktionsaufruf sowie jeder Aufruf von Redis ähnliche Shims durchlief, als wir Tokio 0.1-Implementierungen und -Bibliotheken schrittweise durch solche ersetzten, die mit Tokio 1.0 kompatibel sind.

Kompatibilität-Shims waren unidirektional; das Aufrufen von async / await Code von Legacy-Futures-Code erforderte einen anderen Shim als das Aufrufen von Legacy-Futures-Code von async / await Code.
Um sicherzustellen, dass Kunden keinen Rückgang der Zuverlässigkeit oder eine Verhaltensänderung bemerken, haben wir Feature Flags genutzt, um zur Laufzeit zwischen Legacy- und Tokio 1.0-Implementierungen von Codepfaden zu wechseln. Wenn ein Problem auftrat, konnten wir schnell zur vorherigen Implementierung wechseln, während wir das Problem mit dem neuen Code behoben haben. Kompatibilität-Shims boten eine nützliche Brandschneise zwischen den beiden Welten, bis wir bereit waren, sie in async / await Syntax neu zu schreiben. Indem wir sowohl Legacy- als auch Tokio 1.0-Pfade parallel betrieben und dann den Legacy-Codepfad verwarfen, konnten wir das Fundament, auf dem die Workflows Engine aufgebaut war, schrittweise ersetzen, bis nichts mehr von Tokio 0.1 übrig war.
Herausforderungen
Während des Umstellungsprozesses auf Tokio 1.0 haben wir schließlich ein kleines Toolkit entwickelt, mit dem wir schnell Kompatibilitäts-Shims zwischen der Tokio 0.1-Welt und Tokio 1.0 schreiben konnten. Dies sollte sich als ein äußerst wichtiges Werkzeug erweisen, als wir trotz der Neuentwicklung weiterhin neue Funktionen auslieferten.
Während Kernelemente der Workflows Engine diese Neufassung durchliefen, hätten Funktionen, die in der Zwischenzeit ausgeliefert wurden, andernfalls zweimal neu geschrieben werden müssen – einmal in Tokio 0.1 und erneut in Tokio 1.0. Ein Toolkit zum Erstellen von Kompatibilität-Shims ermöglichte es uns, den gesamten neuen Code in Tokio 1.0 zu schreiben und ihn mit unserer bestehenden Tokio 0.1-Codebasis zu verbinden. Manchmal reichten die Shims nicht aus; zum Beispiel im Fall des oben erwähnten Abbruch-Flows mussten Teile der Implementierung zweimal geschrieben werden, um Codepfade in beiden Runtimes zu unterstützen.
Es gab einige Kernunterschiede zwischen den Legacy Tokio 0.1 und Tokio 1.0 Runtimes, die wir behandeln mussten. Mit Tokio 0.1 wurde jede Future im Heap alloziert, aber mit async / await würden Futures nun standardmäßig auf dem Stack alloziert. Wenn wir nicht vorsichtig wären, würden wir den Stack in einem Worker-Thread schnell überlaufen lassen. Durch Erhöhen der Standard-Stackgröße des Worker-Threads und sorgfältige Auswahl der Futures, die im Heap alloziert werden sollen, konnten wir dieses Problem abschwächen.
Mit dem Legacy Tokio 0.1-Codepfad arbeiteten mehrere Executors parallel, um einen hochgradig gleichzeitigen Output zu erzielen. In der ersten Implementierung mit Tokio 1.0 verwendeten wir nur einen einzigen Executor, um alle Flow-Ausführungen zu verarbeiten. Es stellte sich heraus, dass dies etwa 15 % langsamer war als die Verwendung mehrerer Executors, was an sich schon beeindruckend ist! Obwohl wir schließlich wieder auf mehrere Tokio 1.0-Runtimes umgestiegen sind, um das gleiche Maß an Durchsatz zu ermöglichen, das unsere Kunden erwarten, haben wir jetzt ein paar neue Stellschrauben, an denen wir in Zukunft drehen können, um das zu übertreffen, was mit Tokio 0.1 und dem Legacy-Codepfad möglich war.
Eine schnellere, sauberere Codebasis
Heute sind wir bei Workflows weitgehend nicht mehr auf das Legacy-System Tokio 0.1 angewiesen. Unsere Codebasis ist schneller, leichter zu lesen und besser wartbar als je zuvor, was uns mehr Möglichkeiten bietet, die Ausführungsleistung von Flows zu optimieren und unseren Kunden schneller als je zuvor neue Funktionen bereitzustellen.

Der Übergang zu async / await ermöglichte es uns, über 20.000 Codezeilen aus der Workflows Engine zu entfernen.
Wir haben die lange vernachlässigte Welt von Tokio 0.1 endlich verlassen und können unsere grundlegenden Abhängigkeiten nun kontinuierlich auf dem neuesten Stand halten, was entscheidend für unsere Bemühungen ist, Okta zum sichersten Unternehmen der Welt zu machen. Und natürlich ist es für unser gesamtes Team viel angenehmer, jeden Tag an einer sauberen, lesbaren Codebasis zu arbeiten als an einer mit Tausenden von Zeilen Boilerplate-Code.
Erfahren Sie mehr darüber, wie Sie kritische IT- und Sicherheitsaufgaben in großem Umfang mit Okta Workflows automatisieren können.
Haben Sie Fragen zu diesem Blogbeitrag? Kontaktieren Sie uns unter eng_blogs@okta.com.
Entdecken Sie weitere aufschlussreiche Engineering Blogs von Okta, um Ihr Wissen zu erweitern.
Möchten Sie unserem leidenschaftlichen Team außergewöhnlicher Ingenieure beitreten? Besuchen Sie unsere Karriere -Seite.
Nutzen Sie das Potenzial eines modernen und ausgeklügelten Identitätsmanagements für Ihr Unternehmen.
Wenden Sie sich an den Vertrieb, um weitere Informationen zu erhalten.