Using coroutine.yield() in OnServerInvoke will not respond to client

When using coroutine.yield() in OnServerInvoke, the client will never get a response from the server

--[[ Setup
RemoteFunction in ReplicatedStorage
Script in ServerScriptService
LocalScript in StarterPlayer.StarterPlayerScripts
--]]

--ServerScriptService
game.ReplicatedStorage.RemoteFunction.OnServerInvoke = function()
	print("fire")
	
    --comment from here--------------------------------------
	local Thread = coroutine.running()
	
	delay(1, function()
		print("resumse")
		
		coroutine.resume(Thread)
	end)
	
	print("yielding")
	
	coroutine.yield()
    --to here and the client will get a response ------------------------

	print("return server")
end

--StarterPlayerScripts
print("fire")

local response = game.ReplicatedStorage.RemoteFunction:InvokeServer()

--When coroutine.yield() is being used in OnServerInvoke, this will never print
print("response client", response)
1 Like

Duplicate of Coroutine.resume() bug while used in RemoteFunction & BindableFunction I believe

2 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.

6 Likes

Hey is there any update on this?
Came across this issue recently, posting my code:

local f = Instance.new("RemoteFunction", game.ReplicatedStorage)
    
local threads = {}
    game:GetService("RunService").Stepped:Connect(function()
        for thread in pairs(threads) do
            coroutine.resume(thread)
            threads[thread] = nil
        end
    end)

f.OnServerInvoke = function()
    local t = coroutine.running()
    threads[t] = true
    coroutine.yield()
    print("Returning")
    return 1
end

and in console
=game.ReplicatedStorage.RemoteFunction:InvokeServer()

1 Like

The issue here seems consistent, so I’m not sure whether this really classifies as an actual bug or not.
What’s happening is that Roblox uses a concept called continuations to properly be able to converse back and forth between Lua and C, and when you use coroutines you kill off the continuations in that thread.
Evaera has explained this well:

Explanation

Any C-side code that invokes user code then “waits” for the user code to yield back to the C-side code will be broken, because of the way Roblox models this idea: continuations. Continuations are essentially a set of instructions that travels along with the thread and instructs the C-functions what to do when they’re done running. C-side yield functions have special machinery to pass along these continuations when they are invoked. However, coroutine.resume has no concept of continuations, so when you use it to resume a thread, the continuations are effectively discarded. This means that any pending jobs that were meant to run when the user thread finishes running will never actually run, thus potentially leaving those dependent threads stuck in a yielded state forever.

For an example of this behavior in action, consider the require function. It is a yield function and when it invokes the module thread it passes along a continuation task that asks that thread to resume the requiring script when it’s done. This all happens behind the scenes and you probably wouldn’t realize any of this happens, because if you have a wait(2); return nil in that module, it waits for two seconds and then you get nil back from require in the requiring module.

But if you instead use coroutine.yield() to yield the module thread and then resume it later with coroutine.resume, since that function has effectively discarded any pending continuations that the thread had, the require call from the requiring script will never return a value and that script will be stuck forever in a yielded state.

This behavior isn’t just isolated to require, though. Any time any C-side code invokes user code and waits for it, these caveats will apply. The advantage of using BindableEvent:Wait is that it supports continuations, whereas the coroutine library does not.

I’m going to just reformulate what you said so that it can be better understood, a person learning this would find it hard to grasp what you have explained.


If a module script yields upon being required, then a continuation task will be generated by the function (that yielded the module script) to the Task Scheduler to resume the yielded thread (the one that required the module) once the yield has finished.

However, coroutines are user-managed threads - you’re responsible for handling yields and resuming of the coroutines. coroutine.resume does indeed have no concept of thread continuations and only generates a task to the Task Scheduler to resume the yielded coroutine ONLY. Sure, you resume a yielded thread but the threads that have yielded because of that yielded thread WILL NOT be resumed since the continuations are just thrown away by coroutine.resume.

Continuations in a sense, are just tasks which tell the Task Scheduler to resume any yielded threads once the main thread (that yielded) has resumed by “jumping in” between different code from the yielded one.

Example:

-- Module script

local coro = coroutine.running() -- the thread that the module script
-- is running (

coroutine.wrap(function()
    wait(3) -- wait 3 seconds before resuming the yielded coroutine
    coroutine.resume(coro) -- the yielded coroutine is resumed
end)()

return coroutine.yield()

-------------------

-- A script that required the module script:

local module = require(moduleScript)
-- stuck in a infinite yield because coroutine.resume only
-- 

PS: Its better to say “extra thread continuations” than “continuations” to avoid confusion.

2 Likes

As correctly pointed out, when you manually resume a coroutine you bypass our engine’s resume implementation resulting in continuations not being processed. This means anything waiting on your thread to yield or terminate will yield forever. It’s not strictly a bug more of a side-effect of the way things are implemented.

The good news is we have a solution to this issue! In the coming weeks we’re going to be shipping some new APIs to schedule coroutines. One of them will allow you to resume a thread using the version in our scheduler that correctly handles continuations.

I’ll follow up after these have shipped :slight_smile:

11 Likes

Is this API task.spawn? I ran into this issue again today when trying to batch datastore get requests in a remote function callback. I was using coroutine.yield() and coroutine.resume(thread) which would make the remote function infinitely yield. Switching to task.spawn(thread) fixed the issue and the client gets a response now

That’s the one! You should be able to use them in your projects, but keep an eye out for an announcement sometime this week just to be safe :slight_smile:

4 Likes

These have shipped: Task Library - Now Available!

3 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.