Refactor CacheService and MappingCacheService for improved performance and functionality
- Enhanced CacheService by implementing a more efficient pattern deletion method using a pipeline for batch processing. - Added new methods in CacheService to count keys and calculate memory usage by pattern, improving cache management capabilities. - Updated MappingCacheService to asynchronously retrieve cache statistics, ensuring accurate reporting of total keys and memory usage. - Refactored existing methods for better readability and maintainability, including the introduction of a private scanPattern method for key scanning.
This commit is contained in:
parent
c741ece844
commit
6d327d3ede
90
apps/bff/src/infra/cache/cache.service.ts
vendored
90
apps/bff/src/infra/cache/cache.service.ts
vendored
@ -45,34 +45,62 @@ export class CacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delPattern(pattern: string): Promise<void> {
|
async delPattern(pattern: string): Promise<void> {
|
||||||
let cursor = "0";
|
const pipeline = this.redis.pipeline();
|
||||||
const batch: string[] = [];
|
let pending = 0;
|
||||||
|
|
||||||
const flush = async () => {
|
const flush = async () => {
|
||||||
if (batch.length > 0) {
|
if (pending === 0) {
|
||||||
const pipeline = this.redis.pipeline();
|
return;
|
||||||
for (const k of batch.splice(0, batch.length)) pipeline.del(k);
|
|
||||||
await pipeline.exec();
|
|
||||||
}
|
}
|
||||||
|
await pipeline.exec();
|
||||||
|
pending = 0;
|
||||||
};
|
};
|
||||||
do {
|
|
||||||
const [next, keys] = (await this.redis.scan(
|
await this.scanPattern(pattern, async keys => {
|
||||||
cursor,
|
keys.forEach(key => {
|
||||||
"MATCH",
|
pipeline.del(key);
|
||||||
pattern,
|
pending += 1;
|
||||||
"COUNT",
|
});
|
||||||
1000
|
if (pending >= 1000) {
|
||||||
)) as unknown as [string, string[]];
|
await flush();
|
||||||
cursor = next;
|
|
||||||
if (keys && keys.length) {
|
|
||||||
batch.push(...keys);
|
|
||||||
if (batch.length >= 1000) {
|
|
||||||
await flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} while (cursor !== "0");
|
});
|
||||||
|
|
||||||
await flush();
|
await flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async countByPattern(pattern: string): Promise<number> {
|
||||||
|
let total = 0;
|
||||||
|
await this.scanPattern(pattern, keys => {
|
||||||
|
total += keys.length;
|
||||||
|
});
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
async memoryUsageByPattern(pattern: string): Promise<number> {
|
||||||
|
let total = 0;
|
||||||
|
await this.scanPattern(pattern, async keys => {
|
||||||
|
const pipeline = this.redis.pipeline();
|
||||||
|
keys.forEach(key => {
|
||||||
|
pipeline.memory("usage", key);
|
||||||
|
});
|
||||||
|
const results = await pipeline.exec();
|
||||||
|
if (!results) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
results.forEach(result => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [error, usage] = result as [Error | null, unknown];
|
||||||
|
if (!error && typeof usage === "number") {
|
||||||
|
total += usage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
async exists(key: string): Promise<boolean> {
|
async exists(key: string): Promise<boolean> {
|
||||||
return (await this.redis.exists(key)) === 1;
|
return (await this.redis.exists(key)) === 1;
|
||||||
}
|
}
|
||||||
@ -91,4 +119,24 @@ export class CacheService {
|
|||||||
await this.set(key, fresh, ttlSeconds);
|
await this.set(key, fresh, ttlSeconds);
|
||||||
return fresh;
|
return fresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async scanPattern(
|
||||||
|
pattern: string,
|
||||||
|
onKeys: (keys: string[]) => Promise<void> | void
|
||||||
|
): Promise<void> {
|
||||||
|
let cursor = "0";
|
||||||
|
do {
|
||||||
|
const [next, keys] = (await this.redis.scan(
|
||||||
|
cursor,
|
||||||
|
"MATCH",
|
||||||
|
pattern,
|
||||||
|
"COUNT",
|
||||||
|
1000
|
||||||
|
)) as unknown as [string, string[]];
|
||||||
|
cursor = next;
|
||||||
|
if (keys && keys.length) {
|
||||||
|
await onKeys(keys);
|
||||||
|
}
|
||||||
|
} while (cursor !== "0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,14 +83,17 @@ export class MappingCacheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getStats(): { totalKeys: number; memoryUsage: number } {
|
async getStats(): Promise<{ totalKeys: number; memoryUsage: number }> {
|
||||||
let result = { totalKeys: 0, memoryUsage: 0 };
|
const pattern = `${this.CACHE_PREFIX}:*`;
|
||||||
try {
|
try {
|
||||||
result = { totalKeys: 0, memoryUsage: 0 };
|
const totalKeys = await this.cacheService.countByPattern(pattern);
|
||||||
|
const memoryUsage =
|
||||||
|
totalKeys === 0 ? 0 : await this.cacheService.memoryUsageByPattern(pattern);
|
||||||
|
return { totalKeys, memoryUsage };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error("Failed to get cache stats", { error: getErrorMessage(error) });
|
this.logger.error("Failed to get cache stats", { error: getErrorMessage(error) });
|
||||||
|
return { totalKeys: 0, memoryUsage: 0 };
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async get<T>(key: string): Promise<T | null> {
|
private async get<T>(key: string): Promise<T | null> {
|
||||||
|
|||||||
@ -186,13 +186,6 @@ export class SubscriptionsService {
|
|||||||
cancelled: number;
|
cancelled: number;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
// Get WHMCS client ID from user mapping
|
|
||||||
const mapping = await this.mappingsService.findByUserId(userId);
|
|
||||||
if (!mapping?.whmcsClientId) {
|
|
||||||
throw new NotFoundException("WHMCS client mapping not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all subscriptions and aggregate customer-facing stats only
|
|
||||||
const subscriptionList = await this.getSubscriptions(userId);
|
const subscriptionList = await this.getSubscriptions(userId);
|
||||||
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
const subscriptions: Subscription[] = subscriptionList.subscriptions;
|
||||||
|
|
||||||
@ -282,7 +275,7 @@ export class SubscriptionsService {
|
|||||||
async searchSubscriptions(userId: string, query: string): Promise<Subscription[]> {
|
async searchSubscriptions(userId: string, query: string): Promise<Subscription[]> {
|
||||||
try {
|
try {
|
||||||
if (!query || query.trim().length < 2) {
|
if (!query || query.trim().length < 2) {
|
||||||
throw new Error("Search query must be at least 2 characters long");
|
throw new BadRequestException("Search query must be at least 2 characters long");
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionList = await this.getSubscriptions(userId);
|
const subscriptionList = await this.getSubscriptions(userId);
|
||||||
@ -318,6 +311,7 @@ export class SubscriptionsService {
|
|||||||
options: { page?: number; limit?: number } = {}
|
options: { page?: number; limit?: number } = {}
|
||||||
): Promise<InvoiceList> {
|
): Promise<InvoiceList> {
|
||||||
const { page = 1, limit = 10 } = options;
|
const { page = 1, limit = 10 } = options;
|
||||||
|
const batchSize = Math.min(100, Math.max(limit, 25));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate subscription exists and belongs to user
|
// Validate subscription exists and belongs to user
|
||||||
@ -329,51 +323,35 @@ export class SubscriptionsService {
|
|||||||
throw new NotFoundException("WHMCS client mapping not found");
|
throw new NotFoundException("WHMCS client mapping not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all invoices for the user WITH ITEMS (needed for subscription linking)
|
const relatedInvoices: Invoice[] = [];
|
||||||
// Note: Server-side filtering is handled by the WHMCS service via status and date filters.
|
let currentPage = 1;
|
||||||
// Further performance optimization may involve pagination or selective field retrieval.
|
let totalPages = 1;
|
||||||
const invoicesResponse = await this.whmcsService.getInvoicesWithItems(
|
|
||||||
mapping.whmcsClientId,
|
|
||||||
userId,
|
|
||||||
{ page: 1, limit: 1000 } // Get more to filter locally
|
|
||||||
);
|
|
||||||
|
|
||||||
const allInvoices = invoicesResponse;
|
do {
|
||||||
|
const invoiceBatch = await this.whmcsService.getInvoicesWithItems(
|
||||||
|
mapping.whmcsClientId,
|
||||||
|
userId,
|
||||||
|
{ page: currentPage, limit: batchSize }
|
||||||
|
);
|
||||||
|
|
||||||
// Filter invoices that have items related to this subscription
|
totalPages = invoiceBatch.pagination.totalPages;
|
||||||
// Note: subscriptionId is the same as serviceId in our current WHMCS mapping
|
|
||||||
this.logger.debug(
|
|
||||||
`Filtering ${allInvoices.invoices.length} invoices for subscription ${subscriptionId}`,
|
|
||||||
{
|
|
||||||
totalInvoices: allInvoices.invoices.length,
|
|
||||||
invoicesWithItems: allInvoices.invoices.filter(
|
|
||||||
(inv: Invoice) => inv.items && inv.items.length > 0
|
|
||||||
).length,
|
|
||||||
subscriptionId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const relatedInvoices = allInvoices.invoices.filter((invoice: Invoice) => {
|
invoiceBatch.invoices.forEach(invoice => {
|
||||||
const hasItems = invoice.items && invoice.items.length > 0;
|
if (!invoice.items?.length) {
|
||||||
if (!hasItems) {
|
return;
|
||||||
this.logger.debug(`Invoice ${invoice.id} has no items`);
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasMatchingService = invoice.items?.some((item: InvoiceItem) => {
|
const hasMatchingService = invoice.items.some(
|
||||||
this.logger.debug(
|
(item: InvoiceItem) => item.serviceId === subscriptionId
|
||||||
`Checking item: serviceId=${item.serviceId}, subscriptionId=${subscriptionId}`,
|
|
||||||
{
|
|
||||||
itemServiceId: item.serviceId,
|
|
||||||
subscriptionId,
|
|
||||||
matches: item.serviceId === subscriptionId,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return item.serviceId === subscriptionId;
|
|
||||||
|
if (hasMatchingService) {
|
||||||
|
relatedInvoices.push(invoice);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return hasMatchingService;
|
currentPage += 1;
|
||||||
});
|
} while (currentPage <= totalPages);
|
||||||
|
|
||||||
// Apply pagination to filtered results
|
// Apply pagination to filtered results
|
||||||
const startIndex = (page - 1) * limit;
|
const startIndex = (page - 1) * limit;
|
||||||
@ -384,7 +362,7 @@ export class SubscriptionsService {
|
|||||||
invoices: paginatedInvoices,
|
invoices: paginatedInvoices,
|
||||||
pagination: {
|
pagination: {
|
||||||
page,
|
page,
|
||||||
totalPages: Math.ceil(relatedInvoices.length / limit),
|
totalPages: relatedInvoices.length === 0 ? 0 : Math.ceil(relatedInvoices.length / limit),
|
||||||
totalItems: relatedInvoices.length,
|
totalItems: relatedInvoices.length,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -154,16 +154,7 @@ export class UserProfileService {
|
|||||||
if (!mapping?.whmcsClientId) {
|
if (!mapping?.whmcsClientId) {
|
||||||
this.logger.warn(`No WHMCS mapping found for user ${userId}`);
|
this.logger.warn(`No WHMCS mapping found for user ${userId}`);
|
||||||
|
|
||||||
let currency = "JPY";
|
const currency = "JPY";
|
||||||
try {
|
|
||||||
const profile = await this.getProfile(userId);
|
|
||||||
currency = profile.currency_code || currency;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn("Could not fetch currency from profile", {
|
|
||||||
userId,
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary: DashboardSummary = {
|
const summary: DashboardSummary = {
|
||||||
stats: {
|
stats: {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { apiClient } from "@/lib/api";
|
import { apiClient } from "@/lib/api";
|
||||||
|
import { log } from "@/lib/logger";
|
||||||
import {
|
import {
|
||||||
orderDetailsSchema,
|
orderDetailsSchema,
|
||||||
orderSummarySchema,
|
orderSummarySchema,
|
||||||
@ -15,26 +16,28 @@ async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: st
|
|||||||
...(payload.configurations ? { configurations: payload.configurations } : {}),
|
...(payload.configurations ? { configurations: payload.configurations } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug logging
|
log.debug("Creating order", {
|
||||||
console.log("[DEBUG] Creating order with payload:", JSON.stringify(body, null, 2));
|
orderType: body.orderType,
|
||||||
console.log("[DEBUG] Order Type:", body.orderType);
|
skuCount: body.skus.length,
|
||||||
console.log("[DEBUG] SKUs:", body.skus);
|
hasConfigurations: !!body.configurations,
|
||||||
console.log("[DEBUG] Configurations:", body.configurations);
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.POST("/api/orders", { body });
|
const response = await apiClient.POST("/api/orders", { body });
|
||||||
|
|
||||||
console.log("[DEBUG] Response:", response);
|
|
||||||
|
|
||||||
const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>(
|
const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>(
|
||||||
response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }>
|
response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }>
|
||||||
);
|
);
|
||||||
return { sfOrderId: parsed.data.sfOrderId };
|
return { sfOrderId: parsed.data.sfOrderId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[DEBUG] Order creation failed:", error);
|
log.error(
|
||||||
if (error && typeof error === "object" && "body" in error) {
|
"Order creation failed",
|
||||||
console.error("[DEBUG] Error body:", error.body);
|
error instanceof Error ? error : undefined,
|
||||||
}
|
{
|
||||||
|
orderType: body.orderType,
|
||||||
|
skuCount: body.skus.length,
|
||||||
|
}
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user