Posts in Backend (20 found)
Justin Duke 5 days ago

Adding imports to the Django shell

I was excited to finally remove from my file when 5.2 dropped because they added support for automatic model import. However, I found myself missing one other little escape hatch that exposed, which was the ability to import other arbitrary modules into the namespace. Django explains how to do bring in modules without a namespace , but I wanted to be able to inoculate my shell, since most of my modules follow a similar structure (exposing a single function). It took the bare minimum of sleuthing to figure out how to hack this in for myself, and now here I am to share that sleuthing with you. Behold, a code snippet that is hopefully self-explanatory:

0 views

Functional Threading “Macros”

Read on the website: Threading macros make Lisp-family languages much more readable. Other languages too, potentially! Except… other languages don’t have macros. How do we go about enabling threading “macros” there?

0 views

Consistent hashing

As a concrete experiment, I ran a similar simulation to the one above, but with 10 virtual nodes per node. We'll consider the total portion of the circle mapped to a node when it maps to either of its virtual nodes. While the average remains 18 degrees, the variance is reduced drastically - the smallest one is 11 degrees and the largest 26. You can find the code for these experiments in the demo.go file of the source code repository. This is close to the original motivation for the development of consistent caching by researchers at MIT. Their work, described in the paper "Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web" became the foundation of Akamai Technologies . There are other use cases of consistent hashing in distributed systems. AWS popularized one common application in their paper "Dynamo: Amazon’s Highly Available Key-value Store" , where the algorithm is used to distribute storage keys across servers.

1 views
Max Bernstein 2 months ago

Linear scan register allocation on SSA

Much of the code and education that resulted in this post happened with Aaron Patterson . The fundamental problem in register allocation is to take an IR that uses a virtual registers (as many as you like) and rewrite it to use a finite amount of physical registers and stack space 1 . This is an example of a code snippet using virtual registers: And here is the same example after it has been passed through a register allocator (note that Rs changed to Ps): Each virtual register was assigned a physical place: R1 to the stack, R2 to P0, R3 to P1, and R4 also to P0 (since we weren’t using R2 anymore). People use register allocators like they use garbage collectors: it’s an abstraction that can manage your resources for you, maybe with some cost. When writing the back-end of a compiler, it’s probably much easier to have a separate register-allocator-in-a-box than manually managing variable lifetimes while also considering all of your different target architectures. How do JIT compilers do register allocation? Well, “everyone knows” that “every JIT does its own variant of linear scan” 2 . This bothered me for some time because I’ve worked on a couple of JITs and still didn’t understand the backend bits. There are a couple different approaches to register allocation, but in this post we’ll focus on linear scan of SSA . I started reading Linear Scan Register Allocation on SSA Form (PDF, 2010) by Wimmer and Franz after writing A catalog of ways to generate SSA . Reading alone didn’t make a ton of sense—I ended up with a lot of very frustrated margin notes. I started trying to implement it alongside the paper. As it turns out, though, there is a rich history of papers in this area that it leans on really heavily. I needed to follow the chain of references! For example, here is a lovely explanation of the process, start to finish, from Christian Wimmer’s Master’s thesis (PDF, 2004). There it is, all laid out at once. It’s very refreshing when compared to all of the compact research papers. I didn’t realize that there were more than one or two papers on linear scan. So this post will also incidentally serve as a bit of a survey or a history of linear scan—as best as I can figure it out, anyway. If you were in or near the room where it happened, please feel free to reach out and correct some parts. Throughout this post, we’ll use an example SSA code snippet from Wimmer2010, adapted from phi-SSA to block-argument-SSA. Wimmer2010’s code snippet is between the arrows and we add some filler (as alluded to in the paper): Virtual registers start with R and are defined either with an arrow or by a block parameter. Because it takes a moment to untangle the unfamiliar syntax and draw the control-flow graph by hand, I’ve also provided the same code in graphical form. Block names (and block parameters) are shaded with grey. We have one entry block, , that is implied in Wimmer2010. Its only job is to define and for the rest of the CFG. Then we have a loop between and with an implicit fallthrough. Instead of doing that, we instead generate a conditional branch with explicit jump targets. This makes it possible to re-order blocks as much as we like. The contents of are also just to fill in the blanks from Wimmer2010 and add some variable uses. Our goal for the post is to analyze this CFG, assign physical locations (registers or stack slots) to each virtual register, and then rewrite the code appropriately. For now, let’s rewind the clock and look at how linear scan came about. Linear scan register allocation (LSRA) has been around for awhile. It’s neat because it does the actual register assignment part of register allocation in one pass over your low-level IR. (We’ll talk more about what that means in a minute.) It first appeared in the literature in tcc: A System for Fast, Flexible, and High-level Dynamic Code Generation (PDF, 1997) by Poletto, Engler, and Kaashoek. (Until writing this post, I had never seen this paper. It was only on a re-read of the 1999 paper (below) that I noticed it.) In this paper, they mostly describe a staged variant of C called ‘C (TickC), for which a fast register allocator is quite useful. Then came a paper called Quality and Speed in Linear-scan Register Allocation (PDF, 1998) by Traub, Holloway, and Smith. It adds some optimizations (lifetime holes, binpacking) to the algorithm presented in Poletto1997. Then came the first paper I read, and I think the paper everyone refers to when they talk about linear scan: Linear Scan Register Allocation (PDF, 1999) by Poletto and Sarkar. In this paper, they give a fast alternative to graph coloring register allocation, especially motivated by just-in-time compilers. In retrospect, it seems to be a bit of a rehash of the previous two papers. Linear scan (1997, 1999) operates on live ranges instead of virtual registers. A live range is a pair of integers [start, end) (end is exclusive) that begins when the register is defined and ends when it is last used. This means that there is an assumption that the order for instructions in your program has already been fixed into a single linear sequence! It also means that you have given each instruction a number that represents its position in that order. This may or not be a surprising requirement depending on your compilers background. It was surprising to me because I often live in control flow graph fantasy land where blocks are unordered and instructions sometimes float around. But if you live in a land of basic blocks that are already in reverse post order, then it may be less surprising. In non-SSA-land, these live ranges are different from the virtual registers: they represent some kind of lifetimes of each version of a virtual register. For an example, consider the following code snippet: There are two definitions of and they each live for different amounts of time: In fact, the ranges are completely disjoint. It wouldn’t make sense for the register allocator to consider variables, because there’s no reason the two s should necessarily live in the same physical register. In SSA land, it’s a little different: since each virtual registers only has one definition (by, uh, definition), live ranges are an exact 1:1 mapping with virtual registers. We’ll focus on SSA for the remainder of the post because this is what I am currently interested in. The research community seems to have decided that allocating directly on SSA gives more information to the register allocator 3 . Linear scan starts at the point in your compiler process where you already know these live ranges—that you have already done some kind of analysis to build a mapping. In this blog post, we’re going to back up to the point where we’ve just built our SSA low-level IR and have yet to do any work on it. We’ll do all of the analysis from scratch. Part of this analysis is called liveness analysis . The result of liveness analysis is a mapping of that tells you which virtual registers (remember, since we’re in SSA, instruction==vreg) are alive (used later) at the beginning of the basic block. This is called a live-in set. For example: We compute liveness by working backwards: a variable is live from the moment it is backwardly-first used until its definition. In this case, at the end of B2, nothing is live. If we step backwards to the , we see a use: R16 becomes live. If we step once more, we see its definition—R16 no longer live—but now we see a use of R14 and R15, which become live. This leaves us with R14 and R15 being live-in to B2. This live-in set becomes B1’s live-out set because B1 is B2’s predecessor. We continue in B1. We could continue backwards linearly through the blocks. In fact, I encourage you to do it as an exercise. You should have a (potentially emtpy) set of registers per basic block. It gets more interesting, though, when we have branches: what does it mean when two blocks’ live-in results merge into their shared predecessor? If we have two blocks A and B that are successors of a block C, the live-in sets get unioned together. C C A A C->A B B C->B That is, if there were some register R0 live-in to B and some register R1 live-in to A, both R0 and R1 would be live-out of C. They may also be live-in to C, but that entirely depends on the contents of C. Since the total number of virtual registers is nonnegative and is finite for a given program, it seems like a good lattice for an abstract interpreter . That’s right, we’re doing AI. In this liveness analysis, we’ll: We store gen, kill, and live-in sets as bitsets, using some APIs conveniently available on Ruby’s Integer class. Like most abstract interpretations, it doesn’t matter what order we iterate over the collection of basic blocks for correctness, but it does matter for performance. In this case, iterating backwards ( ) converges much faster than forwards ( ): We could also use a worklist here, and it would be faster, but eh. Repeatedly iterating over all blocks is fine for now. The Wimmer2010 paper skips this liveness analysis entirely by assuming some computed information about your CFG: where loops start and end. It also requires all loop blocks be contiguous. Then it makes variables defined before a loop and used at any point inside the loop live for the whole loop . By having this information available, it folds the liveness analysis into the live range building, which we’ll instead do separately in a moment. The Wimmer approach sounded complicated and finicky. Maybe it is, maybe it isn’t. So I went with a dataflow liveness analysis instead. If it turns out to be the slow part, maybe it will matter enough to learn about this loop tagging method. For now, we will pick a schedule for the control-flow graph. In order to build live ranges, you have to have some kind of numbering system for your instructions, otherwise a live range’s start and end are meaningless. We can write a function that fixes a particular block order (in this case, reverse post-order) and then assigns each block and instruction a number in a linear sequence. You can think of this as flattening or projecting the graph: A couple interesting things to note: Even though we have extra instructions, it looks very similar to the example in the Wimmer2010 paper. Since we’re not going to be messing with the order of the instructions within a block anymore, all we have to do going forward is make sure that we iterate through the blocks in . Finally, we have all that we need to compute live ranges. We’ll more or less copy the algorithm to compute live ranges from the Wimmer2010 paper. We’ll have two main differences: I know I said we were going to be computing live ranges. So why am I presenting you with a function called ? That’s because early in the history of linear scan (Traub1998!), people moved from having a single range for a particular virtual register to having multiple disjoint ranges. This collection of multiple ranges is called an interval and it exists to free up registers in the context of branches. For example, in the our IR snippet (above), R12 is defined in B2 as a block parameter, used in B3, and then not used again until some indetermine point in B4. (Our example uses it immediately in an add instruction to keep things short, but pretend the second use is some time away.) The Wimmer2010 paper creates a lifetime hole between 28 and 34, meaning that the interval for R12 (called i12) is . Interval holes are not strictly necessary—they exist to generate better code. So for this post, we’re going to start simple and assume 1 interval == 1 range. We may come back later and add additional ranges, but that will require some fixes to our later implementation. We’ll note where we think those fixes should happen. Anyway, here is the mostly-copied annotated implementation of BuildIntervals from the Wimmer2010 paper: Another difference is that since we’re using block parameters, we don’t really have this thing. That’s just the block argument. The last difference is that since we’re skipping the loop liveness hack, we don’t modify a block’s set as we iterate through instructions. I know we said we’re building live ranges, so our class only has one on it. This is Ruby’s built-in range, but it’s really just being used as a tuple of integers here. Note that there’s some implicit behavior happening here: For example, if we have and someone calls , we end up with . There’s no gap in the middle. And if we have and someone calls , we end up with . After figuring out from scratch some of these assumptions about what the interval/range API should and should not do, Aaron and I realized that there was some actual code for in a different, earlier paper: Linear Scan Register Allocation in the Context of SSA Form and Register Constraints (PDF, 2002) by Mössenböck and Pfeiffer. Unfortunately, many other versions of this PDF look absolutely horrible (like bad OCR) and I had to do some digging to find the version linked above. Finally we can start thinking about doing some actual register assignment. Let’s return to the 90s. Because we have faithfully kept 1 interval == 1 range, we can re-use the linear scan algorithm from Poletto1999 (which looks, at a glance, to be the same as 1997). I recommend looking at the PDF side by side with the code. We have tried to keep the structure very similar. Note that unlike in many programming languages these days, in the algorithm description represents a set , not a (hash-)map. In our Ruby code, we represent as an array: Internalizing this took us a bit. It is mostly a three-state machine: We would like to come back to this and incrementally modify it as we add lifetime holes to intervals. I finally understood, very late in the game, that Poletto1999 linear scan assigns only one location per virtual register. Ever . It’s not that every virtual register gets a shot in a register and then gets moved to a stack slot—that would be interval splitting and hopefully we get to that later—if a register gets spilled, it’s in a stack slot from beginning to end. I only found this out accidentally after trying to figure out a bug (that wasn’t a bug) due to a lovely sentence in Optimized Interval Splitting in a Linear Scan Register Allocator (PDF, 2005) by Wimmer and Mössenböck): However, it cannot deal with lifetime holes and does not split intervals, so an interval has either a register assigned for the whole lifetime, or it is spilled completely. In particular, it is not possible to implement the algorithm without reserving a scratch register: When a spilled interval is used by an instruction requiring the operand in a register, the interval must be temporarily reloaded to the scratch register Additionally, register constraints for method calls and instructions requiring fixed registers must be handled separately Let’s take a look at the code snippet again. Here it is before register allocation, using virtual registers: Let’s run it through register allocation with incrementally decreasing numbers of physical registers available. We get the following assignments: Some other things to note: If you have a register free, choosing which register to allocate is a heuristic! It is tunable. There is probably some research out there that explores the space. In fact, you might even consider not allocating a register greedily. What might that look like? I have no idea. Spilling the interval with the furthest endpoint is a heuristic! You can pick any active interval you want. In Register Spilling and Live-Range Splitting for SSA-Form Programs (PDF, 2009) by Braun and Hack, for example, they present the MIN algorithm, which spills the interval with the furthest next use. This requires slightly more information and takes slightly more time than the default heuristic but apparently generates much better code. Also, block ordering? You guessed it. Heuristic. Here is an example “slideshow” I generated by running linear scan with 2 registers. Use the arrow keys to navigate forward and backward in time 4 . At this point we have register assignments : we have a hash table mapping intervals to physical locations. That’s great but we’re still in SSA form: labelled code regions don’t have block arguments in hardware. We need to write some code to take us out of SSA and into the real world. We can use a modified Wimmer2010 as a great start point here. It handles more than we need to right now—interval splitting—but we can simplify. Because we don’t split intervals, we know that every interval live at the beginning of a block is either: For this reason, we only handle the second case in our SSA resolution. If we added lifetime holes interval splitting, we would have to go back to the full Wimmer SSA resolution. This means that we’re going to iterate over every outbound edge from every block. For each edge, we’re going to insert some parallel moves. This already looks very similar to the RESOLVE function from Wimmer2010. Unfortunately, Wimmer2010 basically shrugs off with an eh, it’s already in the literature comment. What’s not made clear, though, is that this particular subroutine has been the source of a significant amount of bugs in the literature. Only recently did some folks roll through and suggest (proven!) fixes: This sent us on a deep rabbit hole of trying to understand what bugs occur, when, and how to fix them. We implemented both the Leroy and the Boissinot algorithms. We found differences between Boissinot2009, Boissinot2010, and the SSA book implementation following those algorithms. We found Paul Sokolovsky’s implementation with bugfixes . We found Dmitry Stogov’s unmerged pull request to the same repository to fix another bug. We looked at Benoit Boissinot’s thesis again and emailed him some questions. He responded! And then he even put up an amended version of his algorithm in Rust with tests and fuzzing. All this is to say that this is still causing people grief and, though I understand page limits, I wish parallel moves were not handwaved away. We ended up with this implementation which passes all of the tests from Sokolovsky’s repository as well as the example from Boissinot’s thesis (though, as we discussed in the email, the example solution in the thesis is incorrect 5 ). Leroy’s algorithm, which is shorter, passes almost all the tests—in one test case, it uses one more temporary variable than Boissinot’s does. We haven’t spent much time looking at why. Whatever algorithm you choose, you now have a way to parallel move some registers to some other registers. You have avoided the “swap problem”. That’s great. You can generate an ordered list of instructions from a tangled graph. But where do you put them? What about the “lost copy” problem? As it turns out, we still need to handle critical edge splitting. Let’s consider what it means to insert moves at an edge between blocks when the surrounding CFG looks a couple of different ways. These are the four (really, three) cases we may come across. In Case 1, if we only have two neighboring blocks A and B, we can insert the moves into either block. It doesn’t matter: at the end of A or at the beginning of B are both fine. In Case 2, if A has two successors, then we should insert the moves at the beginning of B. That way we won’t be mucking things up for the edge . In Case 3, if B has two predecessors, then we should insert the moves at the end of A. That way we won’t be mucking things up for the edge . Case 4 is the most complicated. There is no extant place in the graph we can insert moves. If we insert in A, we mess things up for . If we insert in , we mess things up for . Inserting in or doesn’t make any sense. What is there to do? As it turns out, Case 4 is called a critical edge . And we have to split it. We can insert a new block E along the edge and put the moves in E! That way they still happen along the edge without affecting any other blocks. Neat. In Ruby code, that looks like: Adding a new block invalidates the cached , so we also need to recompute that. We could also avoid that by splitting critical edges earlier, before numbering. Then, when we arrive in , we can clean up branches to empty blocks! (See also Nick’s post on critical edge splitting , which also links to Faddegon’s thesis, which I should at least skim.) And that’s it, folks. We have gone from virtual registers in SSA form to physical locations. Everything’s all hunky-dory. We can just turn these LIR instructions into their very similar looking machine equivalents, right? Not so fast… You may have noticed that the original linear scan paper does not mention calls or other register constraints. I didn’t really think about it until I wanted to make a function call. The authors of later linear scan papers definitely noticed, though; Wimmer2005 writes the following about Poletto1999: When a spilled interval is used by an instruction requiring the operand in a register, the interval must be temporarily reloaded to the scratch register. Additionally, register constraints for method calls and instructions requiring fixed registers must be handled separately. Fun. We will start off by handling calls and method parameters separately, we will note that it’s not amazing code, and then we will eventually implement the later papers, which handle register constraints more naturally. We’ll call this new function after register allocation but before SSA resolution. We do it after register allocation so we know where each virtual register goes but before resolution so we can still inspect the virtual register operands. Its goal is to do a couple of things: We’ll also remove the operands since we’re placing them in special registers explicitly now. (Unfortunately, this sidesteps handling the less-fun bit of calls in ABIs where after the 6th parameter, they are expected on the stack. It also completely ignores ABI size constraints.) Now, you may have noticed that we don’t do anything special for the incoming params of the function we’re compiling! That’s another thing we have to handle. Thankfully, we can handle it with yet another parallel move (wow!) at the end of . Again, this is yet another kind of thing where some of the later papers have much better ergonomics and also much better generated code. But this is really cool! If you have arrived at this point with me, we have successfully made it to 1997 and that is nothing to sneeze at. We have even adapted research from 1997 to work with SSA, avoiding several significant classes of bugs along the way. We have just built an enormously complex machine. Even out the gate, with the original linear scan, there is a lot of machinery. It’s possible to write tests that spot check sample programs of all shapes and sizes but it’s very difficult to anticipate every possible edge case that will appear in the real world. Even if the original algorithm you’re using has been proven correct, your implementation may have subtle bugs due to (for example) having slightly different invariants or even transcription errors. We have all these proof tools at our disposal: we can write an abstract interpreter that verifies properties of one graph, but it’s very hard (impossible?) to scale that to sets of graphs. Maybe that’s enough, though. In one of my favorite blog posts, Chris Fallin writes about writing a register allocation verifier based on abstract interpretation. It can verify one concrete LIR function at a time. It’s fast enough that it can be left on in debug builds. This means that a decent chunk of the time (tests, CI, maybe a production cluster) we can get a very clear signal that every register assignment that passes through the verifier satisfies some invariants. Furthermore, we are not limited to Real World Code. With the advent of fuzzing, one can imagine an always-on fuzzer that tries to break the register allocator. A verifier can then catch bugs that come from exploring this huge search space. Some time after finding Chris’s blog post, I also stumbled across the very same thing in V8 ! I find this stuff so cool. I’ll also mention Boissinot’s Rust code again because it does something similar for parallel moves. It’s possible to do linear scan allocation in reverse, at least on traces without control-flow. See for example The Solid-State Register Allocator , the LuaJIT register allocator , and Reverse Linear Scan Allocation is probably a good idea . By doing linear scan this way, it is also possible to avoid computing liveness and intervals. I am not sure if this works on programs with control-flow, though. We built a register allocator that works on SSA. Hopefully next time we will add features such as lifetime holes, interval splitting, and register hints. The full Ruby code listing is not (yet?) public available under the Apache 2 license . UPDATE: See the post on lifetime holes . Thanks to Waleed Khan and Iain Ireland for giving feedback on this post. It’s not just about registers, either. In 2016, Facebook engineer Dave legendarily used linear-scan register allocation to book meeting rooms .  ↩ Well. As I said on one of the social media sites earlier this year, “All AOT compilers are alike; each JIT compiler is fucked up in its own way.” JavaScript: Linear Scan Register Allocation in the Context of SSA Form and Register Constraints (PDF, 2002) by Mössenböck and Pfeiffer notes: Our allocator relies on static single assignment form, which simplifies data flow analysis and tends to produce short live intervals. Register allocation for programs in SSA-form (PDF, 2006) by Hack, Grund, and Goos notes that interference graphs for SSA programs are chordal and can be optimally colored in quadratic time. SSA Elimination after Register Allocation (PDF, 2008) by Pereira and Palsberg notes: One of the main advantages of SSA based register allocation is the separation of phases between spilling and register assignment. Cliff Click (private communication, 2025) notes: It’s easier. Got it already, why lose it […] spilling always uses use/def and def/use edges. This is inspired by Rasmus Andersson ’s graph coloring visualization that I saw some years ago.  ↩ The example in the thesis is to sequentialize the following parallel copy: The solution in the thesis is: but we think this is incorrect. Solving manually, Aaron and I got: which is what the code gives us, too.  ↩

0 views
Phil Eaton 2 months ago

What even is distributed systems

Distributed systems is simply the study of interactions between processes. Every two interacting processes form a distributed system, whether they are on the same host or not. Distributed systems create new challenges (compared to single-process systems) in terms of correctness (i.e. consistency ), reliability, and performance (i.e. latency and throughput). The best way to learn about the principles and fundamentals of distributed systems is to 1) read Designing Data Intensive Applications and 2) read through the papers and follow the notes in the MIT Distributed Systems course . For Designing Data Intensive Applications (DDIA), I strongly encourage you to find buddies at work or online who will read it through with you. You can also always join the Software Internals Discord 's #distsys channel to ask questions as you go. But it's still best if you have some partners to go through the book with, even if they are as new to it as you. I also used to think that you might want to wait a few years into your career before reading DDIA but when you have friends to read it with I think you need not wait. If you have only skimmed the book you should definitely go back and give it a thorough read. I have read it three times already and I will read it again as part of the Software Internals Book Club next year after the 2nd Edition is published. Keep in mind that every chapter of DDIA provides references to papers you can keep reading should you end up memorizing DDIA itself. When you've read parts of DDIA or the MIT Distributed Systems course and you want practice, the Fly.io x Jepsen Distributed Systems Challenge is one guided option. Other options might include simply implementing (getting progressively more complex down the list): And if you get bored there you can see Alex Miller's Data Replication Design Spectrum for more ideas and variants. And if you want more people to follow, check out the Distributed Systems section of my favorite blogs page. If these projects and papers sound arcane or intimidating, know that you will see the problems these projects/papers solve whether or not you know and understand these solutions. Developers often end up reinventing hacky versions of these which are more likely to have subtle bugs. While instead you can recognize and use one of these well-known building blocks. Or at least have the background to better reason about correctness should you be in a situation where you must work with a novel distributed system or you end up designing a new one yourself. And again, if you want folks to bounce ideas off of or ask questions to, I strongly encourage you to join the Software Internals Discord and ask there! I wrote a short post on learning the fundamentals of distributed systems, with a few suggested resources to read and a few suggested projects to try. pic.twitter.com/b0EhDP8K0t two-phase commit three-phase commit single-decree Paxos highly available key-value store on top of a 3rd-party consensus library chain replication (or CRAQ), using a 3rd-party consensus library

0 views
Jefferson Heard 2 months ago

How I design backend services

First off, this is gonna be a long article. Strap in... I'm old, I get it. For the majority of my career, MVC was the way to design web applications. But I read a book about Evolutionary System Design and went to an O'Reilly conference on system architecture in 2018 and saw Martin Fowler talk on the " Microservice Design Canvas ," and the two things together completely changed my thinking on how to design systems. I've really fused these ideas to form my own theory about backend systems design. My goal is to create systems that are: The microservice design canvas has you design commands, queries, publications, and subscriptions, then data. I go a little further, because I try to take deployment, scaling aspects, and security into account at design time. Also I don't take a "microservices first" approach. In my experience the amount of work and money it takes to instrument and monitor microservices really just doesn't pay off for young companies with few or zero customers (where practically every system starts). I make it so it's easy to refactor pieces into microservices, but there's no requirement for the system to start there. There's also no requirement that you start with streaming or event sourcing solutions. This model works for traditional web APIs and for all the more advanced stuff that you end up needing when you finally do scale to hundreds of thousands to tens of millions of active daily users. All code examples in this post will be in Python, but truthfully this could work in anything from Java to Go to Typescript to LISP. My personal preferred core stack is: This is the order you want to do the design in. MVC runs deep in development culture, and most developers will try to start any backend project in the ORM. This is a mistake, as I've pointed out before. If you do that, then you will end up adding elements that serve no purpose in the final version of the service, and you'll end up creating data structures that aren't conducive to the efficient working of your interface. Start with the interface layer. Then hit the data layer. The data layer gives you your options for auth implementation, so then do that. All those tell you enough about your environment and infrastructure requirements that you can design those. And it's not that you won't or can't skip around a bit, but in general these things will resolve themselves fully in this order. Typical examples of service design start with a TODO list, but I'm going to start with a simple group calendar, because there are more moving parts that illustrate more than the trivial TODO list, and you will be able to see how much this model simplifies a service that's naturally a little more complex. This will be short, but I want to set the stage for the decisions I'm making and how I'm breaking it down. I'm treating this like a small service. User management is expected to happen elsewhere. This service will be called (indirectly) by a UI and by directly by other services that serve up the UI and broker scheduling information with other parts of the company's SaaS offering. It is an MVP, so it's not expected to handle calendar slots or anything "Calendly" like. It's just a barebones store of appointment data. Since user management is separate, the appointments can be owned by users, groups, or rooms or anything, but we don't care because that's all handled (presumably) by the user, group, and resource service. What do my customers care about for V1? What does my customer success team care about for V1? What do my product people care about for V1? What are the (non-universal) engineering requirements to make this system flow well? So basically my requirements are: There are also a few affordances that make my job easier: First a link to the post where I go into detail about Commands: Just to sum it up, your command and queries should be governed by base classes that provide the "guts" of their mechanics, and serve as suitable points to enhance those gut mechanics. The main difference between Commands and Queries is that the Query classes should only have access to a read-only connection into the data. But if for example, you want an audit trail of every command or query issued against the system, or if you want a total ordering or the ability to switch to event sourcing, the base classes are where you do it. I typically write bulk mutations first, because there are just so many cases where people want to import data and bulk mutations typically have more efficient implementation options than fanning out atomic ones. For our calendar, here is a list of commands: We want a small set of queries we can optimize our data structures for, but that will suffice to construct any kind of calendar for the UI, and to aid users in creating conflict-free appointments. Our publications: Our subscriptions: Our schedules: Now this gets interesting. Because we've already designed our commands there's a lot more to our data model than we first thought, as well as a lot fewer data attributes in the core model. We want as few attributes as possible. Online database migrations are pretty rock-solid these days, so accidentally getting into production without something that you want for a future release won't be a problem. Also, it's clear from above that we don't just want Postgres for our data model. We're searching appointments, so we want OpenSearch, and for simplicity's sake I'm assuming we're using Redis for pub-sub, and FastStream , which is a FastAPI like framework for streaming applications. You could use Kafka, SQS/SNS, or RabbitMQ, depending on your scale or your dev and SRE teams' proficiency. Now that we know how people want from our service, we can define our data and infrastructure, and points-of-scale in our code around it. We're going to use Postgres to store our appointment information. I'm not going to go into deep detail here about the fields, since those can be derived from the Command classes and the UI requirements but as someone who has designed more than one calendar in my lifetime, I have some notes: Now, let's talk data models. Recurrences will be stored separately from appointments, and each appointment within the recurrence will be an actual appointment record within the database. We'll add a new scheduled task, UpdateOccurrences, which will run once a month and calculate occurrences out for 2 years (an implementation note to tell Product and CS about). The same code should be used when saving an appointment so that occurrences are saved that far out on initial appointment creation. We'll want to set a field on our Recurrence model to say how far occurrences have been calculated. That way if someone modifies or deletes an occurrence from the set, we won't accidentally re-create it when we call UpdateOccurrences. Along with the Postgres record, we're going to want to index title, description, attendees, is external, and owning-application within OpenSearch. I won't bore you with the details of indexing these correctly because the requirements change a lot depending on the locales you operate in and tokenizers you choose. Also, you'll end up needing to query the service that expands attendee IDs into names most likely or code the search function to call it to reify full text searches to possible attendee IDs. This latter idea may be a little better for MVP, since it won't require you to setup an async task and broker to save appointments. How about that audit trail? Well you'll notice conveniently if you're using FastAPI and a pattern like my Command class that all commands are Events, and publications are also events. We can dump those events into Redshift or BigQuery and boom, our audit trail is now real. We can use the event history to cover the CS case of recreating changes in the event a bug or a person someone screws up someone's calendar. We can use the same audit trail to figure out how many appointments were created by whom for engagement metrics. And we can use the audit trail along with any logging we do in DataDog to measure our service against our performance and reliability metrics. The other great thing about everything being an Event is that we can adapt our commands to a Streaming modality easily once we get to the point where we have to scale out. Dump the command event into Kafka and consume it with FastStream. We're using an external service for all our user info, and we're proxying this service through our app-facing and user APIs, so presumably authentication is handled for us. Same with the security environment. All that is probably handled upstream. The only extra requirements we really have here is authorization. We want to allow our customers to make private appointments. That's easy, we can add that as a simple flag. But we probably also need to keep track of who can see who's calendar. I personally love AuthZed's (I swear they don't sponsor me, I just think it's a great tool) open source SpiceDB permission service. It's an implementation of Google's Zanzibar standard, which handles all the permissions storage and checks throughout Google, so you know it can handle your use case. So I'm going to suggest these new permissions in authzed without going into further implementation details: In each command I will make sure that the calendar can be modified, and in each query I will make sure that the response will be viewable to the requesting resource or person, and whether it should just be empty appointments with busy labels. Maybe this belongs above in Data and Infrastructure, but I want to treat this section separately. I prefer to use Datadog personally, but it can be quite expensive. However you create your dashboard and whatever you log data into, you want to measure, at the very least: Cost effective to develop. Easy for new developers to drop into. Performant enough to be cost effective to scale. Easy to break up into microservices or other components to support horizontal scaling as required. Easily "composable," i.e. big things don't require new code so much as aggregates of small things. Easy to monitor, react to problems, and to debug. Easy to add nontrivial things to on a time budget without creating new technical debt, or at least keeping it local to the thing added. CDK or Terraform/Helm AuthZed for permissions Creating, editing, and deleting appointments. Recurrence. Being able to see whether a new appointment will cause a conflict in their own or other attendees calendars, and what those conflicts are. When scheduling multiple people, they want to see the conflict-free openings in the calendar before having to click through weeks of people's calendars to find a time. Being able to view their calendar in various ways, up to a year at a time. Having all their calendar items together. All apps in the company should dump appointment data into this if they are creating it, and I should be able to load outlook and google calendars as well. Average calendar load time for a single person's calendar should be 2sec or less on good internet. Average calendar load time for a month of appointments up to 50 people should be 30 seconds or less. To be able to set 0 or more configurable reminders by email, SMS, or push. If an appointment disappears or moves in the calendar, they want to know how that happened - what app moved it and who, so that if a customer thinks it's a bug they can prove it one way or another and take the appropriate action, and we should be able to restore the old appointment. Engagement: how many calendar views across which people per day, and any appointment metadata that would be appropriate as classifiers distinguishing between the type of appointments people are adding or how many recurring vs. individual, etc. And how many appointments added / removed per day by people (as opposed to external services like Google and Outlook) There are other apps in my company with calendars. I need to make sure that appointments managed by those apps stay managed by those apps if there's metadata that my service cannot account for directly. timezone-aware timespans, including recurrence rules links and attachments appointment metadata such as originator, attendees, title and description some way to refer to an external owner of the appointment, for appts coming from services like Outlook and Google and for internal owners understanding which app made it and whether it has to be managed by that app (such as if the app is storing other metadata on the record that would be corrupted if a different app edited it. An audit trail of some sort to serve both the CS and Product use-cases. Restoring appointments can be manual by CS or engineering, but only if the audit trail exists with sufficient data on it. On average, a person will not have more than 4-5 appointments a day, 5 days a week. That totals out to 1,300 appts per person per year that we have to save, load, or destroy. It's likely I can do this much bulk writing in a single transaction and stay within the time limits we've imposed on ourselves. Across all our services, there are really no more than 150,000 daily active users, and of those, we can estimate that they're only changing their calendars a couple of times a day at most. That means that the write traffic, at least at first, will be fairly low. I can likely get to our MVP without pushing the writes to an async worker, although it's likely something we're going to want as our app grows. Traffic will be bursty, but within limits. When new users are added or when they sign up to sync their calendar from Outlook or Google, we're going to see a spike in traffic from baseline. This can likely be handled by combining FastAPI BackgroundTasks (less clunky than a queueing system and async workers for infrequent bursty traffic) with the kubernetes autoscaler, at least at first. SetOpenHours (rrule) - Allows someone to set the time of day where someone else is allowed to schedule meetings including them. Like work hours. SaveAppointments - Bulk creation and update of appointment data. DeleteAppointments - Bulk deletion of appointment data. Cannot delete from external services or app-management-required appointments. CheckForConflicts (timespan, attendees, limit, cursor) - Return a list of people that have conflicts for a given timespan. FindOpenings (timespan, attendees, limit, cursor) - Return a list of timespans within the input timespan where the attendees have no conflicts. GetAppointments (timespan, attendees, limit, cursor) - Return a list of appointments, sorted by attendee, and then by timespan. SearchAppointments (search_criteria, sort_criteria, timespan, limit, cursor) - Return a list of appointments that meet a certain set of search criteria, which can be one or more of: Title Description Is External Is Owned By (app) AppointmentCreated( id, owner, attendees, timespan) - lets subscribed services know when someone's calendar appointment was created. AppointmentModified (id, owner, attendees, timespan) - lets subscribed services know when someone's calendar appointment was moved. AppointmentDeleted (id, owner, attendees, timespan) - lets subscribed services know when someone's calendar appointment was deleted. AppointmentReminderSentReceived (id, owner, attendees, scheduled_time, actual_time) - lets subscribed services know when a reminder was sent out and received by the given attendees. AppointmentReminderSentFailed (id, owner, attendees, scheduled_time, code, reason) - lets subscribed services know when a reminder was sent but failed to be received. ExternalCalendarsBroker - Trust me, use a 3rd party service for this. I'm going to suggest either Cronofy or Nylas's APIs for this. But be aware of and put in extra time to design for external system failures and hangups. Your users will judge discrepancies between their Google and Outlook calendars vs. yours harshly and you want to be able to explain those differences when they happen and have something to push support on at the service you do choose when there are issues. SendReminders - Sends reminders out to attendees. Will run once a minute to check if there are reminders to send and then send them. This may actually be broken up into a couple of scheduled tasks, depending on how many reminders you're sending out per minute, and how long they take to process. There is a fair amount of subtlety in sending reminders when people are allowed to change appointments up to the last minute. You're going to want to define a "fitness function" for how often reminders can fail, and how often reminders for deleted appointments can be sent out, and use that to determine how fiddly you want to be here. Indexing Timespans - Postgres has an interval type, and that interval can be indexed with a GIST index. This gives us an optimal way to search for appointments with overlapping intervals and covering single points in time. Indexing Attendees - Likewise an array type with a GIN index will give us the ability to search for all appointments that include a given set of attendees. We may need a CTE to deal with set math, but it will still be highly optimized and relatively straightforward. Timezones - Store the timezone as an ISO code (not an offset) on a separate field and store the actual interval in UTC. If you don't store all your time info in the same timezone then you can't effectively index the timespan of an appointment. Okay, you can but your indexing trigger starts to look complicated and you're saving nothing in terms of complexity in the timespan itself. Why use a code and not an offset? Because if someone moves the appointment and it crosses a DST boundary when they do so, you won't know to change the offset without the UI's help, making the interaction between your service and others need more documentation and making it more error prone. Recurrence Info - God save you and your users, read the RRULE standard closely, and in particular read the implementation notes for the most popular Javascript library and whatever your backend implementation language is. Better yet, this is one of those rare places where I'd advise you roll your own if you have time , rather than use the widely accepted standard, because the standard is SO loose, and because the different implementations out there often skirt different sets of details in it. But if you use RRULE, one big non-obvious detail you need to trust me on: store the RRULE itself in the local time and timezone that the user used to create the recurring appointment. If you don't, day-of-week calculations will be thrown off depending on how far away from the UTC timezone someone is, and how close to midnight their appointment starts. It's not that you can't correct for it, but one way lies 2400 lines of madness and bugs and the other way lies a different but far simpler type of madness. CanViewCalendarDetails - Whether or not the caller can view a given attendee's calendar (group, person, whatever – the perm services can handle this) CanModifyCalendar - Whether or not the caller can modify a given attendee's calendar. CanViewCalendarFreeBusy - Whether or not the caller can view free/busy info for a given attendee, even if they can't view the full calendar. p50 and P95 time to execute a save or delete appointment command. Alert if above 75-90% of the threshold determined by the users and schedule time to get it down if alerts are becoming more consistent. P50 and P95 times for each query. There are lots of ways to increase the performance. Sharding OpenSearch indexes, Postgres partitioning, tuning queries, storing the appointment data in multiple redundant tables with different indexing schemes tuned to each specific query, and caching the results of calls to other services or whole calendar responses. Failure rates - you want to alert on spikes in these. 404s, 401s, and 403s are generally problems downstream from you. They could indicate a broken UI or a misconfigured service. 400s could be a failure of coordination between you and your consumers, where you've released a breaking change without telling someone. 500 is definitely on you. 502 and 503 mean your services aren't scaling to meet depend. Track spikes and hourly failure rates over time. If the failure rates are increasing, then you should schedule maintenance time on it before the rates spill over your threshold. The key to good stewardship of engineering is proactivity. If you catch a failure before your customers do, you're doing well.

0 views
Loren Stewart 2 months ago

LLM Tools: Real-time LLM Responses in Production (Part 4)

Build streaming LLM responses with Server-Sent Events, request cancellation, and real-time status updates to create engaging, responsive AI applications.

0 views
Florian Zenker 3 months ago

Using Automated Refactoring Tools in Anger

Go supports functions as a first class concept and it's pretty common to pass a function to inject custom behavior. This can have a performance impact. In the ideal case, like in the example below, the compiler can inline everything, resulting in optimal performance: The loop in is inlined into , as is the function we're passing in as a parameter. The resulting code in the loop is free of function calls (no instruction): However, in many cases, where the function that's called can't be inlined because it's too complex (or if we disallow the compiler to inline) Go needs to make an indirect function call from within the loop. Let's use to forbid the compiler from inlining and compare the generated assembly code: In this case, the loop is not inlined into and we clearly see an indirect function call. The call instruction is the function call, and it's indirect because it's using a register ( ) as an argument instead of a label. In many cases, the difference between the two is unnoticeable. It would definitely not be the right decision to always inline this function, and if the function is truly hot, Go may still inline it when using profile guided optimization . I ran into this while investigating optimization opportunities for znkr.io/diff . I had started out with a generic implementation of the diffing algorithm that works for any type, using a signature like: However, the implementation of diffing algorithms is rather complex and so the function was never inlined. After a number of rounds of optimizations, I ended up with a changed API and an implementation that collapsed all diffs to using the algorithm on an type 1 . The problem was that the function still used an underlying algorithm for and supplied an function that was never inlined. My hypothesis was that I could improve the performance by making sure that the function was inlined. However, I didn't know how to do that. I tried to validate the hypothesis with a simple hack that duplicated the implementation and specialized it by hand. To my surprise, that hack resulted in a runtime reduction by up to -40% . So clearly, that's a worthwhile optimization! Unfortunately, the diff implementation is a bit too complicated to be a good example for this blog post. Instead, let's use a very simple (but inefficient) Quick Sort implementation (which incidentally has a similar recursive structure as the diff implementation I am using): Manually specializing this function for is straightforward: Comparing the runtime of these two algorithms using a simple benchmark (sorting slices with 100 random numbers) on my M1 MacBook Pro also shows a runtime improvement of -40%: I tried a number of ideas but found no way to ensure the passed-in function was inlined. The alternative of maintaining two copies of the same algorithm didn't sound very appealing. It felt like I had to either reduce the API surface by removing the option, take a performance hit to have both, or maintain two versions of the same algorithm. I really wished for specialization that would allow me to write the algorithm once and change aspects of it for types. I liked none of these options, and I was despairing about which one to pick when it hit me: I could implement specialization myself! Go has excellent support for refactoring Go code thanks to the go/ast package. We can use that to build a code generator that performs the manual specialization described above automatically: This is overkill for a simple function like , but keep in mind that this is a simple example by construction. The same principle can be applied to much more complicated functions or even, as in the case that triggered me to do this, whole types with multiple functions. It's also fairly trivial to hook the specialization into a unit test to validate that the specialized version matches the output of the generator and use to regenerate the specialized version. The full version of what I ended up using for the diffing algorithm is here . I found a way to specialize functionality in a way that doesn't require manual maintenance. Is it a good idea, though? I don't really know, but I can drop the specialized version for a performance hit with only a few lines of code. If this turns out to be a bad idea, or if I or someone else finds a better solution, it's quite easily removed or replaced. The details don't matter here, but the idea is to assign a unique integer to every line in both inputs. This happens almost naturally when reducing the problem size by removing every line unique to either input. Both together significantly speed up a number of diffs.  ↩︎

0 views
Fakeman Show 3 months ago

Lessons learned by building my own cookiecutter for REST APIs

During my university days, I avoided building CRUD apps, not because I couldn’t, but because they felt boring. To me, it was just a client (web or mobile) talking to a server that ran some SQL queries. I dodged them in every project I could. Fast forward a three years, after graduating and working at Oracle, I’ve realized that almost everything in software is just a fancy CRUD app. And you know what?

0 views
Loren Stewart 4 months ago

Beyond REST: Adding Real-Time Interactivity with NestJS WebSockets

For decades, the request-response model of REST has been the backbone of the web. But what happens when the server needs to initiate a conversation? Discover how NestJS WebSockets enable real-time interactivity.

0 views
Corrode 4 months ago

Tembo

Recently I was in need of a simple job queue for a Rust project. I already had Postgres in place and wondered if I could reuse it for this purpose. I found PGMQ by Tembo . PGMQ a simple job queue written in Rust that uses Postgres as a backend. It fit the bill perfectly. In today’s episode, I talk to Adam Hendel , the founding engineer of Tembo, about one of their projects, PGMQ, and how it came to be. We discuss the design decisions behind job queues, interfacing from Rust to Postgres, and the engineering decisions that went into building the extension. It was delightful to hear that you could build all of this yourself, but that you would probably just waste your time doing so and would come up with the same design decisions as Adam and the team. CodeCrafters helps you become proficient in Rust by building real-world, production-grade projects. Learn hands-on by creating your own shell, HTTP server, Redis, Kafka, Git, SQLite, or DNS service from scratch. Start for free today and enjoy 40% off any paid plan by using this link . Tembo builds developer tools that help teams build and ship software faster. Their first product, PGMQ , was created to solve the problem of job queues in a simple and efficient way, leveraging the power of Postgres. They since made a pivot to focus on AI-driven code assistance, but PGMQ can be used independently and is available as an open-source project. Adam Hendel is the founding engineer at Tembo, where he has been instrumental in developing PGMQ and other tools like pg_vectorize . He has since moved on to work on his own startup, but remains involved with the PGMQ project.

0 views
James Stanley 4 months ago

Against exponential backoff

When software uses some external dependency, like an HTTP service, it sometimes has to handle failures. Sometimes people choose exponential backoff : after each failure, you wait longer before retrying, for example by doubling the delay each time. In this post I'm going to argue that exponential backoff is a bad idea. I know why you are tempted by exponential backoff. It sounds clever and appealing. If your dependency is down for N minutes then you only make O(log N) attempts to contact it. Everybody loves O(log N) ! What's not to like? But I think you shouldn't do it, for these reasons: 1. it wastes O(N) time If the service is down for an hour and you double your sleep on every failure then you could sleep for another hour while your dependency is actually already working. Your dependency is down for 1 hour, your service is down for up to 2 hours. Is that what you want? That is not a good experience. Nobody would choose that. By choosing exponential backoff, you're choosing that. 2. it makes debugging difficult Let's say you get in to work and there's an outage of your important Fooservice . You work out that the root cause of your outage is that Barservice was down. You fix Barservice . (Let's be honest, you simply restart Barservice and it mysteriously starts working again. Whatever, doesn't matter). But your customer-facing, actually-money-making, super-important end-user product Fooservice still isn't working! What's going on? It turns out that Fooservice is stuck in sleep(1800) for the next 25 minutes. It hasn't even tried to see if Barservice is back. Is that what you want? What do you do next? Of course you restart Fooservice because you know Barservice is back and you want Fooservice to start working again. When you're looking at it , your revealed preference is for the service to try again much sooner than in 25 minutes' time. But if Fooservice wasn't using exponential backoff it would have already started working again on its own. 3. it composes geometrically If Fooservice depends on Barservice depends on Bazservice , and Bazservice is down for half an hour, then Barservice might be down for an hour and Fooservice might be down for 2 hours! Is that what you want? 4. compute is cheap If you have no delay between retries then yeah you max out a CPU core on your machine and you hammer the remote service. But compute is cheap enough that if you wait 1 second between retries then it probably won't be a problem. If it is a problem then pick a number larger than 1. Say, your end of the request takes 9 seconds of CPU time to initialise, so if you only sleep 1 second every time then you still have a 90% duty cycle on CPU usage which is unacceptable. Sleep more than 1 second! I don't care. Just don't let it keep growing. Pick an allowable duty cycle, set your sleep accordingly, don't let it grow. If you really must... If you really must use exponential backoff then let's use a bounded exponential backoff. Instead of doubling forever, pick a (low!) upper limit on the maximum time you will sleep. Ideally your maximum sleep should be almost imperceptible at human time scales. You should be able to fix whatever dependency is broken, and then your dependent service should be working again faster than you can find out that it's not. Don't make it possible for your program to sleep for an hour just because a dependency has been down for an hour. Make your program cap out at 10 second sleeps or something. Please. The downfall of civilisation Btw I think we're living through the downfall of civilisation. It seems like every day stuff gets broken faster than it gets repaired. OK, it doesn't seem like it's changing that much from day to day, but Rome didn't fall in a day. What can we do about it? Probably nothing. But we can at least prolong it by eschewing exponential backoff in favour of constant backoff. Thanks for listening to my TED talk. And remember: friends don't let friends implement exponential backoff.

0 views

urllib3 Origin Story

This post was featured on opensource.org/maintainers/shazow My first big open source project was urllib3 . Today it’s used by almost every Python user and receives about a billion downloads each month, but it started in 2007 out of necessity. I was working at TinEye (formerly known as Idée Inc.) as my first “real” job out of university, and we needed to upload billions of images to Amazon S3. I wrote a script to get processing and estimated how long it would take to finish… two months! Turns out in 2007, HTTP libraries weren’t reusing sockets or connection pooling, weren’t thread-safe, didn’t fully support multipart encoding, didn’t know about resuming or retries or redirecting, and much more that we take for granted today. It took me about a week to write the first version of what ultimately became urllib3, along with workerpool for managing concurrent jobs in Python, and roughly one more week to do the entire S3 upload using these new tools. A month and a half ahead of schedule, and we became one of Amazon’s biggest S3 customers at the time. I was pleased with my work. I was just months into my role at TinEye and I already had a material impact. Reflecting on this time almost two decades later, I realized that doing a good job at work was not what created the real impact. There are many people out there who are smarter and work harder who move the needle further at their jobs than I ever did. The real impact of my work was realized when I asked my boss and co-founder of TinEye, Paul Bloore, if I could open source urllib3 under my own name with a permissive MIT license, and Paul said yes. I did not realize at the time how generous and rare this was, but I learned later after having worked with many companies who fought tooth and nail to retain and control every morsel of intellectual property they could get their hands on. It’s one thing to write high impact code that helps ourselves or our employer, but it’s another thing to unlock it so that it can help millions of other people and organizations too. Choosing a permissive open source license like MIT made Paul’s decision easy: There was no liability or threat to the company. TinEye had all the same rights to the code as I did or any other contributor did. In fact, Paul allowed me to continue improving it while I worked there because it benefited TinEye as much as it benefited everyone else. Releasing urllib3 under my own name allowed me to continue maintaining and improving the project even after leaving, because it was not locked under my employer’s namespace and I felt more ownership over the project. Hundreds of contributors started streaming in, too. Nobody loves maintaining a fork if they don’t have to, so it’s rational to report bugs upstream and supply improvements if we have them. The growth of urllib3 since the first release in 2008 has been a complicated journey. Today, my role is more of a meta-maintainer where I support our active maintainers (thank you Seth M. Larson, Quentin Pradet, Illia Volochii!) while allowing people to transition into alumni maintainers over time as life circumstances change. It’s important to remember that while funding open source is very important and impactful ( please consider supporting urllib3 ), it’s not always about money. People don’t want to work on one thing their whole life, so we have to allow for transition and succession. I learned many lessons from my first big open source project, and I continue to apply them to all of my projects since then with great success. I hope you’ll join along!

0 views
Steve Klabnik 5 months ago

Thoughts on Bluesky Verification

Today, Bluesky rolled out blue checks . And, I was given one. Now, I’m not the biggest fan of this sort of feature, however, I also don’t really think this kind of feature is for me. I really like the idea of domain verification; I have been like “oh okay that’s coming from a account” from time to time. But I don’t think most people really think about domains the way that programmers do. I have wanted to update my “How does Bluesky work?” post for a while now, but I’ve been super busy. So, let’s ignore the product question for now, and focus on the technical question. How does verification work? I’ve also been thinking about designing apps on top of atproto. There’s a kind of rule of thumb that I realized about doing this, a sort of “golden rule” if you will: You are the only person who can write records into your PDS. This has really interesting implications! For example, sometimes people ask for “soft blocks” on Bluesky. This is a twitter ‘feature’ where if you block someone, and then unblock them, they’re not following you any more. But on Bluesky, if you soft block someone, they’re still following you. Why does that happen? Following someone on Bluesky means that you write a record of the lexicon into your PDS. So you might assume that blocking someone would delete this record from their PDS. But that would violate the golden rule! You can’t delete records from someone else’s PDS. So, instead, blocking someone on Bluesky means that you write a record of the lexicon into your PDS. Unblocking them deletes that record from your PDS. But it doesn’t delete the record from their PDS. So, if you block someone, and then unblock them, they still have a record in their PDS that says they’re following you. This may not be the behavior that you want as a user, but it follows from the constraints of the overall protocol design. Now that we know the golden rule, we can understand how verification works. How does an account get verified? Well, someone has to be writing a record into a PDS somewhere. The natural design is the one they’ve chosen: someone becomes verified by someone else writing a record of the type into their PDS. Here is the record verifying me. It looks like this: This is in ’s PDS. It describes my DID, and my handle and display name. One thing not mentioned in the blog post, but is in the comments of the lexicon, is that changing your handle or display name makes this record invalid. I didn’t know that! Good to remember in case I decide to make some jokes in my display name. That’s the basics: a record exists, and a blue check shows up on my account. But wait, isn’t this centralized? I thought Bluesky was decentralized? Well, yes and no. You see, anyone can put a record of this type into their PDS. So, if I wanted to verify someone else, I can do that too: here’s me verifying . But if you go to Jerry’s profile, you won’t see a blue check. Why is that? Well, because the Bluesky AppView only shows a blue check if the record is from a “trusted verifier.” Who is that? Well, as far as we know, it’s from and . Why can’t we tell more? Well, they’ve implemented this as a database column in the AppView, it’s not “in-protocol,” in other words. EDIT: Samuel has provided us with the list: Replying to an unknown post @bsky.app @nytimes.com @wired.com @theathletic.bsky.social 26 10 Read 2 replies on Bluesky So in some sense this is a centralized feature: clients can display information however they choose, and they’ve made a product choice that only trusted verifiers will show up as blue checks. But in another sense, it’s not centralized, as anyone has the power to verify anyone else. Alternative clients can decide to show or hide any of this information; Deer Social , one of these alternate clients, has added a user preference to hide all blue checks, for example. A different one could show every verification as a blue check, or display them differently, say one badge per verification, styled like the avatar of the person who verified you. EDIT: turns out that the default AppView also lets you hide blue checks: Steve Klabnik @steveklabnik.com · Apr 21 Thoughts on Bluesky verification steveklabnik.com/writing/thou... Thoughts on Bluesky Verification steveklabnik.com jolheiser @jolheiser.com This was very informative, thanks for the write-up! I was curious about being able to hide the checkmarks, but unless I'm mistaken this appears to also be an option on the bluesky appview. At least I was able to check the preference in my settings->moderation->verification settings 2 0 Read 1 reply on Bluesky The point is still the same though: there’s choice here. Back to the original post! The underlying protocol is totally open, but you can make an argument that most users will use the main client, and therefore, in practice it’s more centralized than not. My mental model of it is the bundled list of root CAs that browsers ship; in theory, DNS is completely decentralized, but in practice, a few root CAs are more trusted than others. It’s sort of similar here, except there’s no real “delegation” involved: verifiers are peers, not a tree. This core design allows Bluesky to adjust the way it works in the future fairly easily, they could allow you to decide who to trust, for example. We’ll see how this feature evolves over time. I’m flattered that I was chosen to be verified; if I was trying to ship this feature, my name wouldn’t come up 181st on the list. I was really curious about the design when they announced this feature, I assumed it was going to be closer to a PGP web of trust, or delegated in some way. This design is much simpler and less centralized than I expected: this is arguably more horizontal than the web of trust would have been. At the same time, I am interested in seeing if this changes the culture of the site; some see the blue check as a status symbol of some kind, and think it means what you say matters more. I don’t think that’s true, personally, but it doesn’t really matter what I think. To redraw the analogy with DNS, the blue check is very similar to the green lock: it doesn’t mean that what you’re saying is true, or right, or good, it just means that you are who you say you are. But even though a blue check is technically isomorphic (or close enough) to a green lock, I think a lot of people perceive it as being more than that. We’ll just have to see. What I am glad about it is that it’s an in-protocol design, rather than an external one like DMs are. I understand why they did it that way, but I’d rather that remain the exception rather than the rule. Here’s my post about this post on BlueSky: Thoughts on Bluesky verification steveklabnik.com/writing/thou... Thoughts on Bluesky Verification

0 views