KERIpy Async Architecture - Kent Bull

KERICONF26 Day 2 · 43:30

0:00 Kent Bull | KERIpy Async Architecture | KERI Conference 2026

0:03 This is a developer focus

0:07 description for the async, the runtime

0:10 foundation of KERIpy.

0:12 And what this is that's essentially

0:13 what's the process, what's the execution

0:15 model, how does this run?

0:17 And

0:19 if I'm going to read the source code,

0:20 what do I need to know

0:23 in order for me to effectively

0:24 contribute

0:26 to the KERIpy code base.

0:28 And to learn it or to extend it.

0:30 And hopefully by the end of this

0:32 presentation you will have enough of a

0:34 mental map and a mental model to know

0:36 how to effectively talk to Claude

0:38 or Codex or another AI in case you need

0:41 to go and relearn this later to

0:43 concretize your own learning, to anchor

0:44 it in

0:46 or to potentially extend it.

0:49 We're going

0:51 to go over a lot: What structured

0:53 concurrency is, why it matters, how it's

0:55 superior

0:57 to promises async/await

0:59 callbacks and goto.

1:01 And then some common gotchas for when

1:04 you're trying to use KERIpy and why

1:06 things are done the way they are.

1:08 All right, so

1:10 hello is it's Evan, right? No, Keanu!

1:14 You probably know a

1:15 fair bit of this already.

1:19 I know I've talked to you

1:20 about this before, Joe. I was just

1:22 probably talking to you when we say

1:23 stuff about structured concurrency.

1:26 How many would say they're familiar

1:27 enough with structured

1:28 concurrency to be able to explain it?

1:33 Keanu?

1:34 You work at the KERI Foundation.

1:40 – I think, HIO,

1:41 the only time I've really dove into the

1:45 code, was when we had to port

1:48 into the web

1:51 base there. Because we had to use

1:53 HIO because it was async. It was

1:56 asynchronous so we had to adapt it

1:58 because in the web it was sync, so

2:01 we had to convert it and that's what

2:03 we did in the tutorial,

2:05 but I don't want to

2:07 say something that is completely

2:08 wrong, so ..

2:10 Okay, I see.

2:12 This will be valuable for you

2:14 then because maybe you haven't had as

2:16 much experience with the HIO async

2:17 runtime.

2:19 Great cuz if I even have one

2:21 more programmer

2:23 that really deeply understands HIO

2:25 after this, that will be good because I

2:27 got to get this knowledge out of my

2:28 brain.

2:30 Who's going to ask a question? So I

2:31 think structured concurrency is just

2:33 multi-threading and then how you

2:35 actually come to a

2:37 finality?

2:38 It's a good question. You're going in

2:40 the right direction. It's not

2:41 multi-threading. Okay. Are you familiar

2:43 with Go co-routines?

2:45 – Not in Python. So, it's a lightweight

2:48 co-routine construct from Go lang. I

2:51 just said the Go co-routines.

2:53 One of the reasons why Go became

2:55 so popular is people like the simplicity

2:57 of the mental model with Go co-routines.

2:59 Python co-routines are actually better

3:02 than go co-routines in my opinion, the

3:04 way Sam uses them in HIO.

3:06 If you want to think about what the

3:09 unit the base fundamental unit of

3:11 concurrency in a structured concurrency

3:13 framework is, it's a co-routine

3:17 that is cooperative with the other

3:19 co-routines. Usually a thread ..,

3:21 it's like

3:23 a fundamental

3:25 construct where it doesn't matter if you

3:27 block within one thread and the reason

3:29 why you use threads is because you want

3:31 to have a main thread that's not blocked

3:34 by work that will block that you put to

3:36 another thread.

3:39 If you want to adopt the mindset of

3:42 structured concurrency, you need to

3:44 think from a non-blocking mindset. Often

3:46 it's called a reactive mindset. So zero

3:49 of the co-routines should block.

3:53 And you might wonder, how is that

3:54 possible?

3:55 The way this is implemented at the IO

3:58 level with network IO and file IO

4:01 is in a fundamentally non-blocking way.

4:04 And how do you do that? You

4:05 essentially chop up your IO reads and

4:08 writes into chunks and then you… So,

4:10 it'll be blocking ever so slightly to

4:14 process one chunk and then it'll move

4:15 on.

4:16 But it's fast enough, right?

4:19 Exactly. It doesn't like it's like it

4:20 doesn't really block.

4:22 And not only that, but if you look

4:23 at the low-level socket implementation

4:25 in HIO,

4:27 it's it waits for a chunk and if

4:29 there's an error, it just waits and then

4:31 schedules itself for the next cycle of

4:32 the root scheduler to schedule that

4:34 co-routine again.

4:36 Okay. So,

4:38 we're still at the first two

4:39 slides two slides, so you're good.

4:42 So, we're talking about what structured

4:43 concurrency is. The fundamental unit of

4:45 structured concurrency, which we will

4:46 beat to death

4:48 in the rest of this as a Doer, it's a

4:50 co-routine.

4:52 A non-blocking co-routine. And if you

4:54 look at the readme of HIO that we're

4:56 diving into, it says,

4:58 a waitless hierarchical structured

5:01 cooperative concurrency

5:03 runtime.

5:05 Now, you might say, let's dive deep into

5:06 structured concurrency. Well, let's go

5:08 all the way back to 1968.

5:11 You might have heard of Edgar Dijkstra.

5:14 He's very popular in the computer

5:16 science space. There's a Dijkstra

5:18 pathfinding algorithm and lots of

5:19 other things.

5:20 He wrote an article back in 1968 that

5:23 says, “goto statement considered

5:24 harmful”.

5:26 And it's actually not that long. This is

5:27 the whole article, this page and this

5:29 page.

5:30 And he says a key part statement, the

5:33 unbridled use

5:35 of the goto statement

5:37 has an immediate consequence that it

5:40 becomes terribly hard

5:42 to find a meaningful set of coordinates

5:45 in which to describe the process

5:47 progress. He's trying to say this is

5:49 very difficult to understand from a

5:50 procedural standpoint, where I'm at in my

5:52 procedure if I've got goto statements

5:54 that could take me to arbitrary places.

5:57 And we'll give you a bit of a…

6:00 I'm going to give you a visual here. So,

6:01 this is flowmatic. It's

6:03 like Basic. It's like some of the early

6:05 programming languages and

6:06 this is a jump, but a jump is a goto.

6:09 And it's saying, "Okay, when you get

6:10 down here, if this set of conditions

6:11 happens, go to operation two."

6:14 Well, that seems like it might be okay

6:17 till you get a lot of goto statements.

6:19 And now how do you

6:21 understand what the

6:23 boundaries are here?

6:24 Have you ever heard of solid programming

6:26 principles? Highly cohesive, loosely

6:28 coupled? Where's the loose coupling?

6:30 That looks like tight coupling spaghetti

6:31 code.

6:33 Right? Well, that's where the name

6:34 spaghetti code came from. Right. We

6:36 don't like this. This is bad. This is

6:38 hard on humans. It's hard on LLMs.

6:40 Nobody likes it.

6:42 Now, the reason if we go back to couple

6:44 slides,

6:45 so goto, the go statement in Go for go

6:49 promises, callbacks,

6:51 they're all variations, if you look at

6:53 them closely,

6:55 of a goto statement. And the

6:57 problem is they break abstractions.

7:00 We as programmers endeavor

7:02 persistently

7:04 to have clean abstractions that let us

7:06 reason about the complexity of a

7:08 system that we're modeling. Like Sam

7:09 said earlier yesterday, all models are

7:11 wrong, but some are

7:13 useful.

7:14 All of our models of abstractions of the

7:16 real world are wrong, but they're useful

7:18 to accomplish a certain end. They're

7:20 abstractions.

7:22 We try really hard with software

7:23 architecture to have invariants about

7:26 abstractions, conditions that are true

7:28 no matter where our system is at. And we

7:29 create state machines to ever so

7:34 specifically describe the behavior in

7:36 the states of our system.

7:38 Well, that only works if our

7:40 abstractions are stable.

7:41 Goto breaks abstrations.

7:47 There's a very good article by a man named

7:49 Nathaniel J. Smith, not related to Sam

7:50 Smith,

7:52 where he talks about structured

7:54 concurrency

7:55 in the context of a goto statements in

7:57 in the context of structured

7:58 concurrency. And he talks about…

8:00 That's where this picture comes from.

8:03 He shows you how effectively a promise,

8:05 whether it's a JavaScript promise

8:08 or a

8:10 which is async/await in JavaScript,

8:12 or in Python, where you have async/

8:14 await, they're goto statements.

8:16 They really are.

8:18 And I'm not going to go into the deep…

8:19 So, we have so much to cover today.

8:21 You just go read the

8:22 article. You're going to

8:24 be so happy you read the article. Let me

8:26 go to…

8:27 There's actually a couple of articles

8:28 cuz he talks about what about timeouts

8:30 and cancellations. How do we

8:32 thread that through the code without

8:33 making it explode and blow up and look

8:35 ugly. This right here,

8:36 https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/

8:39 You can

8:40 take a picture now or get it with the

8:41 digest that Kim's going to give

8:42 everybody later [site keri.foundation]. This link is in the

8:44 notes.

8:45 What's fascinating is that Samuel Smith and

8:49 another excellent engineer, Charles

8:52 Lowell, who wrote Effection, which is a

8:53 structured concurrency library in

8:56 JavaScript, also TypeScript, they both

8:59 cite this article [Nathaniel J. Smith] as their inspiration.

9:01 I highly recommend, and in the

9:04 wallet that I put out

9:06 in preparation for yesterday's talk on

9:08 KERIA,

9:09 it's a job TypeScript application. It

9:11 uses Effection structured concurrency.

9:13 And we'll see a little bit of that code

9:14 later in this presentation.

9:16 So, you If you want to understand in

9:18 precise intricate detail why promises,

9:22 callbacks,

9:24 the Go channel, Golang, Go channels are

9:26 different kinds of

9:28 goto statements expresses concurrency

9:30 constructs, read this article. And

9:33 at the end of it, you'll be

9:34 disgusted with the different kind of

9:36 goto statements. And you'll say, "How can

9:37 I repair my abstractions? How can I have

9:40 clean abstractions?" Highly recommend

9:43 this article. You're just going to love

9:44 it. It's kind of long, so you

9:46 get a nice refreshment or give

9:48 yourself an hour or two.

9:51 So, what is structured concurrency?

9:52 Let me jump forward to a couple…

10:03 This is a borrowed diagram from Sam

10:07 Smith.

10:08 So, what's

10:10 interesting, you might say, “Well, let's

10:11 talk about root Python, the async/await.”

10:13 What it is the async HIO has an

10:15 event loop, and the event loop runs any

10:18 function that has async in the function,

10:19 right? So, the way in Python

10:22 you use their native async support

10:24 is it'll just run a

10:26 generator for you. And the way you

10:28 declare a function as a generator is you

10:30 mark it an async function.

10:33 The async function is the only way you

10:34 can run

10:36 This is not HIO. This is just regular

10:37 Python. The only way you can run an

10:39 await statement inside of a function is

10:41 if you declare it

10:43 with async as async def, right? That's

10:46 the signal to the Python runtime to plop

10:49 it into the async HIO event loop,

10:52 so it'll get scheduled. If you don't put

10:54 async in front of it,

10:55 it won't get scheduled, and then it'll

10:57 never get run. Well, now let's take that

10:59 kind of that mental model, and let's do

11:01 the HIO version of it.

11:03 The root scheduler is called a Doist.

11:06 The Doist is what will run all of your

11:09 async tasks. There are two kinds of

11:11 async tasks in HIO.

11:14 There's the DoDoer and the Doer. Now,

11:17 the DoDoer is an aggregation

11:19 construct. You can run many child tasks,

11:23 and it can schedule them. So, the

11:25 DoDoer is like a little bit of a

11:27 combination of a scheduler

11:29 and a task. It has a

11:31 function where it'll perform work and it

11:34 has the ability to add more tasks to

11:36 itself. You'll see a little bit of

11:37 similarity between HIO's structured

11:40 concurrency design and other async

11:42 frameworks like Trio and Curio

11:45 as and Effection as we go on.

11:47 So at a high-level architecture

11:48 diagram, this is what we're dealing

11:50 with. Primarily three concepts.

11:53 The scheduler,

11:54 a task aggregator that's also a task

11:56 itself,

11:57 and a task.

11:59 This is the fundamental unit of

12:00 execution in HIO.

12:03 The Doer. This is your task.

12:05 You can see this like a task group.

12:07 Schedulable task group.

12:13 So the guy who wrote this article about

12:15 fixing this problem.

12:19 Why would we use structured concurrency?

12:21 This slide says a couple of benefits.

12:24 The biggest benefit is my first bullet

12:25 point here. Program flow or task flow

12:29 follows lexical scope. Now, what does

12:31 that mean?

12:34 It means that the execution of the

12:36 program will follow your intuition as a

12:38 programmer.

12:39 You're going to write a function. You

12:40 expect control flow to go from the

12:42 beginning of the function to the end.

12:45 And when that function is over, you

12:47 expect operations you've created in that

12:49 function to have completed. Unless

12:51 you're doing threads or processes, in

12:52 which case you might return a handle

12:56 from the function to sort of go later,

12:58 but structured concurrency, usually the

13:01 mental model is

13:02 if I have a function that

13:04 fires off a bunch of tasks,

13:06 all of those should complete by the time

13:08 the function returns. Okay. And so what

13:10 you're doing is you're turning the

13:11 function

13:13 into a bit of a almost like a scheduler

13:15 of a bunch of subtasks. Are you familiar

13:17 with Erlang and a process supervisor?

13:19 No. Okay. That's a good mental model.

13:21 Erlang or Elixir?

13:23 No. so it's there's a process super

13:25 supervision model.

13:26 But I'll leave that terminology

13:28 aside.

13:29 It'll be clearer as

13:30 I get further into the code examples,

13:32 but essentially

13:33 the idea is that

13:35 if I enter into a function

13:37 and that function has tasks in it,

13:39 there's a guarantee that structured

13:41 concurrency provides that all those

13:43 tasks will finish or be canceled

13:46 by the time that I leave that function.

13:49 What that does is now you don't have any…

13:52 So, the promises break this

13:54 because what happens if

13:59 my outer scope cancels and returns and

14:02 then my promise returns later. Well, now

14:03 it's a different scope. It's a different

14:05 function scope. You might use closures

14:07 or other things to try to transfer state

14:08 to callbacks to promises as a kind

14:10 of a callback.

14:11 But, promises break this. So, what

14:13 essentially what this is saying

14:15 is that

14:16 your function is a kind of abstraction.

14:20 That function

14:22 you're not going to break the boundary

14:23 of that function cuz by the time that

14:25 function returns, everything in it will

14:27 be completed and you'll see that in code

14:28 in just a minute. Another thing,

14:30 so child tasks cannot outlive their

14:33 parent scope.

14:34 A function is like a parent scope.

14:37 All tasks you spawn within that will

14:38 finish before their parent returns. And

14:41 HIO enforces this with the dot done

14:43 attribute. Go ahead.

14:44 – When you say lexical scope, that

14:45 just makes me…

14:47 my brain, I go to the program task flow

14:52 can't outlive. Like, when the variables

14:54 that you declared in that scope go out

14:57 of scope,

14:58 the task outflow is also complete.

15:00 Yes, exactly. Precisely.

15:02 That's right.

15:04 Yes. Now, now that's a common structured…

15:06 – Closures and stuff. Like, you basically

15:08 say you don't have to deal with any of

15:09 that because

15:11 everything happens in context with stuff

15:14 that's in context with it.

15:15 Yeah. You don't have to pass any of that

15:16 state around cuz it's

15:19 you will you just avoid that problem.

15:20 – Yes, that's correct.

15:22 Okay. – This

15:23 make sense, this seems much more simple.

15:25 Exactly.

15:27 That's why people use it.

15:29 That's the whole selling point for

15:31 structured concurrency.

15:32 – But it feels more complicated for some

15:33 reason.

15:35 Well, Venkat Subramaniam, he's one of the most

15:37 popular speakers in the JVM space. He

15:39 says: it's unfamiliar. It's not that it's

15:43 difficult, it's unfamiliar.

15:45 Cuz once you learn structured

15:46 concurrency, you're like, "Oh, that maps

15:48 more onto how my brain thinks."

15:50 So, there's a little bit of upfront

15:51 learning curve, but then you're like,

15:53 "Oh, this is so straightforward. I like

15:55 this."

15:56 – What do you call that concept in the

15:57 Java world?

15:58 Is that a like a task?

16:00 They're trying to do I think

16:02 there's a fiber, if I remember right.

16:03 There's

16:04 JVM 22, I believe, is trying to do some

16:07 integration with some structured

16:08 concurrency, but it's ugly. It's not

16:11 very nice.

16:12 And Pythons and JavaScripts are very,

16:15 very nice.

16:16 HIO is the best Python

16:19 structured concurrency library out

16:21 there. It's not the most usable, but

16:23 as far as the most intuitive, by far the

16:25 most intuitive. David Beazley, a big

16:28 name in the space has one

16:29 called Curio that you'll see referenced

16:31 later. And then Nathaniel Smith has one

16:33 named Trio.

16:35 And then there's Sam's.

16:38 I've looked at all the other ones. Sam's

16:39 is the best.

16:40 It's not the most usable as far as

16:44 like there's nice wrapper

16:45 functions because the other ones have so

16:46 much more adoption, they hit a bunch of

16:48 problems, and there's a little bit more

16:49 wrapper functions to adapt them into the

16:51 async/await stuff. The async/await is a

16:53 dumpster fire. – Does Java have async

16:55 await?

16:57 Or

16:57 – They better never get it. It's a

16:59 dumpster fire. It's too bad that

17:02 it infected Python.

17:04 – It's future. I think

17:05 that's where it is.

17:06 Aasync/await, the

17:07 JavaScript form of async/await, was

17:10 forced upon the world because of the

17:11 limitations of the browser execution

17:13 engine. It was a mistake to put that

17:15 into the Python runtime itself. They

17:17 should have done structured concurrency.

17:19 If there's any mistake that

17:21 Guido made, the benevolent dictator

17:23 for life until he resigned

17:25 in Python, it was putting async/await

17:26 into Python.

17:28 They shouldn't have done that. It's

17:29 really ugly and garish. It works, but

17:32 it's overly simplified.

17:36 Because people were complaining about

17:38 generators. They were saying they're too

17:39 hard.

17:40 But you can

17:42 make frameworks that abstract the hard

17:44 parts and still preserve the structured

17:46 concurrency guarantees. So, Python had

17:48 an opportunity

17:50 the whole language ecosystem had an

17:51 opportunity to do adopt structured

17:53 concurrency and they missed it. And now

17:55 there's libraries.

17:57 Okay, so other important guarantees of

18:00 structured concurrency, if you spawn in

18:02 a lexical scope, you will join before

18:05 that lexical scope returns. And

18:07 in HIO that means

18:09 if you have a DoDoer and you spin up

18:11 some child processes, those will get

18:13 to the done state before the DoDoer

18:16 finishes. So, the Doist

18:18 will own all of its children will

18:20 complete before the Doist returns.

18:25 And some nice consequences of this is

18:29 then we have straightforward execution

18:30 handling. Failure cancellation

18:33 propagation throughout the task tree is

18:34 very intuitive from child tasks up to

18:37 parent tasks.

18:39 And now you can intelligently respond to

18:41 both failures and cancellation signals.

18:44 And there's actually a

18:46 sister article to the one that I

18:47 mentioned earlier, the note the notes on

18:49 structured concurrency goto statement

18:50 considered harmful. There's one on

18:52 timeouts and cancellations. And then

18:53 there's one on control C and interrupts.

18:55 You want to read all three. They're so

18:57 well written.

18:59 Okay, so and exceptions are carried all

19:01 the way up through the scheduling tree.

19:02 This is one of the major benefits of

19:04 structured concurrency, is that you can

19:07 have error propagation all the way up to

19:09 the root scheduler and handle it there

19:11 or in any of the parents. It's an

19:14 intuitive, straightforward way to handle

19:16 exceptions.

19:18 So, consistent, predictable exception

19:19 behavior. Very awesome and

19:20 amazing.

19:22 There's Trio and Nurseries from the

19:24 guy who wrote the article that you're

19:25 going to go read. And this is

19:27 an example from his article…

19:29 So, he uses “async with”, his

19:32 async with is essentially a Doist or a

19:35 DoDoer, just think

19:36 of it as a Doist.

19:39 So, you start one task, which might

19:41 have a child task, which might have a

19:42 child task.

19:44 And all of those complete before the

19:46 outer bounding lexical scope of the

19:48 <i>async with</i> returns.

19:51 And because it's a context manager, that

19:53 means you can have startup and teardown

19:55 logic that will complete predictably for

19:57 all these tasks, for any resources that

19:59 you need to open or close. It's kind of

20:01 Java started to go in this direction

20:03 with the try with resources, with the

20:04 auto-closeable interfaces, but they

20:06 didn't go to fully-structured concurrency

20:08 like this.

20:10 Okay, so I talked about HIO and Doers

20:12 a bit. We went here. We'll sort of

20:13 breeze past this. You already know the

20:14 Doist, Doer, DoDoer. Let me

20:16 double-check our time here. Oh, we've

20:17 only got 19 minutes left.

20:19 So, now we're getting into the HIO

20:21 internals. You get some of the high

20:23 level and an object level, what the

20:25 benefits are. Let's anchor this in

20:26 some code.

20:28 So, we mentioned the Doist is the root

20:29 scheduler, the code routine tasks are

20:31 the Doers and DoDoers.

20:32 We won't get into the details of time.

20:35 It's really fascinating. It's really

20:36 interesting. I highly recommend you read

20:38 the source. I had to read it a couple of

20:39 times.

20:40 And this is before AI came out, by the

20:42 way.

20:43 Now you can ask AI and it'll walk you

20:45 through it much faster than I learned

20:46 it. But you need to

20:48 understand the outer scheduler has

20:51 its own concept of time.

20:54 And it accumulates time.

20:56 You'll see the one sleep in the entire

20:58 system. There's only one "sleep", and it's

21:00 in the Doist, the root scheduler. And it

21:02 keeps track of its own cycle time, so

21:05 that .., and you don't need to worry about

21:06 completely understanding this, but just

21:08 anchored in your brain somewhere, the

21:09 Doist keeps track of time so it can have

21:11 deterministic scheduling of its array of

21:13 tasks.

21:15 In order to make it deterministic, it

21:18 has to keep track of its own sense of

21:19 elapsed time from the start. You always

21:22 start at zero and increase.

21:24 If you keep track of your own time and

21:25 your own sort of talk, essentially your

21:27 your next… how long you're going to

21:29 sleep before you advance to your next

21:32 execution of your array of

21:34 coroutines, you have to have an internal

21:36 concept of time. This came and bit me

21:38 and cost me 40 to 80 dollars in

21:41 in AI tokens with Codex.

21:44 Because it wasn't able to model

21:46 reality and realized that "Oh, HIO has

21:48 its own concept of time" and I actually

21:49 hit that as a bug.

21:50 [laughter]

21:51 So, you realize LLMs are going to

21:53 struggle with structured concurrency.

21:55 And if you don't know how to tell it to

21:56 do the right thing, you might waste tens

21:58 of dollars of tokens like I did. I'm

22:00 glad it wasn't hundreds.

22:01 So, remember, Doist has its own concept

22:03 of time, but we won't get into that

22:04 here. It's another presentation.

22:06 We're going to anchor this

22:08 in with some actual

22:10 commands. And we're going to go through

22:12 the KERIpy code base. If you've used

22:14 KERIpy,

22:15 you've done KLI incept.

22:17 What this does is it calls into what's

22:19 called kli.py this main function. This

22:22 is the entry point for the command line.

22:24 The important part is it says

22:26 runController.

22:27 That's important because it calls the

22:29 root scheduler.

22:30 Here, runController has these three

22:32 lines, defines the tock, which is the

22:34 run rate of the cycle time, how

22:36 quickly the root scheduler iterates over

22:38 all of its tasks.

22:41 Which means this is 1/32 of a second, so

22:43 it'll 32 times a second it'll run its

22:45 entire task list. Unless there's like

22:47 something that takes some time to

22:49 execute. Here's the instantiation of the

22:52 root scheduler. And then we say root

22:54 scheduler.do. This is a life cycle

22:56 function. This is the life cycle

22:58 function that runs the system.

23:01 So, this is the entry point of the HIO

23:03 runtime.

23:05 Okay, we don't have to worry about those

23:06 other parameters just yet. Let's get

23:08 through this.

23:09 And so, here's the whole class outline

23:11 of the Doist. Here's the Do. The Do

23:13 calls all of these other functions.

23:14 These enter, recur, exit.

23:17 So, think if for enter think

23:19 resource allocation and exit resource

23:22 closing. That's what the enter and exit

23:23 context is. There's an enter and an exit

23:27 for every task

23:29 in the HIO ecosystem. So, every Doer has

23:31 an enter and an exit.

23:33 Now, that means it

23:35 can be used as a context manager.

23:37 What does a context manager do?

23:38 What you're saying is

23:42 "Give me a before and an before to

23:44 allocate resources and an after to clean

23:46 up." That's why people use context

23:47 managers or try with resources in Java

23:50 or languages like that, so that you can

23:52 have automated cleanup [of processes, red.]

23:54 And so, what the enter and the exit do

23:57 is it allows you to make arbitrarily

23:58 smart your coroutines.

24:00 Python coroutines by themselves are dumb

24:02 and stupid. You can't add this nice cool

24:04 logic to it. This is a big selling point

24:06 of HIO

24:07 is that your tasks have an enter and an

24:09 exit.

24:10 Just about everything else in the

24:11 space only has a recur.

24:14 Go ahead. – Can you

24:16 implement handlers to those… They're

24:18 essentially events, when you

24:20 enter or when you exit then you can

24:22 attach handlers and then react to it?

24:25 – Think of in terms of

24:27 subclass. You subclass the Doer. You

24:30 override those

24:31 functions. That would be equivalent of

24:33 handlers. Okay, so now let's go deeper

24:35 into the Do function. You'll see.

24:37 So, the red arrows

24:39 are the life cycle functions. Remember I

24:41 said enter, exit, recur?

24:43 Enter.

24:45 Where are we? Recur, I guess I made this

24:47 one special cuz I'm going to go there

24:48 next. There's enter and exit, startup,

24:50 teardown.

24:52 We've got the recur, which we're going

24:53 to jump into later. Here's the only

24:55 sleep in the system. There is one sleep

24:58 in the entire Doist run time. It's this

25:00 right here. This is what accumulates

25:02 time, so essentially starts to

25:04 track the time elapsed to know when to

25:05 schedule the next co-routine.

25:11 One thing to be aware of, that self.that

25:12 enter, what it does is it takes all of

25:15 your tasks and runs enter on every task.

25:18 So, by the time that finishes, all your

25:20 resources have been provisioned for all

25:22 the tasks that are going to run. – After

25:24 that one line? – After that one line, yes.

25:26 And what it does,

25:28 it advances every single

25:30 co-routine to be primed to be ready,

25:32 essentially.

25:34 So, that readies all the tasks for

25:35 execution, and then recur begins the

25:38 infinite while loop to run all of your

25:39 tasks until you get a an abort an

25:42 interrupt signal, which will then

25:44 trigger exit. Like control-C will

25:46 trigger exit, and it'll exit all of the

25:48 child tasks, and then their nested

25:50 tasks, and then it'll finally exit the

25:51 Doist.

25:53 That's one of the cool

25:54 things about structured concurrency.

25:57 Okay, so we're going to jump in. Let's

25:58 jump in. So, now this is recur,

26:00 remember?

26:02 We're going into the blue arrow, or

26:03 the teal green arrow.

26:06 This is the recur function. The main

26:08 So, what actually runs…

26:10 We're still in the scheduler. When are

26:12 we going to get to the task? Well, right

26:14 here, this send. And that send right

26:15 there, that's the Python generator API.

26:19 Cuz remember, the

26:21 co-routines are just generators.

26:24 And the syntax to run a

26:26 generator

26:28 is this "send" right here.

26:30 You can ask

26:32 Claude or Codex about it.

26:34 There's also a really excellent… So,

26:36 Luciano Ramalho, the guy who wrote the

26:38 Python reference manual,

26:40 his 2016 version of the book has a

26:42 really great chapter on co-routines. And

26:44 then when the whole Python language

26:46 community went the wrong direction to

26:47 the oversimplified async/await,

26:49 for the 2022 version of the Python

26:51 reference manual, he did not include

26:53 that awesome content on co-routines, but

26:56 it's on his website. So, you can go to I

26:58 think it's [https://www.fluentpython.com/]

27:00 and you'll find the chapter 16, I think,

27:02 on the co-routines. It'll go

27:04 through that in exhaustively in-depth.

27:05 It'll teach you everything you want to

27:07 know about Python co-routines.

27:08 High-density, high-value content.

27:10 Anyway, so this is what runs it. We

27:12 won't worry about the rest of the stuff.

27:13 So, this interest interestingly enough,

27:15 last note on the root scheduler, this

27:17 tick advances you forward that next 1/32

27:20 of a second. It starts the next time it

27:22 schedules the time sleep, after which

27:26 the next iteration of all the tasks will

27:27 occur.

27:29 Okay, so now let's go into the

27:30 individual task. What it does, that ".send"

27:33 calls .., you might be familiar with

27:35 other Dunder methods, the underscore

27:37 underscore methods in Python.

27:39 It calls the call method of a Doer.

27:42 And what the Doer does is it says

27:44 self.do. So, what that send does,

27:48 this

27:49 the and it's dog, I won't get into the

27:50 syntax here. Dog stands for Doer

27:53 generator. There's some

27:55 Sam-isms here that it takes a little bit

27:56 of time to get accustomed to,

27:59 but this is essentially the generator

28:01 that is the subtask.

28:03 The "dog". So, we're doing the send

28:05 and that eventually results in self.do.

28:07 Now, why is that important?

28:09 The Doer.do is the entry point into the

28:12 life cycle of one task. And let's

28:15 look at that. Here's the Doer.do.

28:17 So, look at that. We've got an enter,

28:19 exit, and a couple of other life cycle

28:21 functions for different reasons that we

28:23 don't have time to get into, but

28:25 there's a <i>clean</i>, there's an

28:26 <i>abort</i>, there's a <i>cease</i>.

28:28 And the important thing is

28:30 that

28:31 Okay, you see the <i>enter</i> and the <i>exit</i>,

28:33 startup teardown, and you see the <i>recur</i>.

28:35 So, self.recur. This will actually

28:37 execute the self, but there's another

28:39 important thing

28:41 that you need to know a little bit about

28:42 Python generators, to know how this

28:44 works.

28:45 See how we say isgeneratorfunction,

28:47 Self.recur.

28:48 What that means is if you have a yield

28:50 statement, this is how Python works. If

28:53 you put a yield or a yield from in a

28:54 function body,

28:56 it turns that into a generator.

28:58 If you're going to do anything with

29:00 IO, the network, files,

29:02 that's usually where

29:05 you're going to have to wait. And

29:08 usually you use some sort of yield or

29:09 yield from or async/await. You need to

29:11 drive that

29:13 in a way to where it can be paused and

29:15 resumed later.

29:16 That's what generators are used for in

29:17 Python. So, you need to use yield from

29:20 syntax to drive the generator function.

29:22 The generator function will stay inert

29:24 and it'll never execute unless you have

29:26 a "yield from."

29:28 So, if you have a recur function in a

29:30 task that needs to call some business

29:32 logic that's going to execute like a

29:33 network call or a file call, you need

29:36 this yield from. This is what runs ..,

29:39 you can put synchronous logic in here,

29:41 in your recur, and it'll

29:43 execute this line, but when you

29:44 have async logic, it'll

29:47 call the yield from.

29:48 Now, let's we're going really fast and

29:50 we I know we only have 10 minutes left,

29:52 so I'm going to do my best to just keep

29:53 on going.

29:54 If it's uncomfortable, if you don't

29:56 aren't quite tracking, it's okay. Let's

29:59 get to the end and then we'll get

30:00 to some questions.

30:02 So, we went to the from the Doist.do

30:06 to the recur

30:08 of the main event

30:09 loop or scheduler. And now we're going

30:12 to the individual task do. And the

30:14 individual task do is taking us to the

30:16 recur of the task.

30:19 So, and you'll see it's empty except for

30:21 return false. You have to subclass it if

30:23 you want to do work, right? So, the

30:25 the main Doer construct is almost

30:27 like an abstract class in the sense that

30:29 you need to subclass it, override the

30:31 recur, and that's where you call your

30:33 business logic function.

30:35 And the false there is interesting. So,

30:37 if you return false, it'll keep

30:39 running until you return true. Because

30:42 the way that you end something is you

30:43 return true cuz it's the done or not

30:45 status. So, return true sets done to

30:48 true in the root scheduler for that

30:50 task.

30:51 That's why the default is to return

30:52 false.

30:54 Now, this is an example

30:56 from the KERIpy code base. If you've

30:57 used KERI it all, you probably have to

30:59 deal with database migrations. We have a

31:01 Doer that does database migrations. We

31:04 have a recur.

31:06 We open up the database.

31:09 And then after that, so really this

31:10 database stuff should be in an enter

31:12 function. Whoever wrote this wasn't

31:14 trying to fully embrace the life cycle

31:17 functions. This is set up that really

31:19 should be

31:20 in a

31:22 What do you call it? And you notice,

31:24 where's the cleanup?

31:27 We need an enter and an exit function.

31:28 So, this is not as good as it could

31:30 be, but it's sort of sufficient for now.

31:32 So, we run all this business logic, we

31:33 run database migrate, return true.

31:36 Runs the database migrations, return

31:38 true, and it's done.

31:39 So, this is a basic task. If I do

31:42 "kli migrate run"

31:44 this will trigger this.

31:46 It'll migrate your database version.

31:48 Now, if you're going to look through the

31:49 rest of the KERIpy code base, you

31:50 might say, "Why do we use so many

31:53 DoDoers, but not many Doers?"

31:56 And it's because

31:58 of one function, doing.doify.

32:03 That's kind of funny.

32:05 Sam has a really

32:07 fascinating naming policy.

32:09 But what doify does,

32:11 it essentially allows you to use yield

32:13 from in the body of a function. Remember

32:15 how I said structured concurrency says,

32:19 "Program flow follows lexical scope."

32:23 The reason why doify is used is because

32:26 it allows that sort of intuitive program

32:28 flow to follow lexical scope

32:30 contract to be met. And it's easy to

32:33 essentially schedule a lot of subtasks

32:34 in one lexical context that makes

32:36 intuitive sense.

32:38 And so, I'll give you an example here.

32:41 Oh, so and actually there's a

32:42 difference. So, this is the DoDoer

32:43 function. You'll notice there's a

32:44 critical difference. So, the reason why

32:46 you use doify is because there's there's

32:49 something that

32:50 I think is my opinion it's missing

32:52 from DoDoer.

32:54 I think Sam argues differently. He

32:55 argues for a different or

32:57 way to arrange things.

32:59 Personally, I think that the way that we

33:01 use doify

33:03 is evidence

33:05 that we should add the missing part

33:07 here. See here we have self.recur, but

33:09 we don't have the generator check.

33:10 Remember the generator check? If I go

33:12 one slide over, here's the Doer.

33:15 We say if is generator yield from. So,

33:17 what this means is a DoDoer will not

33:19 allow you to use yield from in its

33:21 recur. That's why we use doify all the

33:24 time. And Sam has this argument about

33:26 hierarchical state machines is the right

33:28 place to use state to essentially

33:30 manage multiple sets of Doers, and then

33:31 you have a scheduler Doer that manages

33:33 other Doers as a set of Doers within a

33:35 DoDoer. There's an argument there, but

33:37 I don't think it's very intuitive.

33:39 I understand the argument. Maybe I just

33:41 need to maybe I'm just unfamiliar and I

33:43 need to learn that. But to me, the fact

33:45 that we use doify so much is

33:46 evidence that we should add

33:50 this is generator

33:52 in the Doer and see now we're in DoDoer

33:54 right above here. We should put that is

33:57 generator check and run it that way in

33:58 my opinion. And you'll see why. So, now

34:00 let's get to the code. But notice the

34:02 DoDoer has the same life cycle functions.

34:05 Enter, exit, clean abort sees, and we

34:08 call recur.

34:10 So, the DoDoer

34:13 And you'll notice this return self.done

34:15 for when a task is done, true or false.

34:17 The difference between the DoDoer and

34:19 the Doer that is the DoDoer you can add

34:22 it's extend or remove. You can add tasks

34:24 to it or remove tasks to it. So, it's an

34:26 aggregation construct. The Doer is not.

34:30 So, you can see how the DoDoer is a bit

34:33 of a combination between a scheduler

34:36 and a task, but the Doer is just a task.

34:44 There's an example. So,

34:45 back to the [inaudible] incept.

34:47 Inside of the init function for the

34:49 Incept Doer, what you'll notice is a

34:50 DoDoer. We call doing.doify on the

34:53 main function that runs everything.

34:56 What that does

34:58 Here's the incept do. You see we have a

34:59 yield from yield from. It's

35:03 pretty intuitive to co-locate a lot of

35:05 that logic. Rather than create a

35:06 complicated state machine and worry

35:08 about the state of this Doer and that

35:10 Doer and sort of schedule them at the

35:11 right time, we just locate them in the

35:12 same lexical scope.

35:15 That's pretty easy for humans to

35:17 understand that program flow.

35:20 That's why it's so handy. That's why in

35:21 my opinion

35:23 this really should be a recur

35:25 function, and the recur should allow you

35:27 to use these yields and yields from

35:29 in the body

35:30 of the DoDoer's recur.

35:32 That's not how it works. I actually did

35:35 go through the trouble

35:36 in the DID:webs repository in our tests

35:39 to do it the way that Sam recommended,

35:40 and it's kind of interesting. It does

35:41 make you clean up your code a little bit…

35:45 There's an argument there. And what it

35:46 does is essentially the mental model is

35:48 if you're going to do things the right

35:49 idiomatic way according to Sam's

35:52 idea, every place where you have a yield

35:54 from should essentially be a

35:56 separate Doer.

35:58 You have at least one, two,

36:00 possibly three more here. And what you

36:02 do is you would add all of those…

36:05 See how we have a list of

36:07 Doers here, and then we send it when we

36:08 say

36:09 call the constructor we init Doers

36:11 equals Doers.

36:12 What you would do instead of having all

36:13 those yield froms in one function, you

36:15 would make separate Doers for that sub

36:17 part of the task, and you'd have that be

36:20 part of the task array that you send to

36:21 yourself when you're scheduling

36:23 yourself.

36:24 You can see it sort of force

36:26 you to lift things out, and make

36:27 dependencies more explicit. It's kind of

36:28 like smaller functions versus larger

36:30 functions. So it is more hygienic in

36:31 some senses, a little bit more

36:32 convoluted. So it's a bit of a

36:34 trade-off.

36:37 Now I'm just mention Effection. So

36:39 Effection is a JavaScript structured

36:41 concurrency library. Now that you

36:43 understand

36:45 the yield from syntax and the yield and

36:47 whatever, now you can see what it looks

36:48 like in JavaScript cuz JavaScript has a

36:50 construct that's like yield from.

36:53 If you see yield star in JavaScript,

36:55 it's the same thing. It runs a

36:57 co-routine

36:59 from that spot. So this is using the

37:01 Effection

37:03 structured concurrency library

37:05 just like how we did

37:06 doing.doify.

37:08 This is part of why I lean in favor of

37:10 making DoDoer.recur do allow yield from

37:13 because in Effection in the structured

37:15 concurrency library in TypeScript /

37:17 JavaScript

37:18 this whole function is like a doified

37:20 function.

37:21 You can have as many yield stars,

37:24 which are yield froms, as you want and

37:26 just sort of naturally proceed forth in

37:27 an execution chain in one function.

37:30 You'll see I'm saying yield from

37:31 list identifier service and we'll look

37:33 at that one layer deeper.

37:35 This actually it wraps the Signify

37:37 client. You'll see it has a Signify

37:38 client right here.

37:39 This is the wrapper. This is another

37:42 thing that I think is sort of missing

37:43 from HIO that we could add to it.

37:45 Wouldn't be that hard to add it. Sam has

37:46 actually been experimenting with the

37:48 wrappers.

37:49 More recently with trying to

37:51 integrate async/await into HIO. It's

37:54 already done in the Effection.

37:56 We say call promise and turn it into a

37:58 generator.

37:59 This allows you to preserve the

38:00 structured concurrency guarantees

38:03 even when you use promises in

38:04 JavaScript. You'll have promises at sort

38:06 of the edge of your architecture and all

38:08 the nice stuff in between you'll have

38:09 your structured concurrency guarantees.

38:11 – Just call a library you're importing

38:12 that you can't control.

38:13 – Exactly. Precisely. And

38:17 if you look at the wallet that I demoed

38:18 yesterday for my… I need a really

38:20 need to wrap up. Oh.

38:22 Okay, let me just run through the end.

38:23 Okay, the last thing is…

38:25 Look just look at these in the slides

38:26 later. You'll see this weird

38:28 convention used everywhere in KERIpy

38:30 the self.wind, self.talk and yield. And what

38:33 it is there's something with all

38:34 generators in JavaScript and in Python

38:35 where you need to prime it. Prime it

38:37 means advance to the first yield. You

38:39 don't want the priming of your generator

38:41 to run business logic.

38:44 You don't want it to come down here, so

38:45 you put this yield at the front. I think

38:47 that should be handled by a framework in

38:48 my opinion.

38:50 It's kind of weird to force people to

38:51 manage that, but that's why this exists.

38:53 And

38:55 Okay, and I would say there's

38:59 Oh, we'll cut it there.

39:02 That's HIO.

39:04 [applause]