Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
230 lines
6.4 KiB
Go
230 lines
6.4 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package sql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/herodot"
|
|
"github.com/ory/kratos/courier"
|
|
"github.com/ory/kratos/persistence/sql/update"
|
|
"github.com/ory/pop/v6"
|
|
"github.com/ory/x/dbal"
|
|
"github.com/ory/x/otelx"
|
|
keysetpagination "github.com/ory/x/pagination/keysetpagination_v2"
|
|
"github.com/ory/x/sqlcon"
|
|
"github.com/ory/x/uuidx"
|
|
)
|
|
|
|
var _ courier.Persister = new(Persister)
|
|
|
|
func (p *Persister) AddMessage(ctx context.Context, m *courier.Message) (err error) {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.AddMessage")
|
|
defer otelx.End(span, &err)
|
|
|
|
m.NID = p.NetworkID(ctx)
|
|
m.Status = courier.MessageStatusQueued
|
|
return sqlcon.HandleError(p.GetConnection(ctx).Create(m)) // do not create eager to avoid identity injection.
|
|
}
|
|
|
|
func (p *Persister) ListMessages(ctx context.Context, filter courier.ListCourierMessagesParameters, opts []keysetpagination.Option) (_ []courier.Message, _ *keysetpagination.Paginator, err error) {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ListMessages")
|
|
defer otelx.End(span, &err)
|
|
|
|
q := p.GetConnection(ctx).Where("nid=?", p.NetworkID(ctx))
|
|
|
|
if filter.Status != nil {
|
|
q = q.Where("status=?", *filter.Status)
|
|
}
|
|
|
|
if filter.Recipient != "" {
|
|
q = q.Where("recipient=?", filter.Recipient)
|
|
}
|
|
|
|
opts = append(opts, keysetpagination.WithDefaultToken(courier.Message{}.DefaultPageToken()))
|
|
opts = append(opts, keysetpagination.WithDefaultSize(10))
|
|
paginator, err := keysetpagination.NewPaginator(opts...)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
messages := make([]courier.Message, paginator.Size())
|
|
if err := q.Scope(keysetpagination.Paginate[courier.Message](paginator)).
|
|
All(&messages); err != nil {
|
|
return nil, nil, sqlcon.HandleError(err)
|
|
}
|
|
|
|
messages, nextPage := keysetpagination.Result(messages, paginator)
|
|
|
|
return messages, nextPage, nil
|
|
}
|
|
|
|
func (p *Persister) NextMessages(ctx context.Context, limit uint8) (messages []courier.Message, err error) {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.NextMessages")
|
|
defer otelx.End(span, &err)
|
|
|
|
if err := p.Transaction(ctx, func(ctx context.Context, tx *pop.Connection) error {
|
|
q := tx.Where("nid = ? AND status = ?",
|
|
p.NetworkID(ctx),
|
|
courier.MessageStatusQueued,
|
|
)
|
|
if tx.Dialect.Name() == dbal.DriverCockroachDB {
|
|
// On CockroachDB, exclude messages enqueued within the last second.
|
|
// Concurrent INSERTs land at created_at ~ now; keeping them out of
|
|
// the scanned span avoids read-write contention between this poll
|
|
// and message enqueueing, which surfaces as transaction retries
|
|
// (ReadWithinUncertaintyIntervalError). Skipped messages are picked
|
|
// up by a subsequent poll. The other databases serve this query
|
|
// from an MVCC snapshot that does not conflict with concurrent
|
|
// inserts, so they keep the exact current behavior.
|
|
q = q.Where("created_at < ?", time.Now().UTC().Add(-time.Second))
|
|
}
|
|
|
|
var m []courier.Message
|
|
if err := q.
|
|
Order("created_at ASC").
|
|
Limit(int(limit)).
|
|
All(&m); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(m) == 0 {
|
|
return sql.ErrNoRows
|
|
}
|
|
|
|
for i := range m {
|
|
message := &m[i]
|
|
message.Status = courier.MessageStatusProcessing
|
|
if err := update.Generic(ctx, tx, p.r.Tracer(ctx).Tracer(), message, "status"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
messages = m
|
|
return nil
|
|
}); err != nil {
|
|
if errors.Cause(err) == sql.ErrNoRows {
|
|
return nil, errors.WithStack(courier.ErrQueueEmpty)
|
|
}
|
|
return nil, sqlcon.HandleError(err)
|
|
}
|
|
|
|
if len(messages) == 0 {
|
|
return nil, errors.WithStack(courier.ErrQueueEmpty)
|
|
}
|
|
|
|
return messages, nil
|
|
}
|
|
|
|
func (p *Persister) LatestQueuedMessage(ctx context.Context) (_ *courier.Message, err error) {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.LatestQueuedMessage")
|
|
defer otelx.End(span, &err)
|
|
|
|
var m courier.Message
|
|
if err := p.GetConnection(ctx).
|
|
Where("nid = ? AND status = ?",
|
|
p.NetworkID(ctx),
|
|
courier.MessageStatusQueued,
|
|
).
|
|
Order("created_at DESC").
|
|
First(&m); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, errors.WithStack(courier.ErrQueueEmpty)
|
|
}
|
|
return nil, sqlcon.HandleError(err)
|
|
}
|
|
|
|
return &m, nil
|
|
}
|
|
|
|
func (p *Persister) SetMessageStatus(ctx context.Context, id uuid.UUID, ms courier.MessageStatus) (err error) {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.SetMessageStatus")
|
|
defer otelx.End(span, &err)
|
|
|
|
count, err := p.GetConnection(ctx).RawQuery(
|
|
"UPDATE courier_messages SET status = ? WHERE id = ? AND nid = ?",
|
|
ms,
|
|
id,
|
|
p.NetworkID(ctx),
|
|
).ExecWithCount()
|
|
if err != nil {
|
|
return sqlcon.HandleError(err)
|
|
}
|
|
|
|
if count == 0 {
|
|
return errors.WithStack(sqlcon.ErrNoRows())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Persister) IncrementMessageSendCount(ctx context.Context, id uuid.UUID) (err error) {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.IncrementMessageSendCount")
|
|
defer otelx.End(span, &err)
|
|
|
|
count, err := p.GetConnection(ctx).RawQuery(
|
|
"UPDATE courier_messages SET send_count = send_count + 1 WHERE id = ? AND nid = ?",
|
|
id,
|
|
p.NetworkID(ctx),
|
|
).ExecWithCount()
|
|
if err != nil {
|
|
return sqlcon.HandleError(err)
|
|
}
|
|
|
|
if count == 0 {
|
|
return errors.WithStack(sqlcon.ErrNoRows())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Persister) FetchMessage(ctx context.Context, msgID uuid.UUID) (_ *courier.Message, err error) {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FetchMessage")
|
|
defer otelx.End(span, &err)
|
|
|
|
var message courier.Message
|
|
if err := p.GetConnection(ctx).
|
|
Where("id = ? AND nid = ?", msgID, p.NetworkID(ctx)).
|
|
Eager().
|
|
First(&message); err != nil {
|
|
return nil, sqlcon.HandleError(err)
|
|
}
|
|
|
|
return &message, nil
|
|
}
|
|
|
|
func (p *Persister) RecordDispatch(ctx context.Context, msgID uuid.UUID, status courier.CourierMessageDispatchStatus, err error) error {
|
|
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.RecordDispatch")
|
|
defer otelx.End(span, &err)
|
|
|
|
dispatch := courier.MessageDispatch{
|
|
ID: uuidx.NewV4(),
|
|
MessageID: msgID,
|
|
Status: status,
|
|
NID: p.NetworkID(ctx),
|
|
}
|
|
|
|
if err != nil {
|
|
// We use herodot as a carrier for the error's data
|
|
her := herodot.ToDefaultError(err, "")
|
|
content, mErr := json.Marshal(her)
|
|
if mErr != nil {
|
|
return errors.WithStack(mErr)
|
|
}
|
|
dispatch.Error = content
|
|
}
|
|
|
|
if err := p.GetConnection(ctx).Create(&dispatch); err != nil {
|
|
return sqlcon.HandleError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|