feat(server/desktop): ingest desktop account health reports
Add a POST /desktop/accounts/health-reports endpoint that buffers reports in Redis, dedupes via signature, and publishes change events to a new desktop.account.health RabbitMQ queue. A sink worker drains the queue, and the account service overlays runtime health onto database health when serving desktop accounts. Publish job creation now consults runtime health so a stale DB row no longer blocks publishing once the desktop client reports it healthy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,50 +58,56 @@ type RedisConfig struct {
|
||||
}
|
||||
|
||||
type RabbitMQConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
MonitorResultExchange string `mapstructure:"monitor_result_exchange"`
|
||||
MonitorResultRoutingKey string `mapstructure:"monitor_result_routing_key"`
|
||||
MonitorResultQueue string `mapstructure:"monitor_result_queue"`
|
||||
MonitorResultDLX string `mapstructure:"monitor_result_dlx"`
|
||||
MonitorResultDLQ string `mapstructure:"monitor_result_dlq"`
|
||||
MonitorResultDLQRouteKey string `mapstructure:"monitor_result_dlq_routing_key"`
|
||||
MonitorProjectionExchange string `mapstructure:"monitor_projection_exchange"`
|
||||
MonitorProjectionRoutingKey string `mapstructure:"monitor_projection_routing_key"`
|
||||
MonitorProjectionQueue string `mapstructure:"monitor_projection_queue"`
|
||||
MonitorProjectionDLX string `mapstructure:"monitor_projection_dlx"`
|
||||
MonitorProjectionDLQ string `mapstructure:"monitor_projection_dlq"`
|
||||
MonitorProjectionDLQRouteKey string `mapstructure:"monitor_projection_dlq_routing_key"`
|
||||
GenerationExchange string `mapstructure:"generation_exchange"`
|
||||
GenerationRoutingKey string `mapstructure:"generation_routing_key"`
|
||||
GenerationQueue string `mapstructure:"generation_queue"`
|
||||
GenerationDLX string `mapstructure:"generation_dlx"`
|
||||
GenerationDLQ string `mapstructure:"generation_dlq"`
|
||||
GenerationDLQRouteKey string `mapstructure:"generation_dlq_routing_key"`
|
||||
GenerationEventExchange string `mapstructure:"generation_event_exchange"`
|
||||
DesktopTaskEventExchange string `mapstructure:"desktop_task_event_exchange"`
|
||||
DesktopDispatchExchange string `mapstructure:"desktop_dispatch_exchange"`
|
||||
DesktopDispatchPublishPrefix string `mapstructure:"desktop_dispatch_publish_prefix"`
|
||||
DesktopDispatchMonitorPrefix string `mapstructure:"desktop_dispatch_monitor_prefix"`
|
||||
TemplateAssistExchange string `mapstructure:"template_assist_exchange"`
|
||||
TemplateAssistRoutingKey string `mapstructure:"template_assist_routing_key"`
|
||||
TemplateAssistQueue string `mapstructure:"template_assist_queue"`
|
||||
TemplateAssistDLX string `mapstructure:"template_assist_dlx"`
|
||||
TemplateAssistDLQ string `mapstructure:"template_assist_dlq"`
|
||||
TemplateAssistDLQRouteKey string `mapstructure:"template_assist_dlq_routing_key"`
|
||||
KolAssistExchange string `mapstructure:"kol_assist_exchange"`
|
||||
KolAssistRoutingKey string `mapstructure:"kol_assist_routing_key"`
|
||||
KolAssistQueue string `mapstructure:"kol_assist_queue"`
|
||||
KolAssistDLX string `mapstructure:"kol_assist_dlx"`
|
||||
KolAssistDLQ string `mapstructure:"kol_assist_dlq"`
|
||||
KolAssistDLQRouteKey string `mapstructure:"kol_assist_dlq_routing_key"`
|
||||
ImageAssetExchange string `mapstructure:"image_asset_exchange"`
|
||||
ImageAssetRoutingKey string `mapstructure:"image_asset_routing_key"`
|
||||
ImageAssetQueue string `mapstructure:"image_asset_queue"`
|
||||
ImageAssetDLX string `mapstructure:"image_asset_dlx"`
|
||||
ImageAssetDLQ string `mapstructure:"image_asset_dlq"`
|
||||
ImageAssetDLQRouteKey string `mapstructure:"image_asset_dlq_routing_key"`
|
||||
ConsumerPrefetch int `mapstructure:"consumer_prefetch"`
|
||||
PublishChannelPoolSize int `mapstructure:"publish_channel_pool_size"`
|
||||
URL string `mapstructure:"url"`
|
||||
MonitorResultExchange string `mapstructure:"monitor_result_exchange"`
|
||||
MonitorResultRoutingKey string `mapstructure:"monitor_result_routing_key"`
|
||||
MonitorResultQueue string `mapstructure:"monitor_result_queue"`
|
||||
MonitorResultDLX string `mapstructure:"monitor_result_dlx"`
|
||||
MonitorResultDLQ string `mapstructure:"monitor_result_dlq"`
|
||||
MonitorResultDLQRouteKey string `mapstructure:"monitor_result_dlq_routing_key"`
|
||||
MonitorProjectionExchange string `mapstructure:"monitor_projection_exchange"`
|
||||
MonitorProjectionRoutingKey string `mapstructure:"monitor_projection_routing_key"`
|
||||
MonitorProjectionQueue string `mapstructure:"monitor_projection_queue"`
|
||||
MonitorProjectionDLX string `mapstructure:"monitor_projection_dlx"`
|
||||
MonitorProjectionDLQ string `mapstructure:"monitor_projection_dlq"`
|
||||
MonitorProjectionDLQRouteKey string `mapstructure:"monitor_projection_dlq_routing_key"`
|
||||
GenerationExchange string `mapstructure:"generation_exchange"`
|
||||
GenerationRoutingKey string `mapstructure:"generation_routing_key"`
|
||||
GenerationQueue string `mapstructure:"generation_queue"`
|
||||
GenerationDLX string `mapstructure:"generation_dlx"`
|
||||
GenerationDLQ string `mapstructure:"generation_dlq"`
|
||||
GenerationDLQRouteKey string `mapstructure:"generation_dlq_routing_key"`
|
||||
GenerationEventExchange string `mapstructure:"generation_event_exchange"`
|
||||
DesktopTaskEventExchange string `mapstructure:"desktop_task_event_exchange"`
|
||||
DesktopDispatchExchange string `mapstructure:"desktop_dispatch_exchange"`
|
||||
DesktopDispatchPublishPrefix string `mapstructure:"desktop_dispatch_publish_prefix"`
|
||||
DesktopDispatchMonitorPrefix string `mapstructure:"desktop_dispatch_monitor_prefix"`
|
||||
DesktopAccountHealthExchange string `mapstructure:"desktop_account_health_exchange"`
|
||||
DesktopAccountHealthRoutingKey string `mapstructure:"desktop_account_health_routing_key"`
|
||||
DesktopAccountHealthQueue string `mapstructure:"desktop_account_health_queue"`
|
||||
DesktopAccountHealthDLX string `mapstructure:"desktop_account_health_dlx"`
|
||||
DesktopAccountHealthDLQ string `mapstructure:"desktop_account_health_dlq"`
|
||||
DesktopAccountHealthDLQRouteKey string `mapstructure:"desktop_account_health_dlq_routing_key"`
|
||||
TemplateAssistExchange string `mapstructure:"template_assist_exchange"`
|
||||
TemplateAssistRoutingKey string `mapstructure:"template_assist_routing_key"`
|
||||
TemplateAssistQueue string `mapstructure:"template_assist_queue"`
|
||||
TemplateAssistDLX string `mapstructure:"template_assist_dlx"`
|
||||
TemplateAssistDLQ string `mapstructure:"template_assist_dlq"`
|
||||
TemplateAssistDLQRouteKey string `mapstructure:"template_assist_dlq_routing_key"`
|
||||
KolAssistExchange string `mapstructure:"kol_assist_exchange"`
|
||||
KolAssistRoutingKey string `mapstructure:"kol_assist_routing_key"`
|
||||
KolAssistQueue string `mapstructure:"kol_assist_queue"`
|
||||
KolAssistDLX string `mapstructure:"kol_assist_dlx"`
|
||||
KolAssistDLQ string `mapstructure:"kol_assist_dlq"`
|
||||
KolAssistDLQRouteKey string `mapstructure:"kol_assist_dlq_routing_key"`
|
||||
ImageAssetExchange string `mapstructure:"image_asset_exchange"`
|
||||
ImageAssetRoutingKey string `mapstructure:"image_asset_routing_key"`
|
||||
ImageAssetQueue string `mapstructure:"image_asset_queue"`
|
||||
ImageAssetDLX string `mapstructure:"image_asset_dlx"`
|
||||
ImageAssetDLQ string `mapstructure:"image_asset_dlq"`
|
||||
ImageAssetDLQRouteKey string `mapstructure:"image_asset_dlq_routing_key"`
|
||||
ConsumerPrefetch int `mapstructure:"consumer_prefetch"`
|
||||
PublishChannelPoolSize int `mapstructure:"publish_channel_pool_size"`
|
||||
}
|
||||
|
||||
type SchedulerConfig struct {
|
||||
@@ -523,6 +529,24 @@ func normalizeRabbitMQConfig(cfg *RabbitMQConfig) {
|
||||
if strings.TrimSpace(cfg.DesktopDispatchMonitorPrefix) == "" {
|
||||
cfg.DesktopDispatchMonitorPrefix = "monitor"
|
||||
}
|
||||
if strings.TrimSpace(cfg.DesktopAccountHealthExchange) == "" {
|
||||
cfg.DesktopAccountHealthExchange = "desktop.account.health"
|
||||
}
|
||||
if strings.TrimSpace(cfg.DesktopAccountHealthRoutingKey) == "" {
|
||||
cfg.DesktopAccountHealthRoutingKey = "desktop.account.health.report"
|
||||
}
|
||||
if strings.TrimSpace(cfg.DesktopAccountHealthQueue) == "" {
|
||||
cfg.DesktopAccountHealthQueue = "desktop.account.health.report"
|
||||
}
|
||||
if strings.TrimSpace(cfg.DesktopAccountHealthDLX) == "" {
|
||||
cfg.DesktopAccountHealthDLX = "desktop.account.health.dlx"
|
||||
}
|
||||
if strings.TrimSpace(cfg.DesktopAccountHealthDLQ) == "" {
|
||||
cfg.DesktopAccountHealthDLQ = "desktop.account.health.report.dlq"
|
||||
}
|
||||
if strings.TrimSpace(cfg.DesktopAccountHealthDLQRouteKey) == "" {
|
||||
cfg.DesktopAccountHealthDLQRouteKey = "desktop.account.health.report.dlq"
|
||||
}
|
||||
if strings.TrimSpace(cfg.TemplateAssistExchange) == "" {
|
||||
cfg.TemplateAssistExchange = "template.assist"
|
||||
}
|
||||
|
||||
@@ -116,6 +116,10 @@ func (c *Client) PublishDesktopDispatch(ctx context.Context, routingKey string,
|
||||
return c.publish(ctx, c.cfg.DesktopDispatchExchange, routingKey, body)
|
||||
}
|
||||
|
||||
func (c *Client) PublishDesktopAccountHealth(ctx context.Context, body []byte) error {
|
||||
return c.publish(ctx, c.cfg.DesktopAccountHealthExchange, c.cfg.DesktopAccountHealthRoutingKey, body)
|
||||
}
|
||||
|
||||
// ConsumeDesktopDispatch declares a transient per-process queue bound to the
|
||||
// desktop dispatch topic exchange using bindingKeys, and begins consuming from
|
||||
// it. Every tenant-api instance gets its own queue so every instance sees every
|
||||
@@ -283,6 +287,10 @@ func (c *Client) ConsumeDesktopTaskEvents(consumerName string) (<-chan amqp.Deli
|
||||
return c.consumeFanout(consumerName, c.cfg.DesktopTaskEventExchange)
|
||||
}
|
||||
|
||||
func (c *Client) ConsumeDesktopAccountHealth(consumerName string) (<-chan amqp.Delivery, *amqp.Channel, error) {
|
||||
return c.consume(consumerName, c.cfg.DesktopAccountHealthQueue)
|
||||
}
|
||||
|
||||
func (c *Client) ConsumeTemplateAssistTask(consumerName string) (<-chan amqp.Delivery, *amqp.Channel, error) {
|
||||
return c.consume(consumerName, c.cfg.TemplateAssistQueue)
|
||||
}
|
||||
@@ -685,6 +693,18 @@ func (c *Client) ensureTopology(conn *amqp.Connection) error {
|
||||
return fmt.Errorf("declare rabbitmq exchange %s: %w", c.cfg.DesktopDispatchExchange, err)
|
||||
}
|
||||
|
||||
if err := c.ensureWorkQueue(
|
||||
ch,
|
||||
c.cfg.DesktopAccountHealthExchange,
|
||||
c.cfg.DesktopAccountHealthRoutingKey,
|
||||
c.cfg.DesktopAccountHealthQueue,
|
||||
c.cfg.DesktopAccountHealthDLX,
|
||||
c.cfg.DesktopAccountHealthDLQ,
|
||||
c.cfg.DesktopAccountHealthDLQRouteKey,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.ensureWorkQueue(
|
||||
ch,
|
||||
c.cfg.TemplateAssistExchange,
|
||||
|
||||
Reference in New Issue
Block a user