From 9382d33ee521e6a72d763f906b24dd53893103df Mon Sep 17 00:00:00 2001 From: Huiwen Date: Mon, 29 Apr 2024 19:25:39 +0800 Subject: [PATCH] Update usage scheduler according to FF (#19655) * Update usage scheduler according to FF * Update dashboard * Try validate duration first * Empty fallback to installer config * undefined fallback to default * fixup * Update go mod * Fix test * 1m * Add test cases --- .../dashboard/src/data/featureflag-query.ts | 1 + .../dashboard/src/data/usage/usage-query.ts | 4 +- components/dashboard/src/usage/UsageView.tsx | 27 +- components/usage/go.mod | 2 + components/usage/go.sum | 8 + components/usage/pkg/server/server.go | 173 +++++++++--- components/usage/pkg/server/server_test.go | 253 ++++++++++++++++++ .../pkg/components/usage/configmap.go | 1 + .../pkg/components/usage/configmap_test.go | 3 +- 9 files changed, 429 insertions(+), 43 deletions(-) create mode 100644 components/usage/pkg/server/server_test.go diff --git a/components/dashboard/src/data/featureflag-query.ts b/components/dashboard/src/data/featureflag-query.ts index b741214f674479..c31b18c3a1573c 100644 --- a/components/dashboard/src/data/featureflag-query.ts +++ b/components/dashboard/src/data/featureflag-query.ts @@ -38,6 +38,7 @@ const featureFlags = { // Logging tracing for added for investigate hanging issue dashboard_logging_tracing: false, showBrowserExtensionPromotion: false, + usage_update_scheduler_duration: "15m", }; type FeatureFlags = typeof featureFlags; diff --git a/components/dashboard/src/data/usage/usage-query.ts b/components/dashboard/src/data/usage/usage-query.ts index 02a4ee4fcee5f8..740328ac07ca1a 100644 --- a/components/dashboard/src/data/usage/usage-query.ts +++ b/components/dashboard/src/data/usage/usage-query.ts @@ -15,8 +15,8 @@ export function useListUsage(request: ListUsageRequest) { return getGitpodService().server.listUsage(request); }, { - cacheTime: 1000 * 60 * 10, // 10 minutes - staleTime: 1000 * 60 * 10, // 10 minutes + cacheTime: 1000 * 60 * 1, // 1 minutes + staleTime: 1000 * 60 * 1, // 1 minutes retry: false, }, ); diff --git a/components/dashboard/src/usage/UsageView.tsx b/components/dashboard/src/usage/UsageView.tsx index 4c19904250a817..3573ba599b80bd 100644 --- a/components/dashboard/src/usage/UsageView.tsx +++ b/components/dashboard/src/usage/UsageView.tsx @@ -24,12 +24,20 @@ import classNames from "classnames"; import { UsageDateFilters } from "./UsageDateFilters"; import { DownloadUsage } from "./download/DownloadUsage"; import { useQueryParams } from "../hooks/use-query-params"; +import { useFeatureFlag } from "../data/featureflag-query"; const DATE_PARAM_FORMAT = "YYYY-MM-DD"; interface UsageViewProps { attributionId: AttributionId; } + +const durationUnitMap: Record = { + s: "seconds", + m: "minutes", + h: "hours", +}; + export const UsageView: FC = ({ attributionId }) => { const location = useLocation(); const history = useHistory(); @@ -88,9 +96,26 @@ export const UsageView: FC = ({ attributionId }) => { const usageEntries = usagePage.data?.usageEntriesList || []; + const schedulerDuration = useFeatureFlag("usage_update_scheduler_duration"); + + const readableSchedulerDuration = useMemo(() => { + const duration = schedulerDuration.toString().toLowerCase(); + if (duration === "undefined") { + return "15 minutes"; + } + const unit = duration.slice(-1); + const unitStr = durationUnitMap[unit]; + if (!unitStr) { + console.error("failed to parse duration", duration); + return "15 minutes"; + } + const value = parseInt(duration.slice(0, -1), 10); + return `${value} ${unitStr}`; + }, [schedulerDuration]); + return ( <> -
+
> usage %d", usage.ReconcileUsageTimes) + log.Infof(">> bill %d", usage.ResetUsageTimes) + gotUsage = append(gotUsage, usage.ReconcileUsageTimes) + gotBill = append(gotBill, usage.ResetUsageTimes) + case <-ctx.Done(): + break + } + } + if len(gotUsage) != len(tt.expectUsageTimes) { + t.Errorf("%s expected ReconcileUsageTimes %v, got %v", tt.name, tt.expectUsageTimes, gotUsage) + return + } + if len(gotBill) != len(tt.expectBillTImes) { + t.Errorf("%s expected ResetUsageTimes %v, got %v", tt.name, tt.expectBillTImes, gotBill) + return + } + for i, v := range tt.expectUsageTimes { + if gotUsage[i] != v { + t.Errorf("%s expected ReconcileUsageTimes %v, got %v", tt.name, tt.expectUsageTimes, gotUsage) + break + } + } + for i, v := range tt.expectBillTImes { + if gotBill[i] != v { + t.Errorf("%s expected ResetUsageTimes %v, got %v", tt.name, tt.expectBillTImes, gotBill) + break + } + } + log.Infof("test %s completed", tt.name) + }) + } + +} + +type mockUsageService struct { + ReconcileUsageTimes int + ReconcileUsageTimesArr []int + ResetUsageTimes int +} + +var _ v1.UsageServiceClient = (*mockUsageService)(nil) + +func (m *mockUsageService) AddUsageCreditNote(ctx context.Context, in *v1.AddUsageCreditNoteRequest, opts ...grpc.CallOption) (*v1.AddUsageCreditNoteResponse, error) { + panic("unimplemented") +} + +func (m *mockUsageService) GetBalance(ctx context.Context, in *v1.GetBalanceRequest, opts ...grpc.CallOption) (*v1.GetBalanceResponse, error) { + panic("unimplemented") +} + +func (m *mockUsageService) GetCostCenter(ctx context.Context, in *v1.GetCostCenterRequest, opts ...grpc.CallOption) (*v1.GetCostCenterResponse, error) { + panic("unimplemented") +} + +func (m *mockUsageService) ListUsage(ctx context.Context, in *v1.ListUsageRequest, opts ...grpc.CallOption) (*v1.ListUsageResponse, error) { + panic("unimplemented") +} + +func (m *mockUsageService) ReconcileUsage(ctx context.Context, in *v1.ReconcileUsageRequest, opts ...grpc.CallOption) (*v1.ReconcileUsageResponse, error) { + log.Info("usage.ReconcileUsage") + m.ReconcileUsageTimes++ + return &v1.ReconcileUsageResponse{}, nil +} + +func (m *mockUsageService) ResetUsage(ctx context.Context, in *v1.ResetUsageRequest, opts ...grpc.CallOption) (*v1.ResetUsageResponse, error) { + log.Info("usage.ResetUsage") + m.ResetUsageTimes++ + return &v1.ResetUsageResponse{}, nil +} + +func (m *mockUsageService) SetCostCenter(ctx context.Context, in *v1.SetCostCenterRequest, opts ...grpc.CallOption) (*v1.SetCostCenterResponse, error) { + panic("unimplemented") +} + +type mockBillService struct { + ReconcileInvoicesTimes int +} + +var _ v1.BillingServiceClient = (*mockBillService)(nil) + +func (m *mockBillService) CancelSubscription(ctx context.Context, in *v1.CancelSubscriptionRequest, opts ...grpc.CallOption) (*v1.CancelSubscriptionResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) CreateHoldPaymentIntent(ctx context.Context, in *v1.CreateHoldPaymentIntentRequest, opts ...grpc.CallOption) (*v1.CreateHoldPaymentIntentResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) CreateStripeCustomer(ctx context.Context, in *v1.CreateStripeCustomerRequest, opts ...grpc.CallOption) (*v1.CreateStripeCustomerResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) CreateStripeSubscription(ctx context.Context, in *v1.CreateStripeSubscriptionRequest, opts ...grpc.CallOption) (*v1.CreateStripeSubscriptionResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) FinalizeInvoice(ctx context.Context, in *v1.FinalizeInvoiceRequest, opts ...grpc.CallOption) (*v1.FinalizeInvoiceResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) GetPriceInformation(ctx context.Context, in *v1.GetPriceInformationRequest, opts ...grpc.CallOption) (*v1.GetPriceInformationResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) GetStripeCustomer(ctx context.Context, in *v1.GetStripeCustomerRequest, opts ...grpc.CallOption) (*v1.GetStripeCustomerResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) OnChargeDispute(ctx context.Context, in *v1.OnChargeDisputeRequest, opts ...grpc.CallOption) (*v1.OnChargeDisputeResponse, error) { + panic("unimplemented") +} + +func (m *mockBillService) ReconcileInvoices(ctx context.Context, in *v1.ReconcileInvoicesRequest, opts ...grpc.CallOption) (*v1.ReconcileInvoicesResponse, error) { + log.Info("bill.ReconcileInvoices") + m.ReconcileInvoicesTimes++ + return &v1.ReconcileInvoicesResponse{}, nil +} + +func (m *mockBillService) UpdateCustomerSubscriptionsTaxState(ctx context.Context, in *v1.UpdateCustomerSubscriptionsTaxStateRequest, opts ...grpc.CallOption) (*v1.UpdateCustomerSubscriptionsTaxStateResponse, error) { + panic("unimplemented") +} + +type mockExps struct { + StringValue string +} + +var _ experiments.Client = (*mockExps)(nil) + +func (m *mockExps) GetBoolValue(ctx context.Context, experimentName string, defaultValue bool, attributes experiments.Attributes) bool { + return defaultValue +} + +func (m *mockExps) GetFloatValue(ctx context.Context, experimentName string, defaultValue float64, attributes experiments.Attributes) float64 { + return defaultValue +} + +func (m *mockExps) GetIntValue(ctx context.Context, experimentName string, defaultValue int, attributes experiments.Attributes) int { + return defaultValue +} + +func (m *mockExps) GetStringValue(ctx context.Context, experimentName string, defaultValue string, attributes experiments.Attributes) string { + return m.StringValue +} diff --git a/install/installer/pkg/components/usage/configmap.go b/install/installer/pkg/components/usage/configmap.go index 6aebb62277a9a6..70a3bf55531c18 100644 --- a/install/installer/pkg/components/usage/configmap.go +++ b/install/installer/pkg/components/usage/configmap.go @@ -42,6 +42,7 @@ func configmap(ctx *common.RenderContext) ([]runtime.Object, error) { Address: common.ClusterAddress(redis.Component, ctx.Namespace, redis.Port), }, ServerAddress: common.ClusterAddress(common.ServerComponent, ctx.Namespace, common.ServerGRPCAPIPort), + GitpodHost: "https://" + ctx.Config.Domain, } expWebAppConfig := common.ExperimentalWebappConfig(ctx) diff --git a/install/installer/pkg/components/usage/configmap_test.go b/install/installer/pkg/components/usage/configmap_test.go index f413ef9f421a92..d38ab996cb295b 100644 --- a/install/installer/pkg/components/usage/configmap_test.go +++ b/install/installer/pkg/components/usage/configmap_test.go @@ -51,7 +51,8 @@ func TestConfigMap_ContainsSchedule(t *testing.T) { "address": "0.0.0.0:9001" } } - } + }, + "gitpodHost": "https://test.domain.everything.awesome.is" }`, cfgmap.Data[configJSONFilename], )