mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 19:07:30 +00:00
perf: project quotas and usages (#6441)
* project quota added * project quota removed * add periods table * make log record generic * accumulate usage * query usage * count action run seconds * fix filter in ReportQuotaUsage * fix existing tests * fix logstore tests * fix typo * fix: add quota unit tests command side * fix: add quota unit tests command side * fix: add quota unit tests command side * move notifications into debouncer and improve limit querying * cleanup * comment * fix: add quota unit tests command side * fix remaining quota usage query * implement InmemLogStorage * cleanup and linting * improve test * fix: add quota unit tests command side * fix: add quota unit tests command side * fix: add quota unit tests command side * fix: add quota unit tests command side * action notifications and fixes for notifications query * revert console prefix * fix: add quota unit tests command side * fix: add quota integration tests * improve accountable requests * improve accountable requests * fix: add quota integration tests * fix: add quota integration tests * fix: add quota integration tests * comment * remove ability to store logs in db and other changes requested from review * changes requested from review * changes requested from review * Update internal/api/http/middleware/access_interceptor.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * tests: fix quotas integration tests * improve incrementUsageStatement * linting * fix: delete e2e tests as intergation tests cover functionality * Update internal/api/http/middleware/access_interceptor.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * backup * fix conflict * create rc * create prerelease * remove issue release labeling * fix tracing --------- Co-authored-by: Livio Spring <livio.a@gmail.com> Co-authored-by: Stefan Benz <stefan@caos.ch> Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
This commit is contained in:
@@ -2,58 +2,180 @@ package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
errs "errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/call"
|
||||
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/repository/quota"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func (q *Queries) GetDueQuotaNotifications(ctx context.Context, config *quota.AddedEvent, periodStart time.Time, usedAbs uint64) ([]*quota.NotificationDueEvent, error) {
|
||||
if len(config.Notifications) == 0 {
|
||||
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, zitadel_errors.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
|
||||
}
|
||||
|
||||
aggregate := config.Aggregate()
|
||||
wm, err := q.getQuotaNotificationsReadModel(ctx, aggregate, periodStart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usedRel := uint16(math.Floor(float64(usedAbs*100) / float64(config.Amount)))
|
||||
|
||||
var dueNotifications []*quota.NotificationDueEvent
|
||||
for _, notification := range config.Notifications {
|
||||
if notification.Percent > usedRel {
|
||||
for _, notification := range notifications.Configs {
|
||||
reachedThreshold := calculateThreshold(usedRel, notification.Percent)
|
||||
if !notification.Repeat && notification.Percent < reachedThreshold {
|
||||
continue
|
||||
}
|
||||
|
||||
threshold := notification.Percent
|
||||
if notification.Repeat {
|
||||
threshold = uint16(math.Max(1, math.Floor(float64(usedRel)/float64(notification.Percent)))) * notification.Percent
|
||||
}
|
||||
|
||||
if wm.latestDueThresholds[notification.ID] < threshold {
|
||||
dueNotifications = append(
|
||||
dueNotifications,
|
||||
quota.NewNotificationDueEvent(
|
||||
ctx,
|
||||
&aggregate,
|
||||
config.Unit,
|
||||
notification.ID,
|
||||
notification.CallURL,
|
||||
periodStart,
|
||||
threshold,
|
||||
usedAbs,
|
||||
),
|
||||
)
|
||||
}
|
||||
dueNotifications = append(
|
||||
dueNotifications,
|
||||
quota.NewNotificationDueEvent(
|
||||
ctx,
|
||||
"a.NewAggregate(qu.ID, instanceID).Aggregate,
|
||||
unit,
|
||||
notification.ID,
|
||||
notification.CallURL,
|
||||
periodStart,
|
||||
reachedThreshold,
|
||||
usedAbs,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return dueNotifications, nil
|
||||
}
|
||||
|
||||
func (q *Queries) getQuotaNotificationsReadModel(ctx context.Context, aggregate eventstore.Aggregate, periodStart time.Time) (*quotaNotificationsReadModel, error) {
|
||||
wm := newQuotaNotificationsReadModel(aggregate.ID, aggregate.InstanceID, aggregate.ResourceOwner, periodStart)
|
||||
return wm, q.eventstore.FilterToQueryReducer(ctx, wm)
|
||||
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 errs.Is(err, sql.ErrNoRows) {
|
||||
return nil, zitadel_errors.ThrowNotFound(err, "QUERY-bbqWb", "Errors.QuotaNotification.NotExisting")
|
||||
}
|
||||
return nil, zitadel_errors.ThrowInternal(err, "QUERY-8copS", "Errors.Internal")
|
||||
}
|
||||
if nextDueThreshold.Valid {
|
||||
cfg.NextDueThreshold = uint16(nextDueThreshold.Int16)
|
||||
}
|
||||
cfgs.Configs = append(cfgs.Configs, cfg)
|
||||
}
|
||||
return cfgs, nil
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user