Integrating with Cosock

So far we have covered what cosock provides but what if we want to integrate our own libraries directly into cosock, what would that look like?

To start the general interface for a "cosock aware" lua table is to define a method setwaker which takes 2 arguments, kind: str and waker: fun()|nil. The general idea here is that a "waker" function can be provided that will get called when that task is ready to be woken again.

Let's try and build an example Timer that will define this setwaker method to make it "cosock aware"

local cosock = require "cosock"

local Timer = {}
Timer.__index = Timer

function Timer.new(secs)
  return setmetatable({
    secs = secs,
    waker = nil,
  }, Timer)
end

function Timer:wait()
  coroutine.yield({self}, {}, self.secs)
end

function Timer:setwaker(kind, waker)
  print("setwaker", kind, waker)
  if waker then
    self.waker = function()
      print("waking up!")
      waker()
    end
  else
    self.waker = nil
  end
end

cosock.spawn(function()
  local t = Timer.new(2)
  print("waiting")
  t:wait()
  print("waited")
end)

cosock.run()

To start we create a lua meta-table Timer, which has the properties secs: number and waker: fun()|nil. There is a constructor Timer.new(secs) which takes the number of seconds we want to wait for. Finally, we define Timer:wait which is where our magic happens. This method calls coroutine.yield, with 3 arguments {self}, an empty table, and self.secs. These arguments match exactly what would be passed to socket.select, the first is a list of any receivers, the second is a list of any senders and finally the timeout. Since we pass {self} as the first argument that means we are treating Timer as a receiver. Ultimately what we are doing here is asking cosock to call socket.select({self}, {}, self.secs). While we don't end up calling self.waker ourselves, cosock uses setwaker to register tasks to be resumed so we need to conform to that. Just to illustrate that is happening, a print statement has been added to setwaker, if we run this we would see something like the following.

waiting
setwaker        recvr   function: 0x5645e6410770
setwaker        recvr   nil
waited

We can see that cosock calls setwaker once with a function and a second time with nil. Notice though that self.waker never actually gets called, since we don't see a "waking up" message. That is because we don't really need to be woken up, our timer yields the whole coroutine until we have waited for self.secs, nothing can interrupt that. Let's extend our Timer to have a reason to call self.waker, we can do that by adding the ability to cancel a Timer.

local cosock = require "cosock"

local Timer = {}
Timer.__index = Timer

function Timer.new(secs)
  return setmetatable({
    secs = secs,
    waker = nil,
  }, Timer)
end

function Timer:wait()
  local r, s, err = coroutine.yield({self}, {}, self.secs)
  if err == "timeout" then
    return 1
  end
  return nil, "cancelled"
end

function Timer:setwaker(kind, waker)
  print("setwaker", kind, waker)
  if waker then
    self.waker = function()
      print("waking up!")
      waker()
    end
  else
    self.waker = nil
  end
end

function Timer:cancel()
  if self.waker then
    self.waker()
  end
end

cosock.spawn(function()
  local t = Timer.new(10)
  cosock.spawn(function()
    cosock.socket.sleep(3)
    t:cancel()
  end)
  print("waiting")
  local s = os.time()
  local success, err = t:wait()
  local e = os.time()
  print("waited", os.difftime(e, s), success, err)
end)

cosock.run()

In this example, we create our timer that will wait 10 seconds but before we call wait we spawn a new task that will sleep for 3 seconds and then call cancel. If we look over the changes made to wait we can see that we still call coroutine.yield({self}, {}, self.secs) but this time we are assigning its result to r, s, err. Cosock calls coroutine.resume with the same return values we would get from select, that is a list of ready receivers, a list of ready senders, and an optional error string. If the timer expires, we would expect to get back nil, nil, "timeout", if someone calls the waker before our timer expires we would expect to get back {self}, {}, nil. This means we can treat any err == "timeout" as a normal timer expiration but if err ~= "timeout" then we can safely assume our timer was canceled. If we were to run this code we would see something like the following.

waiting
setwaker        recvr   function: 0x556d39beb6d0
waking up!
setwaker        recvr   nil
setwaker        recvr   nil
waited  3.0     nil     cancelled

Notice we only slept for 3 seconds instead of 10, and wait returned nil, "cancelled"! One thing we can take away from this new example is that the waker API is designed to allow one coroutine to signal cosock that another coroutine is ready to wake up. With that in mind, let's try and build something a little more useful, a version of the cosock.channel api that allows for a maximum queue size. Looking over the existing channels, to implement this we are going to need to have 3 parts. A shared table for queueing and setting the appropriate wakers, a receiver table and a sender table. Let's start by defining the shared table.