De-Sync Bugs with Instance Replication Fix

[Update] May 25, 2022


Hello Developers,

We will soon roll out a change to replication: when a Server/Client receives an Instance, it will remove all of its replicated children. This will correct various DataModel de-sync bugs.

We will release this change on Studio first, and then globally for all experiences over the next week. Please test this behavior out on your experience and tell us what you think!

What does this mean?

Let’s go through an example!

When an Instance gets added to the DataModel, we expect it to replicate as normal. For example, if the server adds a part named “PartA” to Workspace, we expect that to replicate to clients, so everyone sees:

Workspace

→ PartA 

Similarly, if another Instance (say, folder) gets added under PartA, we expect everyone to see that too:

Workspace

→ PartA

  → Folder

Now, suppose the server does the following in a script:

local partA = workspace.PartA

local folder = partA.Folder

-- set parent of PartA to nil, removing it from the DataModel

partA.Parent = nil

-- set the parent of Folder to nil, removing it from under PartA

folder.Parent = nil

-- set the parent of PartA back to Workspace

partA.Parent = workspace

After this script runs, we would expect everyone to see:

Workspace

→ PartA

This is not the case. Let’s go over how it works in its current form.


Current Behavior

Suppose the client had created a LocalScript like so:

local partA = workspace.PartA

print(partA.Name)

When the Server removes PartA from the DataModel, the client will remove PartA too. However, because of the lingering reference to PartA in the LocalScript, it won’t delete PartA and its subtree from memory! So, PartA and PartA.Folder will be floating along somewhere in the Client’s Ether of Memory™️.

So now, when the server brings PartA back under Workspace, the client will grab the PartA that it already has in memory, and parent PartA to workspace. The problem is, folder comes along for the ride, resulting in the server seeing:

Workspace

→ PartA

While the client sees this:

Workspace

→ PartA

  → Folder

New Behavior

The change we are releasing will prevent de-syncs of DataModels by having the client remove all children of PartA upon receiving it from the server.

To get more specific, when the server adds PartA back to Workspace, the client will:

  1. Remove all children from PartA (remove folder)

  2. All relevant signals like ChildRemoved signal will still fire

  3. Parent PartA to the Workspace

  4. Children of PartA that do still exist on the Server will replicate to the Client again

Resulting in both the server and client having the consistent view:

Workspace

→ PartA

What about Instances LocalScripts created and added that the client never replicates? As these Instances are not known by the Server, we believe it doesn’t make sense for the server to modify them. By this logic, we decided that the new behavior will ignore local, unreplicated Instances.

However, if the client has replicated the locally created instance to the server (e.g. a Tool weld), that instance is eligible for removal with this new behavior.

Thanks so much and let us know if you have any feedback or questions!

107 Likes

This topic was automatically opened after 10 minutes.

Is this contributing to the error that has just swept accross the site?

2 Likes

Seems like a very logical and helpful update, should prevent weird and unexpected bugs, glad this change was made!

I presume the studio update will be opt-in to avoid inconsistencies.

4 Likes

I don’t think so - this feature has only been enabled for Studio at the moment. We plan on releasing it globally in a week or so

9 Likes

I have a quick question, how is replication handled for Attributes and Instances. I had cases where the attribute will be replicated first than the Instances. For example:

Server:

local Folder = ServerStorage.Folder:Clone() --> Imagine there is a bunch of ValueBase under the Folder Instance.
Folder.Parent = Player --> Player Instance
Player:SetAttribute("FolderLoaded", true)

Client:

if Player:GetAttribute("FolderLoaded") == nil then Player:GetAttributeChangedSignal("FolderLoaded"):Wait() end

Player.Folder.ValueBase.Value = true --> Error: ValueBase is not a valid member of Folder.

To fix the issue, I stopped using an attribute and instead I used a BoolValue named ‘FolderLoaded’ and just parented. Though I still want to know why that behavior happens, if possible.

EDIT 1: I haven’t test if this issue happens in a live game, but is happening in Studio a lot.
EDIT 2: The issue happens as well with RemoteEvents when you trigger them so the client does something with the Object parented.

4 Likes

I was a bit scared for a moment reading the first part of this article, but yeah, this makes sense. Should hopefully help with replicating important items (like weapons and other tools) from server to client, as I’ve also had my own bugs with this. Thanks for the change!

Also, is there a way that I can enable a “Beta Feature” to instantly toggle this on, rather than it being active in Studio only? Would rather not wait to use this live, as I believe it would significantly benefit my experiences.

4 Likes

In this instance, say there is a ChildB which is always a child of PartA (never gets removed). Even though the direct parent never changes for ChildB, are you saying that this change means that it will now firstly fire a ChildRemoved(ChildB), and then re-fire a ChildAdded(ChildB)? Whereas before this change, this removal and readdition event wouldn’t have occured?

6 Likes

Yes, that is what will happen with this new behavior

3 Likes

I am a bit confused, in the example of the old behavior, you state that after the local script runs, then the server removes partA, there is a lingering reference. However, I thought once a script ended, any references were removed?

3 Likes

Astute observation: The example is just a stand-in, assume that the print statement is actually some arbitrary real work which keeps the script doing stuff and actively holding onto the reference.

4 Likes

oh ok, I … sort of … assumed that was the case, but just wanted to make sure.
Thanks for the clarification.

2 Likes

Is the current behavior maybe being exploited with the trick parenting a character to Lighting, and modifying or switching it somehow?

2 Likes

In your folder example, what happens if the client retains a reference to that folder? Is it destroyed by the client as soon as it’s parented to nil by the server or is it just parented to nil on the client as well?

If the latter, wouldn’t that be a memory issue since instances aren’t gced unless destroyed?

2 Likes

The new behavior does not call Destroy() - it just parents to nil on the Client for symmetric behavior, and like you pointed out, this could result in a memory leak.

Though doing something like Destroy() on the Client-side might be an elegant solution for some use cases, there are also definitely cases that it breaks. What if the Client intentionally has a LocalScript holding a reference to Folder, because they need it for a script? What if the Server decides to bring Folder back into the DM later in the experience, but the Client can’t because Folder’s parent has been locked via Destroy()?

We designed this change under the assumption that if a Dev does write a LocalScript to have a lingering reference to an Instance that was removed on Server, that it must be intentional.

3 Likes

But maybe we are thinking that something is the audio that Roblox uploaded

1 Like

I’m unsure if this issue is related to this update, but as of recently parenting guis from replicated storage into player gui is automatically destroying the guii in studio, but works in-game.

2 Likes

That might be related - do you have a simple repro you could share?

2 Likes

GuiBug.rbxl (34.6 KB)

3 Likes

This documentation suggests that PlayerGui gets reset whenever the Character gets loaded. If I change the LocalScript in the repro to do this:

local PlayerGui = game:GetService("Players").LocalPlayer.PlayerGui
local Gui = game:GetService("ReplicatedStorage").ScreenGui

local Player = game:GetService('Players').LocalPlayer

Player.CharacterAdded:Wait()

Gui.Parent = PlayerGui

Then the reparenting works, at least on my end.

I’m not totally sure why the repro without the wait works in-game today though…

2 Likes