-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Connect reminder service to minder server to dispatch reminders #3630
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// Copyright 2024 Stacklok, Inc | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Package messages contains the messages used by the reminder service | ||
package messages | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/ThreeDotsLabs/watermill/message" | ||
"github.com/go-playground/validator/v10" | ||
"github.com/google/uuid" | ||
) | ||
|
||
// RepoReminderEvent is an event that is published by the reminder service to trigger repo reconciliation | ||
type RepoReminderEvent struct { | ||
// Project is the project that the event is relevant to | ||
Project uuid.UUID `json:"project"` | ||
// RepositoryID is id of the repository to be reconciled | ||
RepositoryID int64 `json:"repository" validate:"gte=0"` | ||
// ProviderID is the provider of the repository | ||
ProviderID uuid.UUID `json:"provider"` | ||
Comment on lines
+29
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My preference would be to order these Project > Provider > Repo, which is the ownership order. |
||
} | ||
|
||
// NewRepoReminderMessage creates a new repo reminder message | ||
func NewRepoReminderMessage(providerId uuid.UUID, repoID int64, projectID uuid.UUID) (*message.Message, error) { | ||
evt := &RepoReminderEvent{ | ||
RepositoryID: repoID, | ||
Project: projectID, | ||
ProviderID: providerId, | ||
} | ||
|
||
evtStr, err := json.Marshal(evt) | ||
if err != nil { | ||
return nil, fmt.Errorf("error marshalling repo reminder event: %w", err) | ||
} | ||
|
||
msg := message.NewMessage(uuid.New().String(), evtStr) | ||
return msg, nil | ||
} | ||
|
||
// RepoReminderEventFromMessage creates a new repo reminder event from a message | ||
func RepoReminderEventFromMessage(msg *message.Message) (*RepoReminderEvent, error) { | ||
var evt RepoReminderEvent | ||
if err := json.Unmarshal(msg.Payload, &evt); err != nil { | ||
return nil, fmt.Errorf("error unmarshalling payload: %w", err) | ||
} | ||
|
||
validate := validator.New() | ||
if err := validate.Struct(&evt); err != nil { | ||
return nil, err | ||
} | ||
Comment on lines
+61
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the benefit of calling |
||
|
||
return &evt, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,9 @@ import ( | |
|
||
reminderconfig "github.com/stacklok/minder/internal/config/reminder" | ||
"github.com/stacklok/minder/internal/db" | ||
"github.com/stacklok/minder/internal/events" | ||
"github.com/stacklok/minder/internal/events/common" | ||
remindermessages "github.com/stacklok/minder/internal/reminder/messages" | ||
) | ||
|
||
// Interface is an interface over the reminder service | ||
|
@@ -51,7 +54,7 @@ type reminder struct { | |
ticker *time.Ticker | ||
|
||
eventPublisher message.Publisher | ||
eventDBCloser driverCloser | ||
eventDBCloser common.DriverCloser | ||
} | ||
|
||
// NewReminder creates a new reminder instance | ||
|
@@ -144,19 +147,64 @@ func (r *reminder) sendReminders(ctx context.Context) []error { | |
|
||
logger.Info().Msgf("created repository batch of size: %d", len(repos)) | ||
|
||
// Update the reminder_last_sent for each repository to export as metrics | ||
messages := make([]*message.Message, 0, len(repos)) | ||
errorSlice := make([]error, 0) | ||
|
||
tx, err := r.store.BeginTransaction() | ||
if err != nil { | ||
return []error{err} | ||
} | ||
|
||
defer tx.Rollback() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need all of the following database reads and writes to go into a single transaction (but not the |
||
|
||
qtx := r.store.GetQuerierWithTransaction(tx) | ||
|
||
// TODO: Collect Metrics | ||
// Potential metrics: | ||
// - Gauge: Number of reminders in the current batch | ||
// - UpDownCounter: Average reminders sent per batch | ||
// - Histogram: reminder_last_sent time distribution | ||
Comment on lines
+162
to
+166
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we want to export the metrics? And some thoughts on adding these metrics? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I wonder what metrics would be useful as a minder operator. About the histogram, I think a histogram that shows me how out of date (how much before a cutoff) a repo was when it was selected would be useful. I think the metrics would be interesting to fine-tune the algorithm, did you see some other value in the metrics? Since reminder is a separate service, then I guess implementation-wise it should just have its own metrics server that could be scraped separately. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My $0.02 would be:
|
||
for _, repo := range repos { | ||
logger.Debug().Str("repo", repo.ID.String()). | ||
Time("previously", repo.ReminderLastSent.Time).Msg("updating reminder_last_sent") | ||
err := r.store.UpdateReminderLastSentById(ctx, repo.ID) | ||
repoReconcilerMessage, err := remindermessages.NewRepoReminderMessage( | ||
repo.ProviderID, repo.RepoID, repo.ProjectID, | ||
) | ||
if err != nil { | ||
errorSlice = append(errorSlice, err) | ||
continue | ||
} | ||
|
||
logger.Debug(). | ||
Str("repo", repo.ID.String()). | ||
Time("previously", repo.ReminderLastSent.Time). | ||
Msg("updating reminder_last_sent") | ||
|
||
err = qtx.UpdateReminderLastSentById(ctx, repo.ID) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional that you update when the reminder is sent before you send the message? ... except that with transactions, this actually sort-of happens after the message was sent, too -- you may be taking out a lock on the repo row for the duration of the iteration. Transactions can make before/after behavior hard to reason about in some cases, so I'd be cautious about introducing them when they aren't needed / aren't producing specific guarantees. |
||
if err != nil { | ||
logger.Error().Err(err).Str("repo", repo.ID.String()).Msg("unable to update reminder_last_sent") | ||
return []error{err} | ||
errorSlice = append(errorSlice, err) | ||
Comment on lines
183
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general:
Logging the error and also propagating it often means that you'll end up with errors being double- or even triple-logged, as different parts of the stack log the error but then pass it up to be logged in the next part of the stack. |
||
continue | ||
} | ||
|
||
messages = append(messages, repoReconcilerMessage) | ||
} | ||
|
||
if len(messages) != 0 { | ||
err = r.eventPublisher.Publish(events.TopicQueueRepoReminder, messages...) | ||
if err != nil { | ||
errorSlice = append(errorSlice, fmt.Errorf("error publishing messages: %w", err)) | ||
} else { | ||
Comment on lines
+193
to
+195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than an More generally, I'm not convinced that there's a large benefit to batching the Publish messages, rather than sending each in the previous part of the loop. |
||
logger.Info().Msgf("sent %d reminders", len(messages)) | ||
|
||
// Commit the transaction i.e update reminder_last_sent | ||
// only if the messages were sent successfully | ||
if err = tx.Commit(); err != nil { | ||
logger.Error().Err(err).Msg("unable to commit transaction") | ||
errorSlice = append(errorSlice, err) | ||
} | ||
Comment on lines
+198
to
+203
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is fine. Did you consider to split sending the messages into a separate function or return earlier if Something like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like the (I'm not arguing against keeping this data, but it seems like the actual mechanism that prevents re-evaluation is when the reminder is received and a rule evaluation is complete, which could be minutes after the reminder is sent on a particularly bad day.) |
||
} | ||
// TODO: Send the actual reminders | ||
} | ||
|
||
return nil | ||
return errorSlice | ||
} | ||
|
||
func (r *reminder) getRepositoryBatch(ctx context.Context) ([]db.Repository, error) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
// | ||
// Copyright 2024 Stacklok, Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Package reminderprocessor processes the incoming reminders | ||
package reminderprocessor | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/ThreeDotsLabs/watermill/message" | ||
"github.com/rs/zerolog/log" | ||
|
||
"github.com/stacklok/minder/internal/events" | ||
reconcilermessages "github.com/stacklok/minder/internal/reconcilers/messages" | ||
remindermessages "github.com/stacklok/minder/internal/reminder/messages" | ||
) | ||
|
||
// ReminderProcessor processes the incoming reminders | ||
type ReminderProcessor struct { | ||
evt events.Interface | ||
} | ||
|
||
// NewReminderProcessor creates a new ReminderProcessor | ||
func NewReminderProcessor(evt events.Interface) *ReminderProcessor { | ||
return &ReminderProcessor{evt: evt} | ||
} | ||
|
||
// Register implements the Consumer interface. | ||
func (rp *ReminderProcessor) Register(r events.Registrar) { | ||
r.Register(events.TopicQueueRepoReminder, rp.reminderMessageHandler) | ||
} | ||
|
||
func (rp *ReminderProcessor) reminderMessageHandler(msg *message.Message) error { | ||
evt, err := remindermessages.RepoReminderEventFromMessage(msg) | ||
if err != nil { | ||
return fmt.Errorf("error unmarshalling reminder event: %w", err) | ||
} | ||
|
||
log.Info().Msgf("Received reminder event: %v", evt) | ||
|
||
repoReconcileMsg, err := reconcilermessages.NewRepoReconcilerMessage(evt.ProviderID, evt.RepositoryID, evt.Project) | ||
if err != nil { | ||
return fmt.Errorf("error creating repo reconcile event: %w", err) | ||
} | ||
|
||
// This is a non-fatal error, so we'll just log it and continue with the next ones | ||
if err := rp.evt.Publish(events.TopicQueueReconcileRepoInit, repoReconcileMsg); err != nil { | ||
log.Printf("error publishing reconciler event: %v", err) | ||
} | ||
return nil | ||
} | ||
Comment on lines
+45
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels like this is simply forwarding messages from one queue to another; why not simply sending the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, this should use the
id
field from therepositories
table, not therepo_id
from GitHub. That will help insulate this from GitHub-specific fields that are different in (for example) GitLab or BitBucket in the future.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally,
id
is the primary key. It looks like our current indexes are set up offrepo_id
; sorry for not noticing this previously...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking further, it looks like we use the GitHub-matching
repo_id
in NewRepoReconcilerMessage already, so that argues towards using it here as well.