util.async: Add next-tick configuration
authorMatthew Wild <mwild1@gmail.com>
Mon, 29 Nov 2021 14:14:30 +0000
changeset 11966 9a70a543c727
parent 11965 542a9a503073
child 11967 f5c6be4a3ecc
util.async: Add next-tick configuration Running woken runners in the next iteration of the event loop prevents unexpected recursion, unexpected tracebacks, and is generally more predictable. The pattern is borrowed from util.promise, where we're now doing the same.
spec/util_async_spec.lua
util/async.lua
--- a/spec/util_async_spec.lua	Mon Nov 29 14:11:24 2021 +0000
+++ b/spec/util_async_spec.lua	Mon Nov 29 14:14:30 2021 +0000
@@ -669,4 +669,50 @@
 			assert.spy(r.watchers.ready).was.called();
 		end);
 	end);
+
+	describe("#set_nexttick()", function ()
+		after_each(function ()
+			-- Restore to default
+			async.set_nexttick(nil);
+		end);
+		it("should work", function ()
+			local queue = {};
+			local nexttick = spy.new(function (f)
+				assert.is_function(f);
+				table.insert(queue, f);
+			end);
+			async.set_nexttick(nexttick);
+
+			local processed_item;
+			local wait, done;
+			local r = new(function (item)
+				wait, done = async.waiter();
+				wait();
+				processed_item = item;
+			end);
+			r:run("test");
+
+			-- Nothing happened, because the runner is waiting
+			assert.is_nil(processed_item);
+			assert.equal(r.state, "waiting");
+			assert.spy(nexttick).was_called(0);
+			assert.spy(r.watchers.waiting).was.called();
+			assert.spy(r.watchers.ready).was_not.called();
+
+			-- Mark the runner as ready, it should be scheduled for
+			-- the next tick
+			done();
+
+			assert.spy(nexttick).was_called(1);
+			assert.spy(nexttick).was_called_with(match.is_function());
+			assert.equal(1, #queue);
+
+			-- Pretend it's the next tick - call the pending function
+			queue[1]();
+
+			assert.equal(processed_item, "test");
+			assert.equal(r.state, "ready");
+			assert.spy(r.watchers.ready).was.called();
+		end);
+	end);
 end);
--- a/util/async.lua	Mon Nov 29 14:11:24 2021 +0000
+++ b/util/async.lua	Mon Nov 29 14:14:30 2021 +0000
@@ -13,6 +13,9 @@
 
 -- Configurable functions
 local schedule_task = nil; -- schedule_task(seconds, callback)
+local next_tick = function (f)
+	f();
+end
 
 local function runner_from_thread(thread)
 	local level = 0;
@@ -62,8 +65,10 @@
 		-- If state is 'ready', it is our responsibility to update runner.state from 'waiting'.
 		-- We also have to :run(), because the queue might have further items that will not be
 		-- processed otherwise. FIXME: It's probably best to do this in a nexttick (0 timer).
-		runner.state = "ready";
-		runner:run();
+		next_tick(function ()
+			runner.state = "ready";
+			runner:run();
+		end);
 	end
 	return true;
 end
@@ -286,5 +291,6 @@
 	wait_for = wait_for;
 	sleep = sleep;
 
+	set_nexttick = function(new_next_tick) next_tick = new_next_tick; end;
 	set_schedule_function = function (new_schedule_function) schedule_task = new_schedule_function; end;
 };