Double firing of CollectionService:GetInstanceAddedSignal when applying tag in same frame that object is added to DataModel

I am reporting an engine issue about the CollectionService. In my real project, I’m using the CollectionService to populate certain tags with certain objects. It is important to note that the objects used to populate can themselves have tags which require population. I found an issue where tagged children who were created through tag population ie cloned by a handler of a different GetInstanceAddedSignal signal would be detected twice by the GetInstanceAddedSignal signal. I created a test project to be able to see the bug without all of the core code of the main game. Here is the project structure and all related scripts.

Workspace:

  • Part: OriginalCreator (Tagged Populater)

ReplicatedStorage:

  • Part: CloneableCreator (Tagged Populater)
  • Part: CreatedPart (Tagged CreatedPart)

ServerScriptService:

  • Script: TestScript (Source code attached)

TestScript.lua

-- /// Services ///
local CollectionService = game:GetService("CollectionService")

-- /// Helpers ///
local function run_for_each_tagged_part(tag, cb)	
	local function wrapped_cb(part)
		if workspace:IsAncestorOf(part) then
			cb(part)
		end
	end
	
	-- Listen for this tag being applied to objects
	CollectionService:GetInstanceAddedSignal(tag):Connect(wrapped_cb)
	 
	-- Also detect any objects that already have the tag
	for _, object in pairs(CollectionService:GetTagged(tag)) do
		wrapped_cb(object)
	end
end

-- /// Setup some listeners ///
run_for_each_tagged_part("Populater", function(part)
	print("Populating creator named " .. part:GetFullName())
	game.ReplicatedStorage.CreatedPart:Clone().Parent = part
end)

run_for_each_tagged_part("CreatedPart", function(part)
	print("Found a part that got created and is a descendant of the workspace named " .. part:GetFullName())
end)


-- /// Spawn a new creator ///
print("Cloning game.ReplicatedStorage.CloneableCreator and moving to workspace")
game.ReplicatedStorage.CloneableCreator:Clone().Parent = workspace

Current output:
(Logic error is in italics)

Populating creator named Workspace.OriginalCreator
Found a part that got created and is a descendant of the workspace named Workspace.OriginalCreator.CreatedPart
Cloning game.ReplicatedStorage.CloneableCreator and moving to workspace
Populating creator named Workspace.CloneableCreator
Found a part that got created and is a descendant of the workspace named Workspace.CloneableCreator.CreatedPart
Found a part that got created and is a descendant of the workspace named Workspace.CloneableCreator.CreatedPart

Desired output:

Populating creator named Workspace.OriginalCreator
Found a part that got created and is a descendant of the workspace named Workspace.OriginalCreator.CreatedPart
Cloning game.ReplicatedStorage.CloneableCreator and moving to workspace
Populating creator named Workspace.CloneableCreator
Found a part that got created and is a descendant of the workspace named Workspace.CloneableCreator.CreatedPart
Found a part that got created and is a descendant of the workspace named Workspace.CloneableCreator.CreatedPart

What the program does:

  • Setups up two listeners for two tags: Populater and CreatedPart (Note: These events only run on descendants of the workspace):
    • The Populater tag handler copies a part tagged with CreatedPart located in the ReplicatedStorage and reparents it to the detected part
    • The CreatedPart logs the part’s existence to the console
  • The listeners detect a part in the workspace already tagged as a Populater and populates it.
  • The listeners detect the created part in the workspace.OriginalCreator tagged as a CreatedPart and correctly logs it’s existence to the console once.
  • A part tagged as a Populater is coppied from ReplicatedStorage and moved to the workspace
  • This part gets detected by the Populater tag listener where it is populated once.
  • However, the created part in the workspace.ClonedCreator is detected twice by the CreatedPart tag listener and it’s existence is logged out twice even though it’s only created once. The issue lies here.

What I know

  • Cloning the CreatedPart into the Workspace or Workspace.OriginalCreator fixes the double detection of the CreatedPart.
  • Adding a slight wait, running the code with breakpoints or spawning a new thread before cloning the CreatedPart fixes this.

Thanks,
Riley

EDIT:
I found an even easier way to replicate this bug. All it requires is the following script in the ServerScriptService.

local CollectionService = game:GetService("CollectionService")

CollectionService:GetInstanceAddedSignal("Registered"):Connect(function(part)
	print("InstanceAddedSignal called for " .. part:GetFullName())
end)

game.DescendantAdded:Connect(function(part)
	print("DescendantAdded called for " .. part:GetFullName())
	CollectionService:AddTag(part, "Registered")
end)

Instance.new("Part").Parent = workspace

Current output:

DescendantAdded called for workspace.part
InstanceAddedSignal called for workspace.part
InstanceAddedSignal called for workspace.part

Desired output:

DescendantAdded called for workspace.part
InstanceAddedSignal called for workspace.part
InstanceAddedSignal called for workspace.part

What the program does:

  • Setups up an event listener for when an instance is tagged as Registered.
  • Setups up a game.DescendantAdded listener for when an instance is added to the DataModel / game.
14 Likes

I am also experiencing this issue, I’ve attached a place containing a script that reproduces the bug (you’ll find it under ServerScriptService).
CSBug.rbxl (17.0 KB)

3 Likes

I’m also seeing this issue occur, would love to not have this happen, since this induces performance costs in my game.

6 Likes

Wow I did not expect to find a thread that is 3 years old on the same exact issue that I experienced…

I have added up to date code using task* and also provide a rbxl file

Code
local CollectionService = game:GetService("CollectionService")

local Test = workspace.Test

do
	local function setTag(instance: Instance)
		CollectionService:AddTag(instance, "Test")
	end

	for _, descendant: Instance in ipairs(Test:GetDescendants()) do
		task.spawn(setTag, descendant)
	end

	--[[
		Case 1: setting workspace.SignalBehavior to Deferred will fix it
		
		Case 2: adding task.wait before adding a tag will fix it
		
		Case 3: using task.defer before adding a tag will fix it
		
		therefor it seems like waiting a frame before adding a tag will solve our problem
	--]]

	Test.DescendantAdded:Connect(function(descendant: Instance)
		-- Case 2:
		--task.wait()
		setTag(descendant)

		-- Case 3:
		task.defer(CollectionService.AddTag, CollectionService, descendant, "Test")
	end)
end

local function doForTagged(tag: (string), callback: (Instance) -> ()): RBXScriptConnection
	for _, instance: Instance in ipairs(CollectionService:GetTagged(tag)) do
		task.spawn(function()
			print("for ipairs-", instance:GetFullName())
			callback(instance)
		end)
	end

	return CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance: Instance)
		print("GetInstanceAddedSignal-", instance:GetFullName())
		callback(instance)
	end)
end

local cache = {}

doForTagged("Test", function(instance)
	if not cache[instance] then
		cache[instance] = instance
		--print("1st:", instance:GetFullName())
	else
		--! print if GetInstanceAddedSignal got triggered twice
		print("2nd:", instance:GetFullName())
	end
end)

task.spawn(function()
	local model = Instance.new("Model")

	while true do
		local model = Instance.new("Model")
		model.Name = game.HttpService:GenerateGUID()
		model.Parent = Test
		task.wait(1)
	end
end)

Repro.rbxl (33.9 KB)

I’d appreciate any insight on why this happens so I can look out for other potential bugs like this :pray:

5 Likes

I have an issue with GetInstanceRemovedSignal() firing twice. When a part is destroyed, the function is called twice (though it is in a more complex environement so there’s possibly something else doing something??? idk). If I can’t find a nice fix for it that’ll be annoying

Edit: it was caused by connecting two tags to my tag removed function, so when the part was destroyed, both tags were removed, thus firing twice

I am also experiencing this issue and cannot find a consistent workaround.

Edit: Oops, I had a script set to the localscript runcontext in the starterplayer, which caused it to be duplicated, which is why I thought it was firing twice

how is this still not fixed… 30ch

Can confirm this is still an issue. For anyone looking for a workaround, wrap the CollectionService:AddTag() piece in a task.defer function.

Edit: my issue was fixed a while ago, linking it more clearly to help others:


old reply

I think I’m also having this issue. GetInstanceAddedSignal is firing twice in all of my code that use it.

Here's a specific script with relevant code. *(A local script under StarterGui. )*

local CollectionService = game:GetService("CollectionService")
local TweenService = game:GetService("TweenService")

local ToggleLoadingScreenRemoteEvent = game.Workspace.RemoteEventsFolder.UI.ToggleLoadingScreen
local PlayerEnteredAWorld_RemoteEvent = game.Workspace.RemoteEventsFolder.PlayerEnteredAWorld_RemoteEvent
local teleportPlayer_ClientToServer = game.Workspace.RemoteEventsFolder["TeleportPlayer(ClientToServer)"]
local SetGameData_RemoteEvent = game.Workspace.RemoteEventsFolder["Data System"].SetGameData

local objectsLoadedIn_Table = {}
local tagForThisScript = "Portals"
local localPlayer = game.Players.LocalPlayer

local function makeObjectWhatItShouldBeFunction(objectModel_v)
	
	if objectModel_v ~= nil then
		
		local PortalPart
		local WherePlayerGoesPart
		local TeleportingSound
		local debounce = false
		local whatWorldDidThePlayerEnter
		
		local success, errorMessage = pcall(function()

			PortalPart = objectModel_v.portalBit
			WherePlayerGoesPart = objectModel_v.EndTeleport 
			TeleportingSound = objectModel_v.TeleporterPortalSound
			whatWorldDidThePlayerEnter = objectModel_v.WhereDoesThisPortalTakeThePlayer_StringValue.Value

		end)
		
		--print(success,errorMessage)

		if success then
			
			PortalPart.Touched:Connect(function(hit)
				if game.Players:GetPlayerFromCharacter(hit.Parent) == localPlayer then
					if debounce == false then
						debounce = true

						local teleportGoalCFrame = WherePlayerGoesPart.CFrame
						local shouldLocationBeStreamedIn = true
						teleportPlayer_ClientToServer:FireServer(teleportGoalCFrame,shouldLocationBeStreamedIn)


						if objectModel_v.Parent.Name == "World1_Grasslands" then
							-- this means the portal is from grasslands tutorial to hub world
							local whatDataIsBeingSet = "hasPlayerCompletedTheTutorialOnThisSaveFile"
							local whatShouldTheDataBeSetTo = true

							SetGameData_RemoteEvent:FireServer(whatDataIsBeingSet,whatShouldTheDataBeSetTo)

						end


						--TeleportingSound:Play()
						local toggleOnOrOff = true
						ToggleLoadingScreenRemoteEvent:FireServer(toggleOnOrOff)
						PlayerEnteredAWorld_RemoteEvent:FireServer(whatWorldDidThePlayerEnter)
						
						print("Portal Test!!")
						
						task.wait(1.85)

						toggleOnOrOff = false
						ToggleLoadingScreenRemoteEvent:FireServer(toggleOnOrOff)
						debounce = false

					end
				end
			end)
			
			--
		end
		
	end
	
end

-- to get future parts:
CollectionService:GetInstanceAddedSignal(tagForThisScript):Connect(function(newpart: Part)
	--print("Check GetInstanceAddedSignal!")
	print(newpart:GetFullName())
	if table.find(objectsLoadedIn_Table, newpart) == nil then
		table.insert(objectsLoadedIn_Table, newpart)
		--print(newpart:GetFullName())
		makeObjectWhatItShouldBeFunction(newpart)
		wait(0.5)
	end
end)

-- to remove parts as they are unloaded:
CollectionService:GetInstanceRemovedSignal(tagForThisScript):Connect(function(oldpart: Part)
	--print("Check GetInstanceRemovedSignal!")
	local index = table.find(objectsLoadedIn_Table, oldpart)
	if index then
		table.remove(objectsLoadedIn_Table, index)
	end
end)


The models tagged are all Atomic; I’m using StreamingEnabled in my game.


This doesn’t seem to apply/work for me. Do you mean in a CoreScript? I assume not. My implementation does not use :AddTag(). *see code from dropdown above


@CorvusCoraxx seemed to respond to a similar or the same issue, I’m not sure: New Improvements to Streaming Enabled - #122 by CorvusCoraxx

Could you look into this?

I had the same problem but with CollectionService:GetTagged(). I don’t know how to properly use task.defer so this is my solution

local AllTags = CollServ:GetTagged("Absolute")
local Tags = {}
table.move(AllTags, 1, #AllTags/2, 1, Tags)
for i,v in ipairs(Tags) do
	print(i,v)
	AbsoluteButton(v)
end

It moves half of the table to another table and it works. Don’t worry about not working functions, it deletes only clones(somehow). If it works, don’t touch it lol.

Edit: It doesn’t work. It fires one time but the code I put in it doesn’t work. So I will write a script that activates all connections when GUI is active and disconnects them when GUI is hidden to compensate double connections.

Edit2: Wait a minute if code doesn’t work then it means there is no double connections. It might be visual bug

Hi, could you confirm if this bug is still happening? I’m not able to reproduce it with the code you’ve provided, using both deferred and immediate signal behavior. There’s been a few fixes recently related to tag adding and removal, so it’s possible this bug has been fixed indirectly.

1 Like

Edit: actually, this issue might still be a thing for others. I guess waiting for the OP is key here, hence why you replied to them. By bad. I had notifications on for this thread. My specific issue had to deal with something that was fixed quite a while ago. See my old reply for details.

my old reply

Hi, you can close this bug out. In short, it’s not a bug. More info can be found from these quotes:

1 Like