zitadel/internal/query/quota_notifications.go
Tim Möhlmann f680dd934d
refactor: rename package errors to zerrors (#7039)
* chore: rename package errors to zerrors

* rename package errors to gerrors

* fix error related linting issues

* fix zitadel error assertion

* fix gosimple linting issues

* fix deprecated linting issues

* resolve gci linting issues

* fix import structure

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
2023-12-08 15:30:55 +01:00

181 lines
6.1 KiB
Go

package query
import (
"context"
"database/sql"
"errors"
"math"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/zitadel/internal/api/call"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/quota"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
quotaNotificationsTable = table{
name: projection.QuotaNotificationsTable,
instanceIDCol: projection.QuotaNotificationColumnInstanceID,
}
QuotaNotificationColumnInstanceID = Column{
name: projection.QuotaNotificationColumnInstanceID,
table: quotaNotificationsTable,
}
QuotaNotificationColumnUnit = Column{
name: projection.QuotaNotificationColumnUnit,
table: quotaNotificationsTable,
}
QuotaNotificationColumnID = Column{
name: projection.QuotaNotificationColumnID,
table: quotaNotificationsTable,
}
QuotaNotificationColumnCallURL = Column{
name: projection.QuotaNotificationColumnCallURL,
table: quotaNotificationsTable,
}
QuotaNotificationColumnPercent = Column{
name: projection.QuotaNotificationColumnPercent,
table: quotaNotificationsTable,
}
QuotaNotificationColumnRepeat = Column{
name: projection.QuotaNotificationColumnRepeat,
table: quotaNotificationsTable,
}
QuotaNotificationColumnLatestDuePeriodStart = Column{
name: projection.QuotaNotificationColumnLatestDuePeriodStart,
table: quotaNotificationsTable,
}
QuotaNotificationColumnNextDueThreshold = Column{
name: projection.QuotaNotificationColumnNextDueThreshold,
table: quotaNotificationsTable,
}
)
func (q *Queries) GetDueQuotaNotifications(ctx context.Context, instanceID string, unit quota.Unit, qu *Quota, periodStart time.Time, usedAbs uint64) (dueNotifications []*quota.NotificationDueEvent, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
usedRel := uint16(math.Floor(float64(usedAbs*100) / float64(qu.Amount)))
query, scan := prepareQuotaNotificationsQuery(ctx, q.client)
stmt, args, err := query.Where(
sq.And{
sq.Eq{
QuotaNotificationColumnInstanceID.identifier(): instanceID,
QuotaNotificationColumnUnit.identifier(): unit,
},
sq.Or{
// If the relative usage is greater than the next due threshold in the current period, it's clear we can notify
sq.And{
sq.Eq{QuotaNotificationColumnLatestDuePeriodStart.identifier(): periodStart},
sq.LtOrEq{QuotaNotificationColumnNextDueThreshold.identifier(): usedRel},
},
// In case we haven't seen a due notification for this quota period, we compare against the configured percent
sq.And{
sq.Or{
sq.Expr(QuotaNotificationColumnLatestDuePeriodStart.identifier() + " IS NULL"),
sq.NotEq{QuotaNotificationColumnLatestDuePeriodStart.identifier(): periodStart},
},
sq.LtOrEq{QuotaNotificationColumnPercent.identifier(): usedRel},
},
},
},
).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-XmYn9", "Errors.Query.SQLStatement")
}
var notifications *QuotaNotifications
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
notifications, err = scan(rows)
return err
}, stmt, args...)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
for _, notification := range notifications.Configs {
reachedThreshold := calculateThreshold(usedRel, notification.Percent)
if !notification.Repeat && notification.Percent < reachedThreshold {
continue
}
dueNotifications = append(
dueNotifications,
quota.NewNotificationDueEvent(
ctx,
&quota.NewAggregate(qu.ID, instanceID).Aggregate,
unit,
notification.ID,
notification.CallURL,
periodStart,
reachedThreshold,
usedAbs,
),
)
}
return dueNotifications, nil
}
type QuotaNotification struct {
ID string
CallURL string
Percent uint16
Repeat bool
NextDueThreshold uint16
}
type QuotaNotifications struct {
SearchResponse
Configs []*QuotaNotification
}
// calculateThreshold calculates the nearest reached threshold.
// It makes sure that the percent configured on the notification is calculated within the "current" 100%,
// e.g. when configuring 80%, the thresholds are 80, 180, 280, ...
// so 170% use is always 70% of the current bucket, with the above config, the reached threshold would be 80.
func calculateThreshold(usedRel, notificationPercent uint16) uint16 {
// check how many times we reached 100%
times := math.Floor(float64(usedRel) / 100)
// check how many times we reached the percent configured with the "current" 100%
percent := math.Floor(float64(usedRel%100) / float64(notificationPercent))
// If neither is reached, directly return 0.
// This way we don't end up in some wrong uint16 range in the calculation below.
if times == 0 && percent == 0 {
return 0
}
return uint16(times+percent-1)*100 + notificationPercent
}
func prepareQuotaNotificationsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*QuotaNotifications, error)) {
return sq.Select(
QuotaNotificationColumnID.identifier(),
QuotaNotificationColumnCallURL.identifier(),
QuotaNotificationColumnPercent.identifier(),
QuotaNotificationColumnRepeat.identifier(),
QuotaNotificationColumnNextDueThreshold.identifier(),
).
From(quotaNotificationsTable.identifier() + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*QuotaNotifications, error) {
cfgs := &QuotaNotifications{Configs: []*QuotaNotification{}}
for rows.Next() {
cfg := new(QuotaNotification)
var nextDueThreshold sql.NullInt16
err := rows.Scan(&cfg.ID, &cfg.CallURL, &cfg.Percent, &cfg.Repeat, &nextDueThreshold)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowNotFound(err, "QUERY-bbqWb", "Errors.QuotaNotification.NotExisting")
}
return nil, zerrors.ThrowInternal(err, "QUERY-8copS", "Errors.Internal")
}
if nextDueThreshold.Valid {
cfg.NextDueThreshold = uint16(nextDueThreshold.Int16)
}
cfgs.Configs = append(cfgs.Configs, cfg)
}
return cfgs, nil
}
}