Spawn() and coroutine.wrap() not working in a specific module?


#1

Hey all.

I’ve been trying to debug one of my modules (for a push notification UI), and I’ve been seeing really strange behavior from it.

So basically thread manipulation isn’t working inside of the module for certain cases.

Functions such as spawn and coroutine.wrap/create are literally just ignored. When I ran my module line by line in the debugger, I found that spawn and the likes are just skipped over.

In most other cases, however, they work as expected.

Here’s the relevant code and an explaination of it.

NotificationController Module

This module is what handles the managing of push notifications.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : UnmuteNotifications
-- @Description : Un-mutes notifications.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function NotificationController:UnmuteNotifications()
	Notifications_Muted=false
	self:PushNotification(
		"Notifications",
		"Notifications have been un-muted."
	)
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : PushNotification
-- @Description : Creates a push notification with the given title, prompt, color and UI.
-- @Params : string "Title" - The title of the notification.
--           string "Prompt" - The prompt of the notification.
-- optional  string "Color" - The color of the notification.
-- optional  Instance <ScreenGui> "UI" - The UI to display when the notification is clicked.
-- optional function "Func" - The function to run when the notification is clicked.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function NotificationController:PushNotification(Title,Prompt,Color,UI,Func)
	if not Notifications_Muted then
		warn("Push Notification")
		Notification_UI:ShowNotification(Title,Prompt,Color)
	end
end

Notification_UI Module

This module handles the displaying and interaction of notifications. The spawn() in question is in this module.
local Notification_Display_Time=5 --Time for a notification to display before hiding and being archived.

local Notification_UI=game.ReplicatedStorage.UIs.Notification_UI:Clone()
local BaseNotification=Notification_UI.Notification



function NotificationUI:ShowNotification(Title,Prompt,Color)
	warn("Create Notification")

	local Notification=BaseNotification:Clone()
	Notification.Name="_Notification"
	Notification.Title.Text=Title
	Notification.Prompt.Text=Prompt
	Notification.ImageColor3=(Color or Notification.ImageColor3)
	Notification.Position=UDim2.new(1.3,0,0.7,0)
	Notification.Parent=Notification_UI
	
	script.Ding:Play()	
	
	Notification:TweenPosition(UDim2.new(1,0,0.7,0),"Out","Sine",0.5)
	spawn(function() --THIS DOESNT RUN SOMETIMES!?
		wait(Notification_Display_Time)
		Notification:TweenPosition(UDim2.new(1.3,0,0.7,0),"Out","Sine",0.5)
		wait(0.5)
		Notification:Destroy()
	end)

	return
end

LobbyController Module

The function in here is ran when the player enters the lobby from either joining the game or leaving a round.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : ShowLobbyUI
-- @Description : Shows the lobby UI
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function LobbyController:ShowLobbyUI()
	Lobby_UI:Show() --Showing the lobby UI
	StarterGui:SetCore("SetAvatarContextMenuEnabled",true)
	self.Controllers.NotificationController:UnmuteNotifications()
end

Code from localscript that ties the modules together

----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : LoadController
-- @Description : Loads the specified controller module into the engine.
-- @Params :  Instance <ModuleScript> "ControllerModule" - The controller module to load into the engine
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function DragonEngine:LoadController(ControllerModule)
	local ControllerName=ControllerModule.Name
	local Controller;
	local s,m=pcall(function() --If the module fails to load/errors, we want to keep the engine going
		Controller=require(ControllerModule)
	end)
	if not s then --Controller module failed to load
		DragonEngine:Log("Failed to load Controller '"..ControllerName.."' : "..m,"Warning")
	else --Controller moduled was loaded
		setmetatable(Controller,{__index=DragonEngine}) --Exposing Dragon Engine to the controller
		DragonEngine.Controllers[ControllerName]=Controller
		DragonEngine:DebugLog("Loaded Controller '"..ControllerName.."'.")
	end
end

DragonEngine:DebugLog("Loading and initializing controllers...")
for _,ControllerModule in pairs(Recurse(Paths.Controllers)) do
	if ControllerModule:IsA("ModuleScript") and ControllerModule:FindFirstChild("Ignore")==nil then
		DragonEngine:LoadController(ControllerModule)
		local Controller=DragonEngine:GetController(ControllerModule.Name)
		if type(Controller.Init)=="function" then
			DragonEngine:DebugLog("Initializing controller '"..ControllerModule.Name.."'...")
			Controller:Init()
		end
	end
end

DragonEngine:DebugLog("Starting controllers...")
for ControllerName,Controller in pairs(DragonEngine.Controllers) do
	if type(Controller.Start)=="function" then
		DragonEngine:DebugLog("Starting controller '"..ControllerName.."'...")
		coroutine.wrap(Controller.Start)(Controller) --Starting the controller in its own thread, while giving it
																									--direct access to itself
	end
end

As you can see, NotificationController.UnmuteNotifications is called from the lobby controller when the lobby UI is showing. This is to enable notifications while in the lobby, but to mute them while in a round.

For whatever reason, in that case spawn doesn’t work. There is no errors or warnings, spawn is literally just skipped over.

But if I call NotificationController.UnmuteNotifications from a different module other than the LobbyController module, spawn works as expected.

I have honestly no clue what’s causing this.

What’s the issue with the code? spawn works perfectly fine in all of my other modules, it’s just this one specific module that’s giving me the odd behavior.


#2

Have you tried putting print statements or breakpoints in your spawn functions?

spawn functions are not run immediately, they get run in the next Lua cycle. So if you’re stepping through your code they will appear to get skipped over. Eventually, it should come back to it though.

However, coroutines should get run in the same Lua cycle.


#3

Yes, I’ve put print statements inside the spawned functions. They are never executed.


#4

I’d first just like to point out that you can’t spawn new threads. You’re actually just telling the Lua engine to execute a function as if it is a separate schedulable (not a word, but hopefully you understand) entity.

That said, I don’t know the exact cause of your problem. I don’t actually see any code that gives a reason to have spawning and coroutines being employed, either, but perhaps there is more than meets the eye. But my biggest hypothesis for why this is failing is that you’re suffering from a race condition. It may help to put, as @Osyris said, several print statements. It seems very unlikely that the spawn function is acting improperly, and it would help if you could show us some output that better demonstrates that the function inside spawn is in fact failing to be run.


#5

I have the spawn in there so I can have notifications show for 5 seconds, then hide without yielding the calling thread.

E.g. if a trade manager sends a notification, I don’t want the trade manager to wait while the notification is being animated.


#6

Ah okay, fair reason. That said, I still think this may be a race condition or some other bug in your module. I do not think it is the spawn function behaving unexpectedly.


#7

I don’t think it’s a race condition. As I’ve said, spawn does not run at all. It’s just skipped over by the debugger, never to be seen again.


#8

As @Osyris said:

So the debugger will “skip over” the spawn body because that code isn’t run until at least the next Lua cycle. The debugger “skipping over” the spawn body does not mean that the code does not run.


#9

What calls LobbyController:ShowLobbyUI()?


#10

A LocalScript in ReplicatedFirst, and the minigame client module.

Relevant code snippet from localscript:

shared.DragonEngine.Controllers.MusicController.Muted=false --Enabling the music controller.
shared.DragonEngine.Controllers.LobbyController:ChangeSong() --Telling lobby to run music.
shared.DragonEngine.Controllers.LobbyController:ShowLobbyUI() --Showing the lobby UI (Notifications don't run normally here, spawn() doesn't work)
game:GetService("StarterGui"):SetCore("TopbarEnabled", true) --Enabling the core ROBLOX UI.
shared.DragonEngine.Services.PlayerService:SetPlayerStatus("Ready",true)

Relevant code snippet from minigame client module:

		-----------------------------------------------------------------------------------------
		-- Waiting for the transition to the lobby to be completed before removing the fade UI --
		-----------------------------------------------------------------------------------------
		game.Players.LocalPlayer.CharacterAdded:wait()
		self.Controllers.LobbyController:ShowLobbyUI() --Notifications run normally here, spawn() works.
		self.Controllers.LobbyController:ChangeSong()
		Fade_Out:Play()
		Fade_Out.Completed:wait()
		Fade_UI:Destroy()

#11

But the thing is, print() statements inside the function aren’t even running. When I step through my code in cases where the spawn() does work, it goes through the spawn normally.

In the case where the spawn() does not work, the debugger literally doesn’t even highlight any code inside the spawn, it just goes directly to the end of the spawn and said code inside the spawn is never ran.


#12

So it only fails to run when called (ultimately) by the LocalScript?

Do you end up deleting or disabling this LocalScript? Doing so will cause all event connections and spawned functions created while running that script to be stopped, even if the script was running module code at the time of connection/spawning.


#13

Hm, that could be it. Let me check.


#14

Yup… facepalm


#15

That was definitely causing the problem. I cannot believe the issue was that simple… the joys of debugging.

Thank you for pointing this out to me. :slight_smile:


#16

Two workarounds here:

  • Have the LocalScript fire a BindableEvent to get another script to run ShowLobbyUI
  • (Hacky) Destroy the LocalScript before calling ShowLobbyUI, the running coroutine won’t be stopped and you’re free to start new ones, weirdly enough

Me abusing that to have my scripts running but not present in the data model back when stealing server scripts was a concern is the only reason I knew about this honestly, I’m not surprised to see it catch other people out.