mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-12 11:04:25 +00:00
f680dd934d
* 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>
181 lines
6.1 KiB
Go
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,
|
|
"a.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
|
|
}
|
|
}
|