feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
166
internal/notifier/producer.go
Обычный файл
166
internal/notifier/producer.go
Обычный файл
@@ -0,0 +1,166 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
"rsgit.ru/rsmon/rsmon/internal/notifyrender"
|
||||
"rsgit.ru/rsmon/rsmon/internal/wire"
|
||||
)
|
||||
|
||||
// langEN is the wire-side default language tag used when a Message carries
|
||||
// no language hint of its own. Centralized so the literal does not appear
|
||||
// three or more times across this package (goconst).
|
||||
const langEN = "en"
|
||||
|
||||
// ContactKindToMethod maps the legacy Contact.Kind enum used by the sender onto
|
||||
// the worker notification_method enum introduced in
|
||||
// docs/plans/worker-notifier-mvp.md section 4.3. sms/voice remain placeholders
|
||||
// until phase 4.
|
||||
//
|
||||
//nolint:goconst // match arm values must be the wire-method enum literals
|
||||
func ContactKindToMethod(kind string) string {
|
||||
switch kind {
|
||||
case "email":
|
||||
return "email"
|
||||
case "telegram_private", "telegram_group":
|
||||
return "telegram"
|
||||
case "webhook":
|
||||
return "webhook"
|
||||
case "mattermost":
|
||||
return "mattermost"
|
||||
case "sms":
|
||||
return "sms"
|
||||
case "voice":
|
||||
return "voice"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RenderNotificationContent pre-renders subject + bodies for one Message using
|
||||
// the existing internal/sender/get_content.go helpers. The result is what the
|
||||
// worker binary consumes directly so it does not need access to Message/Event
|
||||
// rows, workdays, or NotificationDayStart logic on the data plane.
|
||||
//
|
||||
// Returns the four bodies (subject, text, markdown, html). The caller is
|
||||
// responsible for passing them through to EnqueueNotificationTask.
|
||||
func RenderNotificationContent(msg *models.Message, now time.Time) (subject, bodyText, bodyMarkdown, bodyHTML string, err error) {
|
||||
if msg == nil {
|
||||
return "", "", "", "", errors.New("notifier: nil message")
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("notifier: render panicked")
|
||||
}
|
||||
}()
|
||||
sbuf, tbuf, mbuf, hbuf := notifyrender.GetContent(msg, now)
|
||||
return sbuf.String(), tbuf.String(), mbuf.String(), hbuf.String(), nil
|
||||
}
|
||||
|
||||
// EnqueueNotificationTaskFromMessage is the producer-side hook called from
|
||||
// performEvents (or its replacement). It builds a wire.NotificationTask from the
|
||||
// freshly created Message and enqueues one Task row keyed by the stable
|
||||
// (notification, contact, first-event) idempotency key.
|
||||
//
|
||||
// If the producer's authorization precheck fails the function returns an error:
|
||||
// worker notification tasks are now the only delivery path.
|
||||
func EnqueueNotificationTaskFromMessage(n *models.Notification, c *models.Contact, msg *models.Message) (*models.Task, error) {
|
||||
return enqueueNotificationTaskFromMessageTx(models.DB(), n, c, msg)
|
||||
}
|
||||
|
||||
// enqueueNotificationTaskFromMessageTx keeps message creation and durable task
|
||||
// production in the caller's notifier transaction.
|
||||
func enqueueNotificationTaskFromMessageTx(tx *gorm.DB, n *models.Notification, c *models.Contact, msg *models.Message) (*models.Task, error) {
|
||||
if msg == nil || n == nil || c == nil {
|
||||
return nil, errors.New("notifier: nil message/notification/contact")
|
||||
}
|
||||
if len(msg.Events) == 0 {
|
||||
return nil, errors.New("notifier: message has no events (exp messages go through a separate path)")
|
||||
}
|
||||
|
||||
method := ContactKindToMethod(c.Kind)
|
||||
if method == "" {
|
||||
log.Printf("notifier: unknown contact kind %q for contact %d, skipping enqueue", c.Kind, c.ID)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
subject, bodyText, bodyMarkdown, bodyHTML, err := RenderNotificationContent(msg, now)
|
||||
if err != nil {
|
||||
log.Printf("notifier: render content failed for message %d: %v", msg.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkID := msg.CheckID
|
||||
monitorID := msg.Events[0].MonitorID
|
||||
task := wire.NotificationTask{
|
||||
AccountID: n.AccountID,
|
||||
MessageID: msg.ID,
|
||||
NotificationID: n.ID,
|
||||
EventIDs: eventIDs(msg),
|
||||
CheckID: checkID,
|
||||
MonitorID: &monitorID,
|
||||
Method: method,
|
||||
Contact: wire.NotificationContact{
|
||||
ID: c.ID,
|
||||
Kind: c.Kind,
|
||||
Value: c.Value,
|
||||
Name: c.Name,
|
||||
},
|
||||
Subject: subject,
|
||||
BodyText: bodyText,
|
||||
BodyMarkdown: bodyMarkdown,
|
||||
BodyHTML: bodyHTML,
|
||||
Language: langEN,
|
||||
MessageKind: msg.Kind,
|
||||
}
|
||||
payload, err := json.Marshal(task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contactID := c.ID
|
||||
monitorPtr := task.MonitorID
|
||||
checkPtr := task.CheckID
|
||||
messagePtr := msg.ID
|
||||
|
||||
taskRow, err := models.EnqueueNotificationTaskTx(tx, &models.EnqueueNotificationTaskInput{
|
||||
AccountID: n.AccountID,
|
||||
NotificationID: n.ID,
|
||||
ContactID: contactID,
|
||||
MessageID: &messagePtr,
|
||||
MonitorID: monitorPtr,
|
||||
CheckID: checkPtr,
|
||||
EventIDs: task.EventIDs,
|
||||
Method: method,
|
||||
Subject: subject,
|
||||
BodyText: bodyText,
|
||||
BodyHTML: bodyHTML,
|
||||
BodyMarkdown: bodyMarkdown,
|
||||
Language: task.Language,
|
||||
MessageKind: msg.Kind,
|
||||
NotBefore: now,
|
||||
Payload: payload,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf(
|
||||
"notifier: task enqueued id=%d job=%s kind=notification account=%d method=%s notification=%d contact=%d event=%d idempotency=%s",
|
||||
taskRow.ID, taskRow.JobID, n.AccountID, method, n.ID, c.ID, task.EventIDs[0], taskRow.IdempotencyKey,
|
||||
)
|
||||
return taskRow, nil
|
||||
}
|
||||
|
||||
func eventIDs(msg *models.Message) []int64 {
|
||||
out := make([]int64, 0, len(msg.Events))
|
||||
for _, e := range msg.Events { //nolint:gocritic // range copy is acceptable here
|
||||
out = append(out, e.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Ссылка в новой задаче
Block a user