Skip to content

Commit af90746

Browse files
Merge pull request #798 from decentraland/feat/support-world-and-world-scenes-undeployment
feat: Add support fot world & world scenes undeployment
2 parents 1703c7b + 1dfe0b2 commit af90746

File tree

11 files changed

+943
-131
lines changed

11 files changed

+943
-131
lines changed

package-lock.json

Lines changed: 186 additions & 125 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"dependencies": {
1212
"@contentful/rich-text-react-renderer": "^16.1.6",
1313
"@dcl/hooks": "^1.0.0",
14-
"@dcl/schemas": "^22.4.0",
14+
"@dcl/schemas": "^24.0.0",
1515
"@sentry/browser": "^7.120.3",
1616
"@well-known-components/pushable-channel": "^1.0.3",
1717
"abort-controller": "^3.0.0",
@@ -100,7 +100,7 @@
100100
"workbox-cli": "^6.5.2"
101101
},
102102
"overrides": {
103-
"@dcl/schemas": "^22.4.0"
103+
"@dcl/schemas": "^24.0.0"
104104
},
105105
"keywords": [
106106
"gatsby"

src/entities/CheckScenes/task/consumer.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Events } from "@dcl/schemas/dist/platform/events/base"
33
import {
44
WorldScenesUndeploymentEvent,
55
WorldSettingsChangedEvent,
6+
WorldUndeploymentEvent,
67
} from "@dcl/schemas/dist/platform/events/world"
78
import { SQS } from "aws-sdk"
89
import logger from "decentraland-gatsby/dist/entities/Development/logger"
@@ -22,6 +23,7 @@ export type WorldSqsMessage =
2223
| DeploymentToSqs
2324
| WorldSettingsChangedEvent
2425
| WorldScenesUndeploymentEvent
26+
| WorldUndeploymentEvent
2527

2628
/** Type guard to check if message is a deployment event */
2729
export function isDeploymentEvent(
@@ -42,8 +44,8 @@ export function isSettingsChangedEvent(
4244
)
4345
}
4446

45-
/** Type guard to check if message is an undeployment event */
46-
export function isUndeploymentEvent(
47+
/** Type guard to check if message is a scene undeployment event */
48+
export function isScenesUndeploymentEvent(
4749
message: WorldSqsMessage
4850
): message is WorldScenesUndeploymentEvent {
4951
return (
@@ -54,6 +56,18 @@ export function isUndeploymentEvent(
5456
)
5557
}
5658

59+
/** Type guard to check if message is a full world undeployment event */
60+
export function isWorldUndeploymentEvent(
61+
message: WorldSqsMessage
62+
): message is WorldUndeploymentEvent {
63+
return (
64+
"type" in message &&
65+
message.type === Events.Type.WORLD &&
66+
"subType" in message &&
67+
message.subType === Events.SubType.Worlds.WORLD_UNDEPLOYMENT
68+
)
69+
}
70+
5771
export interface TaskQueueMessage {
5872
id: string
5973
}
@@ -136,10 +150,14 @@ export class SQSConsumer {
136150
errorContext = `<${body.contentServerUrls}/contents/${body.entity.entityId}|${body.entity.entityId}>`
137151
} else if (isSettingsChangedEvent(body)) {
138152
errorContext = `WorldSettingsChanged: ${body.key}`
139-
} else if (isUndeploymentEvent(body)) {
153+
} else if (isScenesUndeploymentEvent(body)) {
140154
errorContext = `WorldScenesUndeployment: ${
141155
body.key
142-
} - entityIds: ${body.metadata.entityIds.join(", ")}`
156+
} - scenes: ${body.metadata.scenes
157+
.map((s) => s.entityId)
158+
.join(", ")}`
159+
} else if (isWorldUndeploymentEvent(body)) {
160+
errorContext = `WorldUndeployment: ${body.metadata.worldName}`
143161
}
144162

145163
notifyError([err.toString(), errorContext])
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { WorldScenesUndeploymentEvent } from "@dcl/schemas/dist/platform/events/world"
2+
import logger from "decentraland-gatsby/dist/entities/Development/logger"
3+
4+
import PlaceModel from "../../Place/model"
5+
import { notifyError } from "../../Slack/utils"
6+
7+
/**
8+
* Handles WorldScenesUndeploymentEvent from the worlds content server.
9+
* Deletes the place records corresponding to the undeployed scenes,
10+
* identified by world name and each scene's base parcel.
11+
*/
12+
export async function handleWorldScenesUndeployment(
13+
event: WorldScenesUndeploymentEvent
14+
): Promise<void> {
15+
const worldName = event.metadata.worldName
16+
17+
if (!worldName) {
18+
logger.error("WorldScenesUndeploymentEvent missing world name")
19+
return
20+
}
21+
22+
const { scenes } = event.metadata
23+
24+
if (!scenes || scenes.length === 0) {
25+
logger.error("WorldScenesUndeploymentEvent has no scenes")
26+
return
27+
}
28+
29+
const loggerExtended = logger.extend({
30+
worldName,
31+
sceneCount: scenes.length,
32+
eventType: "WorldScenesUndeploymentEvent",
33+
})
34+
35+
try {
36+
const basePositions = scenes.map((scene) => scene.baseParcel)
37+
38+
loggerExtended.log(
39+
`Processing scene undeployment for world: ${worldName}, parcels: ${basePositions.join(
40+
", "
41+
)}`
42+
)
43+
44+
await PlaceModel.deleteByWorldIdAndPositions(
45+
worldName,
46+
basePositions,
47+
event.timestamp
48+
)
49+
50+
loggerExtended.log(
51+
`Deleted place records for world: ${worldName} at positions: ${basePositions.join(
52+
", "
53+
)}`
54+
)
55+
} catch (error: any) {
56+
loggerExtended.error(
57+
`Error handling WorldScenesUndeploymentEvent for ${worldName}: ${error.message}`
58+
)
59+
notifyError([
60+
`Error handling WorldScenesUndeploymentEvent`,
61+
`World: ${worldName}`,
62+
error.message,
63+
])
64+
throw error
65+
}
66+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { WorldUndeploymentEvent } from "@dcl/schemas/dist/platform/events/world"
2+
import logger from "decentraland-gatsby/dist/entities/Development/logger"
3+
4+
import PlaceModel from "../../Place/model"
5+
import { notifyError } from "../../Slack/utils"
6+
7+
/**
8+
* Handles WorldUndeploymentEvent from the worlds content server.
9+
* Deletes all place records associated with the undeployed world.
10+
* The world entity itself is not deleted -- it simply won't appear
11+
* in queries once it has no associated places.
12+
*/
13+
export async function handleWorldUndeployment(
14+
event: WorldUndeploymentEvent
15+
): Promise<void> {
16+
const worldName = event.metadata.worldName
17+
18+
if (!worldName) {
19+
logger.error("WorldUndeploymentEvent missing world name")
20+
return
21+
}
22+
23+
const loggerExtended = logger.extend({
24+
worldName,
25+
eventType: "WorldUndeploymentEvent",
26+
})
27+
28+
try {
29+
loggerExtended.log(`Processing world undeployment for world: ${worldName}`)
30+
31+
await PlaceModel.deleteByWorldId(worldName, event.timestamp)
32+
33+
loggerExtended.log(`Deleted all place records for world: ${worldName}`)
34+
} catch (error: any) {
35+
loggerExtended.error(
36+
`Error handling WorldUndeploymentEvent for ${worldName}: ${error.message}`
37+
)
38+
notifyError([
39+
`Error handling WorldUndeploymentEvent`,
40+
`World: ${worldName}`,
41+
error.message,
42+
])
43+
throw error
44+
}
45+
}

src/entities/CheckScenes/task/taskRunnerDispatcher.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import logger from "decentraland-gatsby/dist/entities/Development/logger"
33
import {
44
WorldSqsMessage,
55
isDeploymentEvent,
6+
isScenesUndeploymentEvent,
67
isSettingsChangedEvent,
8+
isWorldUndeploymentEvent,
79
} from "./consumer"
10+
import { handleWorldScenesUndeployment } from "./handleWorldScenesUndeployment"
811
import { handleWorldSettingsChanged } from "./handleWorldSettingsChanged"
12+
import { handleWorldUndeployment } from "./handleWorldUndeployment"
913
import { taskRunnerSqs } from "./taskRunnerSqs"
1014

1115
/**
@@ -20,6 +24,20 @@ export async function taskRunnerDispatcher(
2024
return handleWorldSettingsChanged(message)
2125
}
2226

27+
if (isWorldUndeploymentEvent(message)) {
28+
logger.log(
29+
`Processing WorldUndeploymentEvent for world: ${message.metadata.worldName}`
30+
)
31+
return handleWorldUndeployment(message)
32+
}
33+
34+
if (isScenesUndeploymentEvent(message)) {
35+
logger.log(
36+
`Processing WorldScenesUndeploymentEvent for world: ${message.metadata.worldName}`
37+
)
38+
return handleWorldScenesUndeployment(message)
39+
}
40+
2341
if (isDeploymentEvent(message)) {
2442
logger.log(
2543
`Processing DeploymentEvent for entity: ${message.entity.entityId}`

src/entities/Place/model.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,48 @@ export default class PlaceModel extends Model<PlaceAttributes> {
469469
)
470470
}
471471

472+
/**
473+
* Delete all place records associated with a world that were deployed
474+
* before the given event timestamp. This prevents stale undeployment
475+
* events from removing places that were re-deployed after the event
476+
* was emitted.
477+
*/
478+
static async deleteByWorldId(
479+
worldId: string,
480+
eventTimestamp: number
481+
): Promise<void> {
482+
const normalizedWorldId = worldId.toLowerCase()
483+
const eventDate = new Date(eventTimestamp)
484+
const sql = SQL`
485+
DELETE FROM ${table(this)}
486+
WHERE "world_id" = ${normalizedWorldId}
487+
AND "deployed_at" < ${eventDate}
488+
`
489+
await this.namedQuery("delete_by_world_id", sql)
490+
}
491+
492+
/**
493+
* Delete place records matching a world and specific base positions
494+
* that were deployed before the given event timestamp. This prevents
495+
* stale undeployment events from removing places that were re-deployed
496+
* after the event was emitted.
497+
*/
498+
static async deleteByWorldIdAndPositions(
499+
worldId: string,
500+
basePositions: string[],
501+
eventTimestamp: number
502+
): Promise<void> {
503+
const normalizedWorldId = worldId.toLowerCase()
504+
const eventDate = new Date(eventTimestamp)
505+
const sql = SQL`
506+
DELETE FROM ${table(this)}
507+
WHERE "world_id" = ${normalizedWorldId}
508+
AND "base_position" = ANY(${basePositions})
509+
AND "deployed_at" < ${eventDate}
510+
`
511+
await this.namedQuery("delete_by_world_id_and_positions", sql)
512+
}
513+
472514
static async updateFavorites(placeId: string) {
473515
const sql = buildUpdateFavoritesQuery(this, placeId)
474516
return this.namedQuery("update_favorites", sql)

src/entities/World/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export default class WorldModel extends Model<WorldAttributes> {
205205
${conditional(!!options.disabled, SQL`AND w.disabled IS TRUE`)}
206206
${conditional(!options.disabled, SQL`AND w.disabled IS FALSE`)}
207207
AND w.show_in_places IS TRUE
208+
AND EXISTS (SELECT 1 FROM places p WHERE p.world_id = w.id)
208209
${conditional(
209210
options.names.length > 0,
210211
SQL`AND w.id = ANY(${options.names.map((name) =>
@@ -272,6 +273,7 @@ export default class WorldModel extends Model<WorldAttributes> {
272273
${conditional(!!options.disabled, SQL`AND w.disabled IS TRUE`)}
273274
${conditional(!options.disabled, SQL`AND w.disabled IS FALSE`)}
274275
AND w.show_in_places IS TRUE
276+
AND EXISTS (SELECT 1 FROM places p WHERE p.world_id = w.id)
275277
${conditional(
276278
options.names.length > 0,
277279
SQL`AND w.id = ANY(${options.names.map((name) =>

test/fixtures/undeploymentEvent.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Events } from "@dcl/schemas/dist/platform/events/base"
2+
import {
3+
WorldScenesUndeploymentEvent,
4+
WorldUndeploymentEvent,
5+
} from "@dcl/schemas/dist/platform/events/world"
6+
7+
/**
8+
* Creates a WorldUndeploymentEvent for a full world undeployment.
9+
*/
10+
export function createWorldUndeploymentEvent(
11+
worldName: string,
12+
options: { timestamp?: number } = {}
13+
): WorldUndeploymentEvent {
14+
return {
15+
type: Events.Type.WORLD,
16+
subType: Events.SubType.Worlds.WORLD_UNDEPLOYMENT,
17+
key: worldName,
18+
timestamp: options.timestamp ?? Date.now(),
19+
metadata: {
20+
worldName,
21+
},
22+
}
23+
}
24+
25+
/**
26+
* Creates a WorldScenesUndeploymentEvent for undeploying specific scenes.
27+
*/
28+
export function createWorldScenesUndeploymentEvent(
29+
worldName: string,
30+
scenes: Array<{ entityId: string; baseParcel: string }>,
31+
options: { timestamp?: number } = {}
32+
): WorldScenesUndeploymentEvent {
33+
return {
34+
type: Events.Type.WORLD,
35+
subType: Events.SubType.Worlds.WORLD_SCENES_UNDEPLOYMENT,
36+
key: worldName,
37+
timestamp: options.timestamp ?? Date.now(),
38+
metadata: {
39+
worldName,
40+
scenes,
41+
},
42+
}
43+
}

0 commit comments

Comments
 (0)