···7373- [X] Optimization: avoid waking a process unless it has received messages
7474- [X] Use a single clock source in Scheduler (above optimization slowed per-process clock progression)
7575- [X] Optimization: only wake a process when it receives the message it's waiting for
7676-- [ ] Add timeouts to SimServer calls
7676+- [X] Add timeouts to SimServer calls
7777+- [ ] Optimize timeouts to avoid filling up queue (very slow)
777878797980### Testing
+53-20
lib/construct/scheduler.ex
···2323 defstruct @enforce_keys
2424 end
25252626+ defmodule Timeout do
2727+ @enforce_keys [:pid, :ref]
2828+ defstruct @enforce_keys
2929+ end
3030+2631 defmodule State do
2732 @type t :: %__MODULE__{
2833 clock: non_neg_integer,
···5459 GenServer.cast(scheduler, {:queue_task, self(), delay, resume})
5560 GenServer.cast(scheduler, {:yield, self()})
56615757- ref = resume.ref
5858- receive do
5959- {:resume, ^ref, nil} -> :ok
6060- end
6262+ nil = await_resume(resume)
6363+ :ok
6164 end
62656363- @spec yield_until_message(pid, function) :: :ok
6464- def yield_until_message(scheduler, check_fun) when is_function(check_fun) do
6666+ @spec yield_until_message(pid, function, non_neg_integer | :infinity) :: :ok
6767+ def yield_until_message(scheduler, check_fun, timeout_ms)
6868+ when is_function(check_fun) and ((is_integer(timeout_ms) and timeout_ms >= 0) or timeout_ms == :infinity) do
6569 resume = %Resume{ref: make_ref(), pid: self(), value: nil}
66706767- GenServer.cast(scheduler, {:await_message, self(), check_fun, resume})
7171+ GenServer.cast(scheduler, {:await_message, self(), check_fun, timeout_ms, resume})
6872 GenServer.cast(scheduler, {:yield, self()})
69737070- ref = resume.ref
7171- receive do
7272- {:resume, ^ref, nil} -> :ok
7373- end
7474+ nil = await_resume(resume)
7575+ :ok
7476 end
75777678 @spec spawn_and_yield(term, :link | :nolink, module, atom, [term]) :: pid
···8183 GenServer.cast(scheduler, {:queue_task, self(), 0, spawn})
8284 GenServer.cast(scheduler, {:yield, self()})
83858484- ref = resume.ref
8585- spawned_pid = receive do
8686- {:resume, ^ref, spawned_pid} -> spawned_pid
8787- end
8888-8686+ spawned_pid = await_resume(resume)
8987 case link do
9088 :link -> Process.link(spawned_pid)
9189 :nolink -> :noop
9290 end
93919492 spawned_pid
9393+ end
9494+9595+ defp await_resume(%Resume{ref: ref} = _resume) do
9696+ receive do
9797+ {:resume, ^ref, value} -> value
9898+ {:timeout, ^ref} -> exit(:timeout)
9999+ end
95100 end
9610197102 @spec send(term, pid, term) :: :ok
···169174 {:noreply, state}
170175 end
171176172172- def handle_cast({:await_message, pid, check_fun, %Resume{} = resume}, state) when is_pid(pid) and is_function(check_fun) do
173173- awaiting_message = Map.put(state.awaiting_message, pid, {check_fun, resume})
174174- {:noreply, %State{state | awaiting_message: awaiting_message}}
177177+ def handle_cast({:await_message, pid, check_fun, timeout_ms, %Resume{} = resume}, state) when is_pid(pid) and is_function(check_fun) do
178178+ ref = make_ref()
179179+ state = %State{state | awaiting_message: Map.put(state.awaiting_message, pid, {ref, check_fun, resume})}
180180+181181+ state =
182182+ case timeout_ms do
183183+ :infinity ->
184184+ state
185185+186186+ timeout_ms when is_integer(timeout_ms) and timeout_ms >= 0 ->
187187+ expires_after = state.clock + timeout_ms
188188+ queue_task(state, expires_after, %Timeout{pid: pid, ref: ref})
189189+ end
190190+191191+ {:noreply, state}
175192 end
176193177194 def handle_cast({:yield, pid}, %State{} = state) when is_pid(pid) do
···267284 perform_next_task(state)
268285 end
269286287287+ defp perform(%State{} = state, _time, %Timeout{pid: pid, ref: ref}) do
288288+ case Map.get(state.awaiting_message, pid) do
289289+ # The process is still waiting, so we trigger the timeout
290290+ {^ref, _check_fun, %Resume{} = resume} ->
291291+ state = monitor_current(state, pid)
292292+ send resume.pid, {:timeout, resume.ref}
293293+294294+ state
295295+296296+ # The process is either not waiting anymore, or waiting for
297297+ # a different Timeout
298298+ _ ->
299299+ perform_next_task(state)
300300+ end
301301+ end
302302+270303 defp perform(%State{} = state, _time, %Resume{} = resume) do
271304 #log(state, {:resume, time, resume.pid, resume.value})
272305···289322 nil ->
290323 state
291324292292- {check_fun, %Resume{} = resume} when is_function(check_fun) ->
325325+ {_ref, check_fun, %Resume{} = resume} when is_function(check_fun) ->
293326 case check_fun.(send.message) do
294327 false ->
295328 state
+23-9
lib/construct/sim_server.ex
···2222 end
23232424 """
2525- defmacro yield_receive(scheduler_pid, [do: do_block]) do
2525+ defmacro yield_receive(scheduler_pid, timeout \\ :infinity, [do: do_block]) do
2626 # Here, we use `do_block` to build a function which
2727 # checks if a value matches the intended receive block
2828 #
···8282 case receive_func.() do
8383 ^ref ->
8484 # If there is no matching message, we yield until one is sent to us
8585- Hobbes.Construct.Scheduler.yield_until_message(unquote(scheduler_pid), check_func)
8585+ Hobbes.Construct.Scheduler.yield_until_message(unquote(scheduler_pid), check_func, unquote(timeout))
86868787 # Now that a message has arrived, we extract it with the receive
8888 # The case is a sanity check
···159159 nil ->
160160 GenServer.call(server, request, timeout)
161161 scheduler_pid when is_pid(scheduler_pid) ->
162162- sim_call(scheduler_pid, server, request)
162162+ sim_call(scheduler_pid, server, request, timeout)
163163 end
164164 end
165165···202202 end
203203 end
204204205205- defp sim_call(scheduler_pid, server, request) do
205205+ defp sim_call(scheduler_pid, server, request, timeout) do
206206 request_id = sim_send_request(scheduler_pid, server, request)
207207- sim_receive_response(scheduler_pid, request_id)
207207+ try do
208208+ yield_receive(scheduler_pid, timeout) do
209209+ {^request_id, reply} -> reply
210210+ end
211211+ catch
212212+ :exit, reason ->
213213+ # GenServer: {:timeout, {m, f, a}}
214214+ # SimServer: {{:timeout, {m, f, a}}, stacktrace}
215215+ exit({{reason, {__MODULE__, :call, [server, request]}}, __STACKTRACE__})
216216+ end
208217 end
209218210219 defp sim_send_request(scheduler_pid, server, request) do
···217226 ref
218227 end
219228220220- defp sim_receive_response(scheduler_pid, request_id, _timeout \\ 5000) do
221221- # TODO: timeout?
222222- yield_receive(scheduler_pid) do
223223- {^request_id, reply} -> reply
229229+ defp sim_receive_response(scheduler_pid, request_id, timeout) do
230230+ # TODO: catch timeout exit
231231+ try do
232232+ yield_receive(scheduler_pid, timeout) do
233233+ {^request_id, reply} -> reply
234234+ end
235235+ catch
236236+ :exit, reason ->
237237+ exit({{reason, {__MODULE__, :receive_response, [request_id]}}, __STACKTRACE__})
224238 end
225239 end
226240
+1-1
lib/servers/distributor.ex
···3131 def start_link, do: SimServer.start_link(__MODULE__, nil)
32323333 def move_shard(server, shard_key, to_servers) when is_binary(shard_key) and is_list(to_servers) do
3434- SimServer.call(server, {:move_shard, shard_key, to_servers})
3434+ SimServer.call(server, {:move_shard, shard_key, to_servers}, 20_000)
3535 end
36363737 def init(nil) do
+1
lib/servers/storage.ex
···429429430430 # TODO: assert invariants
431431432432+ # TODO: this can deadlock, it needs to be pipelined
432433 too_new_backoff(fn ->
433434 Storage.read_range(from_pid, read_version, si.current_key, si.end_key, limit: 4)
434435 end)