76 pointsby remyduthu19 hours ago31 comments
  • scribu18 hours ago
    > Awaiting a coroutine does not give control back to the event loop.

    I think this is a subtler point than one might think on first read, which is muddled due to the poorly chosen examples.

    Here's a better illustration:

      import asyncio
      
      async def child():
          print("child start")
          await asyncio.sleep(0)
          print("child end")
      
      async def parent():
          print("parent before")
          await child()        # <-- awaiting a coroutine (not a task)
          print("parent after")
      
      async def other():
          for _ in range(5):
              print("other")
              await asyncio.sleep(0)
      
      async def main():
          other_task = asyncio.create_task(other())
          parent_task = asyncio.create_task(parent())
          await asyncio.gather(other_task, parent_task)
          
      asyncio.run(main())
    
    
    It prints:

      other
      parent before
      child start
      other
      child end
      parent after
      other
      other
      other
    
    So the author's point is that "other" can never appear in-between "parent before" and "child start".

    Edit: clarification

    • titanomachy18 hours ago
      Thank you!! The examples in the post illustrated nothing, it was driving me crazy.
      • cuu50817 hours ago
        Yes, the examples were sloppy.
    • raincole18 hours ago
      > So the author's point is that "other" can never appear in-between "parent before" and "child start".

      But isn't it true for JavaScript too? So I don't really get the author's point... am I missing something or the author('s LLM?) forced a moot comparison to JavaScript?

      Edit: after reading the examples twice I am 99.9% sure it's slop and flagged it.

      Edit2: another article from the same author: https://mergify.com/blog/why-warning-has-no-place-in-modern-...

      > This isn’t just text — it’s structured, filterable, and actionable.

      My conclusion is that I should ask LLM to write a browser userscript to automatically flag and hide links from this domain for me.

      • scribu17 hours ago
        > But isn't it true for JavaScript too?

        You're right, the equivalent JS script produces the same sequence of outputs.

        It turns out there is a way to emulate Python's asyncio.create_task().

        Python:

          await asyncio.create_task(child())
        
        JavaScript:

          const childTask = new Promise((resolve) => {
            setTimeout(() => child().then(resolve), 0)
          })
          await childTask
      • rjmill16 hours ago
        > But isn't it true for JavaScript too?

        I don't think so. It's been a while since I've bled on tricky async problems in either language, but I'm pretty sure in JS it would be

          [...]
          parent_before
          parent_after
          child_before
          [...]
        
        In JS, there are microtasks and macrotasks. setTimeout creates macrotasks. `.then` (and therefore `await`) creates microtasks.

        Microtasks get executed BEFORE macrotasks, but they still get executed AFTER the current call stack is completed.

        From OP (and better illustrated by GP's example) Python's surprise is that it's just putting the awaited coroutine into the current call stack. So `await` doesn't guarantee anything is going into a task queue (micro or macro) in python.

        • fluoridation16 hours ago
          >I'm pretty sure in JS it would be [...]

          That doesn't make sense. That would mean the awaiting function doesn't have access to the result of the Promise (since it can proceed before the Promise is fulfilled), which would break the entire point of promises.

        • raincole16 hours ago
          > Microtasks get executed BEFORE macrotasks

          Correct.

          > they still get executed AFTER the current call stack is completed.

          Correct.

          > I'm pretty sure in JS it would be [...]

          Your understanding of JS event loop is correct but you reached the wrong conclusion.

      • furyofantares17 hours ago
        Yep, it's another slop. We are getting these about daily now where there's lots of comments on articles that'd are clearly slop.

        Half the article is paragraph headings, the other half is bullet points or numbered lists, if there was anything interesting in the prompt it'd been erased by an LLM which has turned it into an infodump with no perspective, nothing to convey, and I have no ability to tell what if anything might have been important to the author (besides blog clicks and maybe the title).

        I really wish we could start recognizing these sooner, I think too many people skim and then go to the comments section but I don't think we really want HN to be a place filled with low value articles just because they're good jumping off points for comments.

        I've been flagging them here and then heading over to kagi and marking as slop there. Makes me wish we had something similar here rather than just "flag".

        And I know we aren't supposed to comment when we flag, but this feels different to me, like we've got to collectively learn to notice this better or we need better tools.

    • fluoridation18 hours ago
      Doesn't this make await a no-op? In what way are async functions asynchronous if tasks do not run interleaved?
      • rcxdude17 hours ago
        They are async across operations that do 'yield', i.e. when the function eventually runs an i/o operation or sleep or similar. Those are the points where the functions can be interleaved. Simply awaiting another function is _not_ one of those points: await here only means the called function might yield to the scheduler at some point in its execution (it doesn't have to!), not that the calling function will yield immediately.
        • fluoridation17 hours ago
          Isn't asyncio.sleep one of those functions? "other" should be able to appear between "parent before" and "parent after".
          • rcxdude16 hours ago
            Yes, but not between "parent before" and "child start" (or between "child end" and "parent after")
      • throwup23817 hours ago
        Tasks are async funcs that have been spawned with asyncio.create_task or similar, which then schedules its execution. A timer of zero doesn't spawn anything so the coroutine just executes in the same frame as the caller so yes it essentially a noop.
  • reactordev18 hours ago
    This is gold.

    Here’s a horror story for you.

    A few years ago I worked for this startup as a principal engineer. The engineering manager kept touting how he was from XYZ and that he ran Pythonista meetups and how vast his knowledge of Python was. We were building a security product and needed to scan hundreds of thousands of documents quickly so we built a fan out with coroutines. I came on board well into this effort to assist in adding another adapter to this other platform that worked similarly. After seeing all the coroutines, being pickled and stored in S3, so that nodes could “resume” if they crashed yet - not a single create_task was present. All of this awaiting, pickling, attempting to resume, check, stuff, report, pickle, happened synchronously.

    When trying to point out the issue with the architecture and getting into a shouting match with Mr. Ego, I was let go.

    • jacquesm18 hours ago
      Classic. I had a similar run-in with the CTO of a company a decade or so ago who point blank refused to use version control for the source code. He insisted on having directories that got zipped and passed around from developer to developer and good luck figuring out how to integrate someone else's changes.

      I won that particular battle but it was uphill all the way, at least he had the grace to afterwards admit that he'd been an idiot.

      • bn-l18 hours ago
        That is a very different run in.
        • reactordev17 hours ago
          When you assume you know, you shut the door to the possible.

          Ego is the worst thing an engineer can possess.

        • jacquesm18 hours ago
          Ego can get in the way of progress, that's the similarity here. The OP was let go, fortunately I was not but the differences in programming environment or task have less to do with the issue here than the similarities.
  • twoodfin18 hours ago
    My New Year’s Resolution will be to give up complaining about this on hn, but for now:

    I find ChatGPT’s style and tone condescending and bland to the point of obfuscating whatever was unique, thoughtful and insightful in the original prompt.

    Trying to reverse-engineer the “Not this: That!” phrasing, artificial narrative drama & bizarre use of emphasis to recapture that insight and thought is not something I’m at all enthusiastic to do.

    Perhaps a middle ground: HN could support a “prompt” link to the actual creative seed?

    • DarkNova617 hours ago
      It’s the main reason I prefer Mistral. It has a reasonable and respectful tone.

      In contrast, ChatGPT repeatedly speaks in an authoritative tone which exceeds its own competence by an order of magnitudes.

    • xjm18 hours ago
      agreed.

      tiring.

      maybe someone will make an "article-to-prompt" sort of reverse ChatGPT?

      But of course someone already did that, and of course it's inside ChatGPT, what was I thinking? Though if I do try it, the prompt I get is not especially pleasant to read: https://chatgpt.com/share/6926f33c-8f98-8011-984e-54e49fdbb0...

    • dataflow18 hours ago
      Are you saying this was generated by ChatGPT? It didn't seem that way to me at all... what gave that away to you?
      • kokada18 hours ago
        Things like "The misconception", "The key truth", "Why ... choose ...", "Putting it all together", "Notice what didn't happen". But not just that but the general wording of this post.

        From the words of ChatGPT itself:

        > The post follows a “classic” structure: introduce a common misconception → explain what’s wrong → show concrete examples → give a clear takeaway / conclusion. The paragraphs are well balanced, each chunk delivering a precise logical step. While that’s good for clarity, it also can feel like the “standard template” many AI-based or marketing blog posts tend to follow.

        Now, if could just be a very well written blog post too. But I feel that AI just naturally converges to the same basic structure, while a human written post generally will miss one or two of those "good practices" for prose that actually ends up making the blog post more interesting (because we don't always need to have all these structures to make something enjoyable to read).

        • dataflow10 hours ago
          Interesting. I'm probably just lacking in experience with this. I see it better now that you mention it, though it's not clear to me how significant of a component AI would've been...
      • skrebbel18 hours ago
        > Putting it all together: a mental model that actually works

        dead giveaway

        • dataflow10 hours ago
          I agree that part sounds very ChatGPT-ish (and I didn't notice it before), but just one line generated by AI doesn't indicate to me the text was mostly generated by AI... does it to you? I could totally see the original text being improved by AI here and there in small pieces.
      • bn-l18 hours ago
        Are you being sarcastic? If not it’s obvious once you’ve seen it often enough.
        • dataflow10 hours ago
          No, I was serious. Perhaps I haven't seen it enough to be able to tell.
    • never_inline17 hours ago
      It is optimized for marketing speak. It's very appealing to people who 1. don't know this is slop 2. don't read for in-depth knowledge but for entertainment.

      Similarly coding is optimized for tutorial-sized code - ignoring exceptions, leaving "IN PRODUCTION DO XYZ" comments, etc...

      • twoodfin15 hours ago
        That’s what I mean by condescending. I don’t need to be sold “one neat trick to show off in code reviews”. I actually wanted to learn something about async Python!
  • jdranczewski18 hours ago
    I may be misunderstanding it, but the examples shown don't seem to be illustrative? It seems reasonably obvious that the prints will happen in this order in any language, because in example 1 we are explicitly (a)waiting for the child to finish, and in example 2 both of the parent prints are above the await. So I don't feel like either of these makes the point the author is trying to get across?
    • mikkelam18 hours ago
      I agree. If you strictly follow the syntax of "Example 1" in JavaScript (calling and awaiting on the same line), the observable output is identical to Python.

      I suppose the author meant to say that if you first called your async function and then later did `await` you would have different behavior.

  • pandaxtc18 hours ago
    Sorry if I've got this wrong, but wouldn't the first example behave the same way in Javascript as well? The function parent() "awaits" the completion of child(), so it wouldn't be possible to interleave the print statements.

    The example from this StackOverflow question might be a better demonstration: https://stackoverflow.com/q/63455683

    • zokier18 hours ago
      The whole article is bit of a mess. Like this thing:

      > My mutation block contained no awaits. The only awaits happened before acquiring the lock. Therefore:

      > * The critical section was atomic relative to the event loop.

      > * No other task could interleave inside the mutation.

      > * More locks would not increase safety.

      That is exactly the same as with e.g. JS. I'm sure there are lot of subtle differences in how Python does async vs others, but the article fails to illuminate any of it; neither the framing story nor the examples really clarify anything

    • gildas18 hours ago
      You haven't got it wrong, the first example in the article behaves the same in JS, see https://jsfiddle.net/L5w2q1p7/.
      • krackers10 hours ago
        I think in JS it's easier to see because of the correspondence between Promises and async/await.

        So in your example the behavior is much more obvious if you sort of desugar it as

          async function parent() {
              print("parent before");
              const p = child();
              await p
              print("parent after");
          }
  • mazswojejzony18 hours ago
    Maybe I'm nitpicking a bit but the second concrete example shows different flow in the parent method compared to the first one. If the second example would be a modification of the first one like shown below the output would be the same in both cases.

      async def parent():
          print("parent before")
          task = asyncio.create_task(child())   # <-- spawn a task
          await task
          print("parent after")
  • zdc118 hours ago
    Personally, I've never been able to make async work properly with Python. In Node.js I can schedule enough S3 ListBucket network requests in parallel to use 100% of my CPU core, by just mapping an array of prefixes into an array of ListBucket Promises. I can then do a Promise.all() and let them happen.

    In Python there's asyncio vs threading, and I feel there's just too much to navigate to quickly get up and running. Do people just somehow learn this just when they need it? Is anyone having fun here?

    • lillecarl18 hours ago
      You'd map to asyncio.create_task then asyncio.gather the result to fill your CPU core.
    • nikcub17 hours ago
      the trio library has an excellent tutorial that explains all of these concepts[0] even if you don't use trio and stick to the core python libs it's worth reading:

      https://trio.readthedocs.io/en/stable/tutorial.html

  • vamega18 hours ago
    The article’s understanding of Java’s Virtual Threads is incorrect. Virtual threads have a preemptive concurrency model, not a co-operative one, so suspension points are not only at calls to things like Future.get()
  • contravariant15 hours ago
    I'm confused, of course if you await immediately it's not going to have a chance to do anything else _before_ returning.

    If you do the following it works as expected

        async def child():
            print("child start")
            await asyncio.sleep(0)
            print("child end")
        
        async def parent():
            print("parent before")
            task = child()
            print("parent after")
            await task
    
    The real difference is that the coroutine is not going to do _anything_ until it is awaited, but I don't think the asyncio task is really different in a meaningful way. It's just a wrapper with an actual task manager so you can run things 'concurrently'.

    Python does have two different coroutines, but they're generators and async functions. You can go from one to the other,

  • cjauvin17 hours ago
    I asked a question about exactly this on Stack Overflow, many years ago, which I think received a nice answer: https://stackoverflow.com/questions/57966935/asyncio-task-vs...
  • chompychop18 hours ago
    Asynchronous and parallel programming are concepts I've never really learned, and admittedly scared to use, because I never really understand what the code is doing or even the right way of writing such code. Does anyone have any good resources that helped you learn this and have a good mental model of the concepts involved?
    • skinner92716 hours ago
      You’ll never learn it by reading something. You have to experience it. Get your hands dirty.

      Write a simple single-threaded http server that takes strings and hashes them with something slow like bcrypt with a high cost value (or, just sleep before returning).

      Write some integration tests (doesn’t have to be fancy) that hammer the server with 10, 100, 1000 requests.

      Time how performant (or not performant) the bank of requests are.

      Now try to write a threaded server where every request spins up a new thread.

      What’s the performance like? (Hint: learn about the global interpreter lock (GIL))

      Hmm, maybe you’re creating too many threads? Learn about thread pools and why they’re better for constraining resources.

      Is performance better? Try a multiprocessing.Pool instead to defeat the GIL.

      Want to try async? Do the same thing! But because the whole point of async is to do as much work on one thread with no idle time, and something like bcrypt is designed to hog the CPU, you’ll want to replace bcrypt with an await asyncio.sleep() to simulate something like a slow network request. If you wanted to use bcrypt in an async function, you’ll definitely want to delegate that work to a multiprocessing.Pool. Try that next.

      Learning can be that simple. Read the docs for Thread, multiprocessing, and asyncio. Python docs are usually not very long winded and most importantly they’re more correct than some random person vibe blogging.

  • tgsovlerkhgsel17 hours ago
    The main takeaway for me is that in Python, this doesn't work:

        async def somethingLongRunning():
           ...
    
        x = somethingLongRunning()
        ... other work that will take a lot of time ...
        await x  # with the expectation that this will be instant if the other work was long enough
    
    That's counterintuitive coming from other languages and seems to defeat one of the key benefits of async/await (easy writing of async operations)?

    I've seen so many scripts where tasks that should be concurrent weren't simply because the author couldn't be arsed to deal with all the boilerplate needed for async. JavaScript-style async/await solves this.

    • skinner92716 hours ago
      I think this is a better example of what, I imagine, the author was trying to illustrate.
  • xill4718 hours ago
    Both examples do the same in C#, together with explanation that couroutine is executed synchronously until hitting a real pause point. Change `asyncio.create_task` to `Task.Run` and that's exactly the same behavior in the second example as well.
  • sandblast18 hours ago
    For the JS developers: similar useful behavior (and more!) can be implemented in JS using the wonderful Effection library.

    https://effection-www.deno.dev/

    • WilcoKruijer17 hours ago
      I like the idea of using generators everywhere, since you have more control over how things execute. At the same time, your codes gets "infected" with unexpected types. In reality, this is very similar to async/await and promises, but those are used broadly in the ecosystem.
  • derangedHorse18 hours ago
    Ironically, by trying to explain awaitables in Python through comparison with other languages, the author shows how much he doesn’t understand the asynchronous models of other languages lol
  • 6r1718 hours ago
    For anyone somewhat interested in task & scheduling I highly recommend phil-op tutorial on building a kernel - there is a late branch on the github that specifically focus on asynchronous task - I implemented multi-core this week and it high a profound enlightenment on what the f* was actually going on. This is something Claude / Gemini helped me achieve so I didn't require me to spend nights on kernel debugging methodology & documentation ; Best experience I could get on async
  • pansa218 hours ago
    Is this the same as the distinction between a model in which async functions "run synchronously to the first await" vs one in which they "always yield once before executing", as discussed here?

    https://www.reddit.com/r/rust/comments/8aaywk/async_await_in...

    • dataflow17 hours ago
      I think there are at least two degrees of freedom being discussed here, though I'm quite unsure, so someone please correct me if I'm wrong:

      - What happens when MyCoroutine() is invoked (as an ordinary function invocation - no await etc.): does execution of the body start right then, or do you just get some sort of awaitable object that can be used to start it later?

      - What happens when the result of said invocation is awaited (using your language's await operator plus any extra function calls needed to begin execution): is there a forced yield to the scheduler at any point here, or do you directly start executing the body?

      The article seems to be treating the two as an indivisible pair, and thus discussing the behavior merely before and after the pair as a whole, but your link seems to be discussing the first?

      Again, I'm quite unsure, so curious if anyone has thoughts.

  • 18 hours ago
    undefined
  • meisel15 hours ago
    How many people just avoid working with await in Python at all costs (e.g., favoring something like multiprocessing instead?). It’s always felt less comfortable in Python than in other languages
  • oersted19 hours ago
    Just to note the it's the same in Rust with tokio::spawn (and alternatives).
  • jerrygenser17 hours ago
    A common use of asyncio is a server framework like fastapi that schedules tasks. I used such a framework for a while before realizing that I needed to create_task for within-task concurrency.
  • mono44217 hours ago
    I'm pretty sure that this post is not right. JS behaves the same way Python does in this example. Not sure about C#, though I suspect it is no different.
  • IgorPartola17 hours ago
    The example with parent() awaiting a child() is showing.. nothing? Maybe I am missing some subtlety but here is a JS example that is equivalent:

      async function sleep(t) {
        return new Promise((resolve) => {
          setTimeout (() => resolve(), t)
        })
      }
    
      async function child() {
        console.log("child start")
        await sleep(500)
        console.log("child end")
      }
    
      async function parent () {
        console.log("parent start")
        await child()
        console.log("parent end")
      }
    
      parent()
    
    It will print:

      parent start
      child start
      child end
      parent end
    
    Just like the Python version. This article is confusing the fact that JS has a lot more code that returns a promise than Python and thinks it means the behavior is different. It isn’t.

    You can roll your own event loop without asyncio by accumulating coroutines in Python and awaiting them in whatever order you want. There is no built-in event loop, however. You can do the same in JavaScript but there you do have a fairly complex event loop (see microtasks) as in there is no running environment without it and if you want a secondary one you have to roll it yourself.

    create_task() simply registers a coroutine with the event loop and returns a “future” which basically says “once the main event loop is done awaiting your coroutine this is the ticket to get the result/exception”. That’s the whole magic of an event loop. It is the difference between dropping off a shirt at a dry cleaner and waiting there for them to be done with it (no you aren’t doing the work but you are also not doing anything else), and dropping it off then leaving to get lunch then coming back to wait until the cleaner is done with your pickup ticket in hand (concurrency).

    But fundamentally awaiting an async function that doesn’t actually do anything async won’t give you parallelism in a single-threaded environment. More at 11.

  • anentropic18 hours ago
    I haven't done much async Python (much more gevent, years ago) but must admit I had exactly the misconception this is describing... Good to know!
  • noobcoder18 hours ago
    but how would you systematically audit a large async codebase to find all the hidden interleave points, unnecessary locks?
  • LelouBil18 hours ago
    How does this compare to Kotlin's supend and coroutines ? (which I am most familiar with)
  • CjHuber18 hours ago
    I mean of course the post does have a very valid point but it almost repeats like a mantra if there are no asyncio.create_task in your code it’s not concurrent. I mean it should probably at least mention asyncio.gather which IMO would be also much better to explain the examples with
    • zbentley18 hours ago
      You’re not wrong, but the very first lines of asyncio.gather wrap the supplied arguments with create_task if they’re not already Tasks.
  • rkangel16 hours ago
    I think this article is missing the point a bit.

    It's saying that the action of calling an async function (e.g. one you've written) isn't itself a yield point. The only yield points are places where we the call would block for external events like IO or time - `await asyncio.sleep(100)` would be one of those.

    This is true, but surely fairly irrelevant? Any async function call has somewhere in its possible call tree one of those yield points. If it didn't then it wouldn't need to be marked async.

  • zbentley17 hours ago
    This article's incomplete/flawed, I'm afraid.

    And like ... I take no pleasure in calling that out, because I have been exactly where the author is when they wrote it: dealing with reams of async code that doesn't actually make anything concurrent, droves of engineers convinced that "if my code says async/await then it's automagically performant a la Golang", and complex and buggy async control flows which all wrap synchronous, blocking operations in a threadpool at the bottom anyway.

    But it's still wrong and incomplete in several ways.

    First, it conflates task creation with deferred task start. Those two behaviors are unrelated. Calling "await asyncfunc()" spins the generator in asyncfunc(); calling "await create_task(asyncfunc())" does, too. Calling "create_task(asyncfunc())" without "await" enqueues asyncfunc() on the task list so that the event loop spins its generator next time control is returned to the loop.

    Second, as other commenters have pointed out, it mischaracterizes competing concurrency systems (Loom/C#/JS).

    Third, its catchphrase of "you must call create_task() to be concurrent" is incomplete--some very common parts of the stdlib call create_task() for you, e.g. asyncio.gather() and others. Search for "automatically scheduled as a Task" in https://docs.python.org/3/library/asyncio-task.html

    Fourth--and this seems like a nitpicky edge case but I've seen a surprising amount of code that ends up depending on it without knowing that it is--"await on coroutine doesn't suspend to the event loop" is only usually true. There are a few special non-Task awaitables that do yield back to the loop (the equivalent of process.nextTick from JavaScript).

    To illustrate this, consider the following code:

        async def sleep_loop():
            while True:
                await asyncio.sleep(1)
                print("Sleep loop")
    
        async def noop():
            return None
    
        async def main():
            asyncio.create_task(sleep_loop())
            while True:
                await noop()
    
    As written, this supports the article's first section: the code will busy-wait forever in while-True-await-noop() and never print "Sleep loop".

    Related to my first point above, if "await noop()" is replaced with "await create_task(noop())" the code will still busy loop, but will yield/nextTick-equivalent each iteration of the busy loop, so "Sleep loop" will be printed. Good so far.

    But what if "await noop()" is replaced with "await asyncio.sleep(0)"? asyncio.sleep is special: it's a regular pure-python "async def", but it uses a pair of async intrinsic behaviors (a tasks.coroutine whose body is just "yield" for sleep-0, or a asyncio.Future for sleep-nonzero). Even if the busy-wait is awaiting sleep-0 and no futures/tasks are being touched, it still yields. This special behavior confuses several of the examples in the article's code, since "await returns-right-away" and "await asyncio.sleep(0)" are not behaviorally equivalent.

    Similarly, if "await noop()" is replaced with "await asyncio.futures.Future()", the task runs. This hints at the real Python asyncio maxims (which, credit where it's due, the article gets pretty close to!):

        Async operations in Python can only interleave (and thus be concurrent) if a given coroutine's stack calls "await" on:
           1. A non-completed future.
           2. An internal intrinsic awaitable which yields to the loop.
           3. One of a few special Python function forms which are treated equivalently to the above.
         Tasks do two things:
           1. Schedule a coroutine to be "await"ed by the event loop itself when it is next yielded to.
           2. Provide a Future-based handle that can optionally be used to directly wait for that coroutine's completion when the loop runs it.
         (As underlined in the article) everything interesting with Python's async concurrency uses Tasks. 
         Wrapping Tasks are often automatically/implicitly created by the stdlib or other functions that run supplied coroutines.
  • adammarples18 hours ago
    If this

    await asyncio.sleep(0)

    doesn't yield control back to an event loop, then what the heck is it actually for?

    • drhagen18 hours ago
      It does yield control. As far as I know, that's "how you're supposed to it". But the example is not great because there is no other task available to switch to, so the event loop goes right back to where it left off. While the text says otherwise, I'm pretty sure the same thing would happen in JS, C#, and Java.
    • jaimehrubiks18 hours ago
      The article is good but the examples are not the best
  • yahoozoo18 hours ago
    [dead]