Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quick and dirty unsafe queue API #3975

Merged
merged 6 commits into from
May 30, 2024

Conversation

djspiewak
Copy link
Member

@djspiewak djspiewak commented Jan 28, 2024

Starting point for an unsafe queue API as part of std. Opening WIP PR to start the discussion.

For background… @armanbilge and I have been discussing this at length for a few months, prompted by some work that @kamilkloch was doing on high performance websocket handling. Specifically, we were looking at how to reduce the overhead associated with Dispatcher.sequential down to a minimum. In Kamil's case, there is a need for something much more bespoke than what could go into Cats Effect itself, but the line of reasoning was interesting nonetheless.

The point was made that most practical use of Dispatcher – particularly Dispatcher.sequential – is composed with Queue#offer. In other words, most practical code isn't actually running general effects, it's just putting stuff onto an asynchronous queue. While in theory the general variant of that problem still requires Dispatcher, there are special cases which can be handled with less overhead. An example of one is actually the work queue inside of Dispatcher itself, which allows for enqueue from impure code and dequeue from pure code. This is precisely the sort of machinery that most people are looking for, and they use Dispatcher + Queue to get it. We can special-case this to remove a whole layer of overhead in what is generally a fairly hot path.

The way to special case here is to make the observation that our existing async queues already allow for impure offer-ish implementations. Or more specifically, our unbounded queue allows for an impure offer (since it will never block), and our bounded queue allows for an impure tryOffer (since the queue might be full). This PR reifies this observation in a pair of traits: cats.effect.std.unsafe.BoundedQueueSink and cats.effect.std.unsafe.UnboundedQueueSink, and then implements those traits (trivially) in the existing implementations, exposing them in a pair of new constructors: Queue.unsafeBounded and Queue.unsafeUnbounded. These constructors return real Queues which can be used as normal, but with the added ability to use the appropriate offer variant impurely.

This btw is a fun theoretical aside, since there's something modestly profound here. Cats Effect safely abstracts over bounded and unbounded queues because fiber blocking is cheap, so we're allowed to have a single offer signature (A => F[Unit]) which works for both, despite the fact that it might block in the bounded case. We got this idea from Java's standard library, which plays the same trick but with threads, but blocking threads is expensive and so arguably Java's abstraction is a bit leaky. Rather than repeat that mistake with our own impure queues, we simply don't allow for unsafeOffer: A => Unit on a bounded queue, which means we differentiate bounded and unbounded queues at the type level in this API (note that, unintuitively, an unbounded queue is a special case of a bounded queue in this paradigm).

Anyway, I'm not super thrilled with the Queue[F, A] with unsafe.BoundedQueueSink[F, A] (and similar with unbounded) paradigm, so it might be saner to build an unsafe.BoundedQueue and unsafe.UnboundedQueue trait which simply names this type, particularly since F is polyvariant. I am fond of the unsafe package prefix though, and I think it works reasonably well.

TODO

  • Become happy with the API
  • Write more than zero tests
  • Write more than zero lines of docs

@djspiewak djspiewak added this to the v3.6.0 milestone Jan 28, 2024
@djspiewak djspiewak marked this pull request as ready for review February 10, 2024 19:30
Copy link
Contributor

@durban durban left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea. I've left a small comment about the scaladoc.


Another fun thing we could do is a Deferred with a def unsafeComplete(a: A): Boolean. (I'm not saying to do it in this PR, it's just a similar idea.)

std/shared/src/main/scala/cats/effect/std/Queue.scala Outdated Show resolved Hide resolved
@armanbilge
Copy link
Member

Another fun thing we could do is a Deferred with a def unsafeComplete(a: A): Boolean. (I'm not saying to do it in this PR, it's just a similar idea.)

There have been multiple attempts by you and me at similar ideas 😅

I think unlike offering to a Queue, completing a Deferred may wake up more than one fiber. In that case you really want to be on the runtime first, see #2810 (comment).

@durban
Copy link
Contributor

durban commented Feb 13, 2024

There have been multiple attempts by you and me at similar ideas 😅

Yep 😄

In that case you really want to be on the runtime first

Yeah, ok, that's fair. (Although apparently I have trouble letting go of this idea...)

@armanbilge armanbilge closed this May 27, 2024
@armanbilge armanbilge reopened this May 27, 2024
@djspiewak djspiewak merged commit 1e445fb into typelevel:series/3.x May 30, 2024
29 of 33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants