feat: bg location lost notify + fix cleanupOrphanedHotGeodata
All checks were successful
/ build (map[dockerfile:./services/app/Dockerfile name:app]) (push) Successful in 1m2s
/ build (map[dockerfile:./services/web/Dockerfile name:web]) (push) Successful in 1m20s
/ build (map[dockerfile:./services/hasura/Dockerfile name:hasura]) (push) Successful in 1m33s
/ build (map[dockerfile:./services/tasks/Dockerfile name:tasks]) (push) Successful in 1m37s
/ build (map[dockerfile:./services/watchers/Dockerfile name:watchers]) (push) Successful in 1m33s
/ build (map[dockerfile:./services/api/Dockerfile name:api]) (push) Successful in 52s
/ build (map[dockerfile:./services/files/Dockerfile name:files]) (push) Successful in 1m9s
/ deploy (push) Successful in 8s

This commit is contained in:
Jo 2025-04-24 17:30:52 +02:00
parent f4313ce9ed
commit dc351b2ed6
5 changed files with 180 additions and 2 deletions

View file

@ -0,0 +1 @@
DELETE FROM "public"."enum_notification_type" WHERE "value" = 'background_geolocation_lost';

View file

@ -0,0 +1 @@
INSERT INTO "public"."enum_notification_type"("value") VALUES (E'background_geolocation_lost');

View file

@ -0,0 +1,103 @@
const { ctx } = require("@modjo/core")
const { taskCtx } = require("@modjo/microservice-worker/ctx")
const addNotification = require("~/services/add-notification")
function createBackgroundGeolocationLostNotification() {
return {
data: {
action: "background_geolocation_lost",
},
notification: {
title: "Localisation en arrière-plan désactivée",
body: "Votre localisation en arrière-plan a été désactivée. Veuillez vérifier les paramètres de l'application.",
channel: "system",
priority: "high",
actionId: "open-settings",
},
}
}
module.exports = async function () {
return Object.assign(
async function backgroundGeolocationLostNotify(params) {
const logger = taskCtx.require("logger")
const sql = ctx.require("postgres")
const { deviceId } = params
try {
// Get the user ID associated with this device
const userResult = await sql`
SELECT
"user_id" as "userId"
FROM
"device"
WHERE
id = ${deviceId}
`
if (!userResult || userResult.length === 0) {
logger.warn(
{ deviceId },
"No user found for device when sending background geolocation lost notification"
)
return
}
const { userId } = userResult[0]
// Get the FCM token for this device
const deviceResult = await sql`
SELECT
"fcm_token" as "fcmToken"
FROM
"device"
WHERE
id = ${deviceId}
`
if (!deviceResult[0]?.fcmToken) {
logger.warn(
{ deviceId, userId },
"No FCM token found for device when sending background geolocation lost notification"
)
return
}
const { fcmToken } = deviceResult[0]
// Create notification config
const notificationConfig = createBackgroundGeolocationLostNotification()
// Send notification
const { success } = await addNotification({
fcmToken,
deviceId,
userId,
type: "background_geolocation_lost",
...notificationConfig,
})
if (!success) {
throw new Error(
"Unable to send background geolocation lost notification"
)
}
logger.info(
{ deviceId, userId },
"Successfully sent background geolocation lost notification"
)
} catch (error) {
logger.error(
{ deviceId, error },
"Error sending background geolocation lost notification"
)
}
},
{
dedupOptions: { enabled: true },
}
)
}

View file

@ -9,4 +9,5 @@ module.exports = {
RELATIVE_ALLOW_ASK_NOTIFY: "relativeAllowAskNotify",
RELATIVE_INVITATION_NOTIFY: "relativeInvitationNotify",
ALERT_CALL_EMERGENCY_INFO_NOTIFY: "alertCallEmergencyInfoNotify",
BACKGROUND_GEOLOCATION_LOST_NOTIFY: "backgroundGeolocationLostNotify",
}

View file

@ -3,6 +3,7 @@ const { ctx } = require("@modjo/core")
const ms = require("ms")
const cron = require("~/libs/cron")
const { DEVICE_GEODATA_MAX_AGE } = require("~/constants/time")
const tasks = require("~/tasks")
const CLEANUP_CRON = "0 */1 * * *" // Run every hour
const MAX_PARALLEL_PROCESS = 10
@ -15,10 +16,61 @@ module.exports = async function () {
const logger = ctx.require("logger")
const redisCold = ctx.require("keydbColdGeodata")
const redisHot = ctx.require("redisHotGeodata")
const { addTask } = ctx.require("amqp")
return async function geodataCleanupCron() {
logger.info("watcher geodataCleanupCron: daemon started")
// this is temporary function (fixing actual data)
async function cleanupOrphanedHotGeodata() {
// Get all devices from hot storage
const hotDevices = new Set()
let hotCursor = "0"
do {
// Use zscan to iterate through the sorted set
const [newCursor, items] = await redisHot.zscan(
HOTGEODATA_KEY,
hotCursor,
"COUNT",
"100"
)
hotCursor = newCursor
// Extract device IDs (every other item in the result is a score)
for (let i = 0; i < items.length; i += 2) {
hotDevices.add(items[i])
}
} while (hotCursor !== "0")
// Process each hot device
await async.eachLimit(
[...hotDevices],
MAX_PARALLEL_PROCESS,
async (deviceId) => {
try {
// Check if device exists in cold storage
const coldKey = `${COLDGEODATA_DEVICE_KEY_PREFIX}${deviceId}`
const exists = await redisCold.exists(coldKey)
// If device doesn't exist in cold storage, remove it from hot storage
if (!exists) {
await redisHot.zrem(HOTGEODATA_KEY, deviceId)
logger.debug(
{ deviceId },
"Removed orphaned device data from hot storage (not found in cold storage)"
)
}
} catch (error) {
logger.error(
{ error, deviceId },
"Error checking orphaned device data"
)
}
}
)
}
// TODO optimize by removing memory accumulation (cursor iteration to make it scalable)
async function cleanupOldGeodata() {
const now = Math.floor(Date.now() / 1000) // Current time in seconds
const coldKeys = new Set() // Store cold geodata keys
@ -68,6 +120,23 @@ module.exports = async function () {
{ deviceId, age: `${Math.floor(age / 3600)}h` },
"Removed old device data from hot storage and marked as cleaned in cold storage"
)
// Enqueue task to notify user about lost background geolocation
try {
await addTask(tasks.BACKGROUND_GEOLOCATION_LOST_NOTIFY, {
deviceId,
})
logger.info(
{ deviceId },
"Enqueued background geolocation lost notification task"
)
} catch (notifError) {
logger.error(
{ deviceId, error: notifError },
"Error enqueueing background geolocation lost notification task"
)
}
} catch (error) {
logger.error({ error, deviceId }, "Error cleaning device data")
}
@ -82,7 +151,10 @@ module.exports = async function () {
)
}
// Schedule the cleanup to run periodically
cron.schedule(CLEANUP_CRON, cleanupOldGeodata)
// Schedule both cleanup functions to run periodically
cron.schedule(CLEANUP_CRON, async () => {
await cleanupOldGeodata()
await cleanupOrphanedHotGeodata()
})
}
}