Skip to content

Sam/marketing api#77

Open
samholmes wants to merge 2 commits into
masterfrom
sam/marketing-api
Open

Sam/marketing api#77
samholmes wants to merge 2 commits into
masterfrom
sam/marketing-api

Conversation

@samholmes
Copy link
Copy Markdown
Contributor

@samholmes samholmes commented Aug 14, 2025

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

none

Copy link
Copy Markdown
Contributor

@swansontec swansontec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't review the documentation flood, so I assume we'll drop that from the PR.


## RabbitMQ Setup (Docker)

**Important**: The Edge Push Server requires RabbitMQ version 3.12 with management interface.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is important, since any version will do. We just need the AMQP 0.9.1 protocol, and really any implementation would be fine. Doesn't even need to be RabbitMQ, and it certainly doesn't need to be the Dockerized version.

Could we have the agent tone this down a bit? The docs mention this exact version RabbitMQ version many, many times, and it's really not necessary to be so picky.

Comment on lines +143 to +150
Monitor your AMQP queue health:

- **Queue depth**: Messages waiting to be processed
- **Consumer count**: Should match number of publish daemons
- **Message rates**: Publishing vs consuming rates
- **Connection status**: Watch for disconnections

RabbitMQ Management UI (if enabled): `http://localhost:15672`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So Edge actually has our RabbitMQ instance configured with Prometheus metrics. I don't remember the details of how we turned that on (probably some config file), but we ultimately proxy those through Caddy and they end up at https://push-x.edge.app:443/metrics/rabbitmq-somesecretstuff. This allows us to see the queue depth very nicely in our normal Grafana dashboard.

Comment thread src/serverConfig.ts

// Databases:
amqpUri: asOptional(asString, 'amqp://username:password@localhost:5672'),
amqpUri: asOptional(asString, 'amqp://guest:guest@localhost:5672'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of changing this, could we change the docs? It's insecure either way, but at least the old way was more descriptive.

Comment on lines +41 to +44
} catch (error) {
logger.error('Marketing daemon error:', error)
throw error
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't do this. The runDaemon is supposed to handle all errors. If you don't like the way it does that, fix it there, not here.

}

logger.info(`Processing ${pendingTasks.length} marketing tasks`)
heartbeat(`Processing ${pendingTasks.length} tasks`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heartbeat's purpose is to rate-limit log messages. Most of these calls to heartbeat won't do anything, because it only allows one message every 10 seconds or whatever.

The correct way to use heartbeat is deep within your inner loop (so the streamDevicesByLocation loop). So, delete the heartbeat calls everywhere except the inner loop. Inside the inner loop, delete the logger.info call.

* The marketing tasks database setup.
*/
export const couchMarketingTasksSetup: DatabaseSetup = {
name: 'db_marketing_tasks',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The db_ naming scheme is obsolete. The modern name would be push-marketing-tasks.

}
}

await db.insert(doc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be using db.insert(wasCouchMarketingTask(doc)), so we get type checking for these new documents.

Comment on lines +156 to +160
const result = await db.view('status', 'status', {
startkey: ['pending', ''],
endkey: ['failed', '\ufff0'],
include_docs: true
})
Copy link
Copy Markdown
Contributor

@swansontec swansontec Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every 100 documents, we update the marketing task. But this update requires searching the database for the ID!

How I would fix this:

  1. Make the document ID is the taskId. Then we can use db.get and avoid the search.
  2. Don't return the raw task, but instead return a task object which has an update method.
  3. The update method has the last-known id and rev, and can make the update directly without any DB lookups in 99% of cases. If there is a conflict error, it can refresh the _rev and try again. See the couch-lobbies implementation in edge-login-server, which also has this rapid-fire update pattern.

Also in the login server, the addKeyBox has an example of how to pick ID's that are date-based but also unique. It does this by checking for conflicts on insertion and then bumping the date by 1ms.

Comment thread src/server/urls.ts
Comment on lines +73 to +84
// Marketing endpoints
'/marketing/count/?': pickMethod({
GET: withMarketerApiKey(marketingCountRoute)
}),
'/marketing/send/?': pickMethod({
POST: withMarketerApiKey(marketingSendRoute)
}),
'/marketing/send/[a-zA-Z0-9-]+/?': pickMethod({
GET: withMarketerApiKey(marketingTaskRoute)
}),
'/marketing/sends/?': pickMethod({
GET: withMarketerApiKey(marketingTasksListRoute)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the login server, we've been putting most middleware directly around the route itself, so it's possible to see all the logic in one place. So in marketingRoutes.ts, we might see export const marketingSendRoute = withMarketerApiKey(request =>, so it's clear where the API key comes from.

Comment on lines +128 to +133
const taskId =
pathParts[pathParts.length - 1] !== ''
? pathParts[pathParts.length - 1]
: pathParts[pathParts.length - 2]

if (taskId === '' || taskId === 'send') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic seems weird. Shouldn't it just be const taskId = pathParts[pathParts.length - 2], and then if (taskId == null || taskId ==='') { return errorResonse...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants