mirror of
https://github.com/tiennm99/openai-status-bot.git
synced 2026-10-14 22:17:28 +00:00
refactor(poller): batch delivery checks and dedupe component helpers
Replace the per-subscriber SISMEMBER delivery check with a single SMEMBERS per event plus in-memory lookups, cutting N round-trips to one. Remove the component-label helpers duplicated between poller and bot by exporting the canonical versions from poller; bot already depends on it. Collapse the repeated component checkpoint closures into a shared helper. Drop the redundant escapeAttr alias and the unused variadic on bot.New.
This commit is contained in:
1 parent
0404e0dd2d
commit
5f26bf4b15
11 files changed
+70
-92
No files matched your search
+2
-6
@@ -40,17 +40,13 @@ type Bot struct {
|
||||
username string
|
||||
}
|
||||
|
||||
func New(telegramClient TelegramClient, statusClient StatusClient, store Store, logger *slog.Logger, username ...string) *Bot {
|
||||
botUsername := ""
|
||||
if len(username) > 0 {
|
||||
botUsername = username[0]
|
||||
}
|
||||
func New(telegramClient TelegramClient, statusClient StatusClient, store Store, logger *slog.Logger, username string) *Bot {
|
||||
return &Bot{
|
||||
telegramClient: telegramClient,
|
||||
statusClient: statusClient,
|
||||
store: store,
|
||||
logger: logger,
|
||||
username: botUsername,
|
||||
username: username,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tiennm99/openai-status-bot/internal/poller"
|
||||
"github.com/tiennm99/openai-status-bot/internal/redisstore"
|
||||
"github.com/tiennm99/openai-status-bot/internal/telegram"
|
||||
)
|
||||
@@ -128,7 +129,7 @@ func (b *Bot) updateComponentFilter(ctx context.Context, message telegram.Messag
|
||||
b.reply(ctx, message, "Not subscribed yet. Use /start first.")
|
||||
return
|
||||
}
|
||||
b.reply(ctx, message, fmt.Sprintf("Subscribed to component: <code>%s</code>\nActive filters: <code>%s</code>", escape(componentLabel(component, duplicateComponentNames(summary.Components)[component.Name])), escape(componentFilterLabels(summary.Components, components))))
|
||||
b.reply(ctx, message, fmt.Sprintf("Subscribed to component: <code>%s</code>\nActive filters: <code>%s</code>", escape(poller.ComponentLabel(component, poller.DuplicateComponentNames(summary.Components)[component.Name])), escape(componentFilterLabels(summary.Components, components))))
|
||||
}
|
||||
|
||||
func withSubscriptionType(types []string, subscriptionType string) []string {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
openai "github.com/tiennm99/openai-status-bot/internal/openai"
|
||||
"github.com/tiennm99/openai-status-bot/internal/poller"
|
||||
)
|
||||
|
||||
type componentResolution struct {
|
||||
@@ -64,10 +65,10 @@ func matchingComponents(components []openai.Component, query string, matches fun
|
||||
}
|
||||
|
||||
func formatAmbiguousComponents(query string, matches []openai.Component) string {
|
||||
duplicates := duplicateComponentNames(matches)
|
||||
duplicates := poller.DuplicateComponentNames(matches)
|
||||
lines := []string{fmt.Sprintf("Component <code>%s</code> is ambiguous. Use one of these IDs:", escape(query)), ""}
|
||||
for _, component := range matches {
|
||||
lines = append(lines, fmt.Sprintf("- %s: <code>%s</code>", escape(componentLabel(component, duplicates[component.Name])), escape(component.ID)))
|
||||
lines = append(lines, fmt.Sprintf("- %s: <code>%s</code>", escape(poller.ComponentLabel(component, duplicates[component.Name])), escape(component.ID)))
|
||||
}
|
||||
return truncateMessage(strings.Join(lines, "\n"))
|
||||
}
|
||||
@@ -76,13 +77,13 @@ func componentFilterLabels(components []openai.Component, ids []string) string {
|
||||
if len(ids) == 0 {
|
||||
return "all"
|
||||
}
|
||||
duplicates := duplicateComponentNames(components)
|
||||
duplicates := poller.DuplicateComponentNames(components)
|
||||
labels := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
label := id
|
||||
for _, component := range components {
|
||||
if component.ID == id {
|
||||
label = componentLabel(component, duplicates[component.Name])
|
||||
label = poller.ComponentLabel(component, duplicates[component.Name])
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -90,32 +91,3 @@ func componentFilterLabels(components []openai.Component, ids []string) string {
|
||||
}
|
||||
return strings.Join(labels, ", ")
|
||||
}
|
||||
|
||||
func duplicateComponentNames(components []openai.Component) map[string]bool {
|
||||
counts := map[string]int{}
|
||||
for _, component := range components {
|
||||
if component.Group {
|
||||
continue
|
||||
}
|
||||
counts[component.Name]++
|
||||
}
|
||||
duplicates := map[string]bool{}
|
||||
for name, count := range counts {
|
||||
duplicates[name] = count > 1
|
||||
}
|
||||
return duplicates
|
||||
}
|
||||
|
||||
func componentLabel(component openai.Component, duplicate bool) string {
|
||||
if !duplicate || component.ID == "" {
|
||||
return component.Name
|
||||
}
|
||||
return fmt.Sprintf("%s (ID: %s)", component.Name, shortID(component.ID))
|
||||
}
|
||||
|
||||
func shortID(value string) string {
|
||||
if len(value) <= 8 {
|
||||
return value
|
||||
}
|
||||
return value[:8]
|
||||
}
|
||||
+8
-12
@@ -33,13 +33,13 @@ func formatStatus(summary openai.Summary) string {
|
||||
fmt.Sprintf("Overall: <code>%s</code>", escape(summary.Status.Description)),
|
||||
}
|
||||
|
||||
duplicates := duplicateComponentNames(summary.Components)
|
||||
duplicates := poller.DuplicateComponentNames(summary.Components)
|
||||
degraded := make([]string, 0)
|
||||
for _, component := range summary.Components {
|
||||
if component.Group || component.Status == "operational" {
|
||||
continue
|
||||
}
|
||||
degraded = append(degraded, fmt.Sprintf("- %s: <code>%s</code>", escape(componentLabel(component, duplicates[component.Name])), escape(poller.StatusLabel(component.Status))))
|
||||
degraded = append(degraded, fmt.Sprintf("- %s: <code>%s</code>", escape(poller.ComponentLabel(component, duplicates[component.Name])), escape(poller.StatusLabel(component.Status))))
|
||||
}
|
||||
|
||||
if len(degraded) == 0 {
|
||||
@@ -56,7 +56,7 @@ func formatStatus(summary openai.Summary) string {
|
||||
func formatComponentStatus(component openai.Component, duplicate bool) string {
|
||||
return fmt.Sprintf(
|
||||
"<b>%s</b>\n\nStatus: <code>%s</code>\nLast change: %s UTC\n\n<a href=\"%s\">View full status page</a>",
|
||||
escape(componentLabel(component, duplicate)),
|
||||
escape(poller.ComponentLabel(component, duplicate)),
|
||||
escape(poller.StatusLabel(component.Status)),
|
||||
escape(formatTime(component.UpdatedAt)),
|
||||
statusURL,
|
||||
@@ -65,12 +65,12 @@ func formatComponentStatus(component openai.Component, duplicate bool) string {
|
||||
|
||||
func formatComponents(summary openai.Summary) string {
|
||||
lines := []string{"<b>OpenAI Components</b>", ""}
|
||||
duplicates := duplicateComponentNames(summary.Components)
|
||||
duplicates := poller.DuplicateComponentNames(summary.Components)
|
||||
for _, component := range summary.Components {
|
||||
if component.Group {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("- %s: <code>%s</code>", escape(componentLabel(component, duplicates[component.Name])), escape(poller.StatusLabel(component.Status))))
|
||||
lines = append(lines, fmt.Sprintf("- %s: <code>%s</code>", escape(poller.ComponentLabel(component, duplicates[component.Name])), escape(poller.StatusLabel(component.Status))))
|
||||
}
|
||||
return truncateMessage(strings.Join(lines, "\n"))
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func formatHistory(incidents []openai.Incident, count int) string {
|
||||
escape(poller.StatusLabel(incident.Status)),
|
||||
)
|
||||
if link != "" {
|
||||
entry += fmt.Sprintf("\n <a href=\"%s\">Details</a>", escapeAttr(link))
|
||||
entry += fmt.Sprintf("\n <a href=\"%s\">Details</a>", escape(link))
|
||||
}
|
||||
lines = append(lines, entry)
|
||||
}
|
||||
@@ -109,14 +109,14 @@ func formatHistory(incidents []openai.Incident, count int) string {
|
||||
|
||||
func formatUptime(summary openai.Summary) string {
|
||||
lines := []string{"<b>OpenAI Component Health</b>", ""}
|
||||
duplicates := duplicateComponentNames(summary.Components)
|
||||
duplicates := poller.DuplicateComponentNames(summary.Components)
|
||||
for _, component := range summary.Components {
|
||||
if component.Group {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf(
|
||||
"%s\n Status: <code>%s</code>\n Last change: %s UTC",
|
||||
escape(componentLabel(component, duplicates[component.Name])),
|
||||
escape(poller.ComponentLabel(component, duplicates[component.Name])),
|
||||
escape(poller.StatusLabel(component.Status)),
|
||||
escape(formatTime(component.UpdatedAt)),
|
||||
))
|
||||
@@ -179,7 +179,3 @@ func emptyDefault(value, fallback string) string {
|
||||
func escape(value string) string {
|
||||
return html.EscapeString(value)
|
||||
}
|
||||
|
||||
func escapeAttr(value string) string {
|
||||
return html.EscapeString(value)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tiennm99/openai-status-bot/internal/poller"
|
||||
"github.com/tiennm99/openai-status-bot/internal/redisstore"
|
||||
"github.com/tiennm99/openai-status-bot/internal/telegram"
|
||||
)
|
||||
@@ -27,7 +28,7 @@ func (b *Bot) replyStatus(ctx context.Context, message telegram.Message, fields
|
||||
b.reply(ctx, message, formatAmbiguousComponents(query, resolution.Matches))
|
||||
return
|
||||
}
|
||||
duplicates := duplicateComponentNames(summary.Components)
|
||||
duplicates := poller.DuplicateComponentNames(summary.Components)
|
||||
b.reply(ctx, message, formatComponentStatus(resolution.Component, duplicates[resolution.Component.Name]))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,6 +46,14 @@ func (e *deliveryError) addAll(other *deliveryError) {
|
||||
|
||||
func (r *Runner) notifySubscribers(ctx context.Context, event notificationEvent, subscribers []redisstore.Subscriber, removed, failed map[string]bool) (*deliveryError, error) {
|
||||
deliveryFailures := &deliveryError{}
|
||||
var delivered map[string]bool
|
||||
if event.deliveryKey != "" {
|
||||
var err error
|
||||
delivered, err = r.store.DeliveredSubscribers(ctx, event.deliveryKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, subscriber := range subscribers {
|
||||
subscriberKey := subscriber.Key()
|
||||
if removed[subscriberKey] || failed[subscriberKey] {
|
||||
@@ -54,14 +62,8 @@ func (r *Runner) notifySubscribers(ctx context.Context, event notificationEvent,
|
||||
if !subscriber.Accepts(event.eventType, event.componentID, event.componentName) {
|
||||
continue
|
||||
}
|
||||
if event.deliveryKey != "" {
|
||||
delivered, err := r.store.HasDelivered(ctx, event.deliveryKey, subscriberKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delivered {
|
||||
continue
|
||||
}
|
||||
if delivered[subscriberKey] {
|
||||
continue
|
||||
}
|
||||
if err := r.notifier.SendMessage(ctx, subscriber, event.text); err != nil {
|
||||
if telegram.IsTerminalSendError(err) {
|
||||
|
||||
@@ -45,7 +45,7 @@ func (r *Runner) collectComponentEvents(ctx context.Context, summary openai.Summ
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
duplicates := duplicateComponentNames(componentsForDuplicateLabels(summary.Components, pending))
|
||||
duplicates := DuplicateComponentNames(componentsForDuplicateLabels(summary.Components, pending))
|
||||
|
||||
events := make([]notificationEvent, 0)
|
||||
before := make([]checkpoint, 0)
|
||||
@@ -62,15 +62,7 @@ func (r *Runner) collectComponentEvents(ctx context.Context, summary openai.Summ
|
||||
sortTime: pendingEvent.UpdatedAt,
|
||||
text: FormatComponentChange(component, pendingEvent.PreviousStatus, duplicates[component.Name]),
|
||||
})
|
||||
after = append(after, func(ctx context.Context) error {
|
||||
return r.store.SaveComponentStatus(ctx, pendingEvent.ComponentID, pendingEvent.Status)
|
||||
})
|
||||
after = append(after, func(ctx context.Context) error {
|
||||
return r.store.RemovePendingComponentEvent(ctx, pendingEvent.ComponentID)
|
||||
})
|
||||
after = append(after, func(ctx context.Context) error {
|
||||
return r.store.ClearDelivery(ctx, pendingEvent.DeliveryKey)
|
||||
})
|
||||
after = append(after, r.resolveComponentCheckpoints(pendingEvent.ComponentID, pendingEvent.Status, pendingEvent.DeliveryKey)...)
|
||||
}
|
||||
|
||||
for _, component := range summary.Components {
|
||||
@@ -120,19 +112,22 @@ func (r *Runner) collectComponentEvents(ctx context.Context, summary openai.Summ
|
||||
sortTime: component.UpdatedAt,
|
||||
text: FormatComponentChange(component, previousStatus, duplicates[component.Name]),
|
||||
})
|
||||
after = append(after, func(ctx context.Context) error {
|
||||
return r.store.SaveComponentStatus(ctx, component.ID, component.Status)
|
||||
})
|
||||
after = append(after, func(ctx context.Context) error {
|
||||
return r.store.RemovePendingComponentEvent(ctx, component.ID)
|
||||
})
|
||||
after = append(after, func(ctx context.Context) error {
|
||||
return r.store.ClearDelivery(ctx, deliveryKey)
|
||||
})
|
||||
after = append(after, r.resolveComponentCheckpoints(component.ID, component.Status, deliveryKey)...)
|
||||
}
|
||||
return events, before, after, nil
|
||||
}
|
||||
|
||||
// resolveComponentCheckpoints builds the post-delivery writes shared by pending
|
||||
// and freshly detected component events: persist the new status, drop the
|
||||
// pending marker, and clear the delivery-state set.
|
||||
func (r *Runner) resolveComponentCheckpoints(componentID, status, deliveryKey string) []checkpoint {
|
||||
return []checkpoint{
|
||||
func(ctx context.Context) error { return r.store.SaveComponentStatus(ctx, componentID, status) },
|
||||
func(ctx context.Context) error { return r.store.RemovePendingComponentEvent(ctx, componentID) },
|
||||
func(ctx context.Context) error { return r.store.ClearDelivery(ctx, deliveryKey) },
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) collectIncidentEvents(ctx context.Context, response openai.IncidentsResponse, initialized bool) ([]notificationEvent, []checkpoint, error) {
|
||||
events := make([]notificationEvent, 0)
|
||||
checkpoints := make([]checkpoint, 0)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func FormatComponentChange(component openai.Component, previousStatus string, duplicate bool) string {
|
||||
return fmt.Sprintf(
|
||||
"<b>OpenAI component update</b>\n\nComponent: <b>%s</b>\nStatus: <code>%s</code> -> <code>%s</code>\n\n<a href=\"https://status.openai.com/\">View full status page</a>",
|
||||
escape(componentLabel(component, duplicate)),
|
||||
escape(ComponentLabel(component, duplicate)),
|
||||
escape(StatusLabel(previousStatus)),
|
||||
escape(StatusLabel(component.Status)),
|
||||
)
|
||||
@@ -75,7 +75,9 @@ func StatusLabel(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func duplicateComponentNames(components []openai.Component) map[string]bool {
|
||||
// DuplicateComponentNames reports which non-group component names appear more
|
||||
// than once, so labels can disambiguate them with a short ID suffix.
|
||||
func DuplicateComponentNames(components []openai.Component) map[string]bool {
|
||||
counts := map[string]int{}
|
||||
for _, component := range components {
|
||||
if component.Group {
|
||||
@@ -90,14 +92,16 @@ func duplicateComponentNames(components []openai.Component) map[string]bool {
|
||||
return duplicates
|
||||
}
|
||||
|
||||
func componentLabel(component openai.Component, duplicate bool) string {
|
||||
// ComponentLabel renders a component's display name, appending a short ID when
|
||||
// the name collides with another component.
|
||||
func ComponentLabel(component openai.Component, duplicate bool) string {
|
||||
if !duplicate || component.ID == "" {
|
||||
return component.Name
|
||||
}
|
||||
return fmt.Sprintf("%s (ID: %s)", component.Name, shortID(component.ID))
|
||||
return fmt.Sprintf("%s (ID: %s)", component.Name, ShortID(component.ID))
|
||||
}
|
||||
|
||||
func shortID(value string) string {
|
||||
func ShortID(value string) string {
|
||||
if len(value) <= 8 {
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type StatusClient interface {
|
||||
type Store interface {
|
||||
ComponentStatuses(ctx context.Context) (map[string]string, error)
|
||||
ClearDelivery(ctx context.Context, eventKey string) error
|
||||
HasDelivered(ctx context.Context, eventKey, subscriberKey string) (bool, error)
|
||||
DeliveredSubscribers(ctx context.Context, eventKey string) (map[string]bool, error)
|
||||
HasIncidentUpdateVersion(ctx context.Context, updateID, version string) (bool, error)
|
||||
IsInitialized(ctx context.Context) (bool, error)
|
||||
ListSubscribers(ctx context.Context) ([]redisstore.Subscriber, error)
|
||||
|
||||
@@ -61,11 +61,11 @@ func (f *fakePollerStore) ClearDelivery(_ context.Context, eventKey string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakePollerStore) HasDelivered(_ context.Context, eventKey, subscriberKey string) (bool, error) {
|
||||
func (f *fakePollerStore) DeliveredSubscribers(_ context.Context, eventKey string) (map[string]bool, error) {
|
||||
if f.hasDeliveredErr != nil {
|
||||
return false, f.hasDeliveredErr
|
||||
return nil, f.hasDeliveredErr
|
||||
}
|
||||
return f.delivered[eventKey][subscriberKey], nil
|
||||
return f.delivered[eventKey], nil
|
||||
}
|
||||
|
||||
func (f *fakePollerStore) HasIncidentUpdateVersion(_ context.Context, updateID, version string) (bool, error) {
|
||||
@@ -407,7 +407,7 @@ func TestPendingComponentDuplicateLabelsIncludeCurrentRename(t *testing.T) {
|
||||
{ID: "c2", Name: "API", Status: "operational"},
|
||||
}
|
||||
|
||||
duplicates := duplicateComponentNames(componentsForDuplicateLabels(components, pending))
|
||||
duplicates := DuplicateComponentNames(componentsForDuplicateLabels(components, pending))
|
||||
if !duplicates["API"] {
|
||||
t.Fatalf("duplicates = %v, want current API rename counted as duplicate", duplicates)
|
||||
}
|
||||
|
||||
@@ -115,8 +115,19 @@ func (s *Store) MarkIncidentUpdateVersion(ctx context.Context, updateID, version
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) HasDelivered(ctx context.Context, eventKey, subscriberKey string) (bool, error) {
|
||||
return s.client.SIsMember(ctx, deliveryStateKey(eventKey), subscriberKey).Result()
|
||||
// DeliveredSubscribers returns the set of subscriber keys already notified for
|
||||
// eventKey. Callers fan a single event out to every subscriber, so one SMEMBERS
|
||||
// plus in-memory lookups replaces one SISMEMBER per subscriber.
|
||||
func (s *Store) DeliveredSubscribers(ctx context.Context, eventKey string) (map[string]bool, error) {
|
||||
members, err := s.client.SMembers(ctx, deliveryStateKey(eventKey)).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delivered := make(map[string]bool, len(members))
|
||||
for _, member := range members {
|
||||
delivered[member] = true
|
||||
}
|
||||
return delivered, nil
|
||||
}
|
||||
|
||||
func (s *Store) MarkDelivered(ctx context.Context, eventKey, subscriberKey string) error {
|
||||
|
||||
Reference in new issue
Block a user