task.delay/defer resume with unexpected arguments

Reproduction Steps
Reproduction script:

print("TEST 1")
local thread = coroutine.running()
task.delay(1, thread, "foo")
print("RESULT", coroutine.yield())

wait(2)

print("TEST 2")
local thread = coroutine.create(function(...)
	print("ONE", ...)
	print("TWO", coroutine.yield())
	print("THREE", coroutine.yield())
	print("FOUR", coroutine.yield())
end)
task.delay(1, thread, "one")
task.delay(1, thread, "two")
task.delay(1, thread, "three")
task.delay(1, thread, "four")

wait(2)

print("TEST 3")
local thread = coroutine.create(function(...)
	print("ONE", ...)
	print("TWO", coroutine.yield())
	print("THREE", coroutine.yield())
	print("FOUR", coroutine.yield())
end)
task.spawn(thread, "one")
task.delay(1, thread, "two")
task.delay(1, thread, "three")
task.delay(1, thread, "four")

wait(2)

print("DONE")

Expected Behavior
That the following output is printed:

TEST 1
RESULT foo
TEST 2
ONE one
TWO two
THREE three
FOUR four
TEST 3
ONE one
TWO two
THREE three
FOUR four
DONE

Actual Behavior
The following output is printed:

TEST 1
RESULT function: 0x725279fe6c969d97
TEST 2
attempt to call a string value
cannot resume non-suspended coroutine
cannot resume non-suspended coroutine
cannot resume non-suspended coroutine
TEST 3
ONE one
TWO four
THREE function: 0x725279fe6c969d97
FOUR function: 0x725279fe6c969d97
DONE

defer/delay seems to be mishandling the arguments passed to the thread when resumed.

  • In the first test, an unknown function is unexpectedly being passed to the thread.
  • In the second test, the argument passed to the last call is apparently being called as a function.
  • In the third test, it is demonstrated that arguments are being overridden by newer calls that involve the same thread. Whether or not this behavior is intentional, users will be expecting that arguments are associated with the call and not the thread.

Issue Area: Engine
Issue Type: Other
Impact: High
Frequency: Constantly
Date First Experienced: 2021-08-10 00:08:00 (+00:00)
Date Last Experienced: 2021-08-10 00:08:00 (+00:00)

4 Likes

Thanks for the report! We’ve filed a ticket to our internal database and we’ll follow up when we have an update for you.

2 Likes

Just bumping to indicate that this is still not fixed. The behavior appears to be similar to recent problems with the select function, which implies a mishandling of the Lua stack, so I feel it’s rather important.

1 Like

Hi @Anaminus , sorry for the late response.
Currently when running test1, the response is cannot delay non-suspended coroutine with argument. This seems like more intuitive behavior. It looks like you are getting a reference to the current active server thread, then calling task.delay to try and access the current running thread to add parameters to it, which we now actively prohibit.
As far as the behavior of test 2 goes, task,delay documentation reads that it schedules the task to be run at the next heartbeat. What I believe is happening here is that the script tries to run the same coroutine at the same time using multiple different threads (because we call task.delay 4 times in rapid succession, scheduling a run of the coroutine 4 times at the same time at the heartbeat 1 second in the future), hence the result of test 2. If you put in small wait calls between each task.delay call in the test, the output looks more like what you expected, because there will be time to properly resolve coroutine.yield() and yield the coroutine between when the task.delay calls resume the coroutine. Test 3 runs into a similar issue in that running the same thread multiple times on the same heartbeat results in unreliable behavior.
To sum up my response, I believe the code works as intended, but you should make sure that coroutines have time to suspend before you resume them again, ideally by verifying what your coroutines return and checking for errors by using something like coroutine.resume .