Using ProfileService to track copy count

I am trying to track the number of copies spawned of specific types of pets. I have run into multiple technical issues, and I think it’s worth the while to list them.

Technical Issues:

  1. Roblox has an experience limit of 6 seconds for write requests.
  2. Any server can spawn in a new pet and that pet needs to be accounted for.
  3. Copy counts cannot be updated immediately due to technical issue #1.

Potential Solution: Have one server act as a host to complete the updates, and switch hosts if that server dies.

My implementation so far is to use a combination of ProfileService and MessagingService. ProfileService provides session locking, but it doesn’t allow you to check if the server that has its session locked still exists. Because this is not a player profile and is instead a server profile, any server can try session locking. To avoid the case where the servers compete to have the session for the updating copies I need to check if the server who has the session still exists.

My code snippet is as follows:

local PLUSHIE_HOST_REQUEST = "PlushieHostRequest"
local PLUSHIE_HOST_RESPONSE = "PlushieHostResponse"
local success, connection = pcall(function()
	return MessagingService:SubscribeAsync(PLUSHIE_HOST_REQUEST, function(message)
		local senderJobId = message.Data.SenderJobId
		local receiverJobId = message.Data.ReceiverJobId
		if receiverJobId == game.JobId then
			-- Respond to the server that sent this message telling it that
			-- this server does, in fact, exist.
			print(string.format("Server Job %s is checking if this server still exists.", senderJobId))
			local success, result = pcall(function()
				MessagingService:PublishAsync(PLUSHIE_HOST_RESPONSE, {
					ReceiverJobId = senderJobId,
					SenderJobId = receiverJobId
				})
			end)
		end
	end)
end)

local isHostAlive = false
local success, connection = pcall(function()
	return MessagingService:SubscribeAsync(PLUSHIE_HOST_RESPONSE, function(message)
		local senderJobId = message.Data.SenderJobId
		local receiverJobId = message.Data.ReceiverJobId
		if receiverJobId == game.JobId then
			print(string.format("Server Job %s still exists.", senderJobId))
			isHostAlive = true
		end
	end)
end)

for plushieType, _ in ipairs(Plushies) do
	local profile = ProfileStore:LoadProfileAsync("Plushie_" .. plushieType, function(placeId, gameJobId)
		local sendTime = os.time()
		local timeSinceSent = 0
		local success, result = pcall(function()
			print(gameJobId)
			MessagingService:PublishAsync(PLUSHIE_HOST_REQUEST, {
				SenderJobId = game.JobId,
				ReceiverJobId = gameJobId
			})
		end)
		
		if not success then
			warn("The message to determine the host has failed. Cancelling load.")
			return "Cancel"
		end
		
		print("Sent a message to the host server. Waiting for response.")
		while not isHostAlive and timeSinceSent <= 5 do
			timeSinceSent = os.time() - sendTime
			task.wait()
		end
		
		if isHostAlive then
			print(string.format("Plushie type %s's host server is still alive. Cancelling load.", plushieType))
			isHostAlive = false
			return "Cancel"
		end

		print(string.format("Plushie type %s's host server is dead. Forcing load.", plushieType))
		return "Steal"
	end)
	
	if profile == nil then
		-- Another server has already claimed a lock on this profile.
		print(string.format("Plushie type %d is hosted by another server.", plushieType))
		continue
	end

	PlushieProfiles[plushieType] = profile
	print(string.format("Plushie type %d is now hosted by this server.", plushieType))

	profile:Reconcile()
	profile:ListenToRelease(function()
		PlushieProfiles[plushieType] = nil
		print(string.format("Plushie type %d is now being hosted by another server.", plushieType))
	end)

	local globalUpdates = profile.GlobalUpdates
	for i, update in ipairs(globalUpdates:GetActiveUpdates()) do
		local updateId, updateData = unpack(update)
		globalUpdates:LockActiveUpdate(updateId)
	end

	for i, update in ipairs(globalUpdates:GetLockedUpdates()) do
		handleLockedUpdate(globalUpdates, unpack(update))
	end

	globalUpdates:ListenToNewActiveUpdate(function(updateId, updateData)
		globalUpdates:LockActiveUpdate(updateId)
	end)

	globalUpdates:ListenToNewLockedUpdate(function(updateId, updateData)
		handleLockedUpdate(globalUpdates, updateId, updateData)
	end)
end

I believe that the “race” condition can still occur if every server sees that the host server no longer exists. Feedback and possible solutions are greatly appreciated!

You could update the number every minute. Create a temporary table that gets cleared after successfully updating the PetsAmountNumber to the database. Meanwhile the temporary table stores the pets that were created during the 1 minute interval.