@juspay/yama 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,146 @@
3
3
  * Provides intelligent caching for PR data, file contents, and AI responses
4
4
  */
5
5
  import NodeCache from "node-cache";
6
+ import { CacheError, } from "../types/index.js";
6
7
  import { logger } from "./Logger.js";
8
+ /**
9
+ * Enhanced cache error detection utility
10
+ * Provides multi-layer error classification to avoid false positives
11
+ */
12
+ class CacheErrorDetector {
13
+ /**
14
+ * Detect if an error is cache-related using multiple strategies
15
+ */
16
+ static isCacheError(error, operation, key) {
17
+ // Strategy 1: Check error type/class (most reliable)
18
+ if (error instanceof CacheError) {
19
+ return true;
20
+ }
21
+ // Strategy 2: Check for specific cache error patterns in NodeCache
22
+ if (error instanceof Error) {
23
+ const errorMessage = error.message.toLowerCase();
24
+ const stackTrace = error.stack?.toLowerCase() || "";
25
+ // Check for NodeCache-specific error patterns
26
+ const nodeCachePatterns = [
27
+ /node_modules\/node-cache/,
28
+ /cache\.js:\d+/,
29
+ /nodecache/,
30
+ ];
31
+ const isNodeCacheError = nodeCachePatterns.some((pattern) => pattern.test(stackTrace));
32
+ if (isNodeCacheError) {
33
+ return true;
34
+ }
35
+ // Strategy 3: Check for specific cache-related error messages (more targeted)
36
+ const cacheSpecificPatterns = [
37
+ /cache.*(?:full|exhausted|limit)/,
38
+ /memory.*(?:cache|allocation).*(?:failed|error)/,
39
+ /storage.*(?:cache|quota).*(?:exceeded|full)/,
40
+ /cache.*(?:initialization|setup).*(?:failed|error)/,
41
+ /ttl.*(?:invalid|expired)/,
42
+ /cache.*(?:key|value).*(?:invalid|malformed)/,
43
+ ];
44
+ const hasCacheSpecificError = cacheSpecificPatterns.some((pattern) => pattern.test(errorMessage));
45
+ if (hasCacheSpecificError) {
46
+ return true;
47
+ }
48
+ // Strategy 4: Context-aware detection
49
+ if (operation && key) {
50
+ // If we're in a cache operation and get memory/storage errors, likely cache-related
51
+ const cacheOperations = [
52
+ "get",
53
+ "set",
54
+ "del",
55
+ "clear",
56
+ "has",
57
+ "getorset",
58
+ "getorsetresilient",
59
+ ];
60
+ const isCacheOperation = cacheOperations.includes(operation.toLowerCase());
61
+ const contextualPatterns = [
62
+ /^out of memory$/,
63
+ /storage quota exceeded/,
64
+ /disk full/,
65
+ ];
66
+ if (isCacheOperation &&
67
+ contextualPatterns.some((pattern) => pattern.test(errorMessage))) {
68
+ return true;
69
+ }
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ /**
75
+ * Classify cache error for better handling and logging
76
+ */
77
+ static classifyError(error, operation, key) {
78
+ if (!this.isCacheError(error, operation, key)) {
79
+ return {
80
+ isCache: false,
81
+ category: "unknown",
82
+ confidence: "high",
83
+ reason: "Not identified as cache-related error",
84
+ };
85
+ }
86
+ if (error instanceof CacheError) {
87
+ const category = error.code.includes("STORAGE")
88
+ ? "storage"
89
+ : error.code.includes("NETWORK")
90
+ ? "network"
91
+ : error.code.includes("SYSTEM")
92
+ ? "system"
93
+ : "operation";
94
+ return {
95
+ isCache: true,
96
+ category,
97
+ confidence: "high",
98
+ reason: `Explicit cache error: ${error.code}`,
99
+ };
100
+ }
101
+ if (error instanceof Error) {
102
+ const message = error.message.toLowerCase();
103
+ const stack = error.stack?.toLowerCase() || "";
104
+ // High confidence patterns
105
+ if (/node_modules\/node-cache/.test(stack)) {
106
+ return {
107
+ isCache: true,
108
+ category: "system",
109
+ confidence: "high",
110
+ reason: "NodeCache stack trace detected",
111
+ };
112
+ }
113
+ // Medium confidence patterns
114
+ if (/cache.*(?:full|exhausted)/.test(message)) {
115
+ return {
116
+ isCache: true,
117
+ category: "storage",
118
+ confidence: "medium",
119
+ reason: "Cache capacity error pattern",
120
+ };
121
+ }
122
+ if (/memory.*cache.*failed/.test(message)) {
123
+ return {
124
+ isCache: true,
125
+ category: "system",
126
+ confidence: "medium",
127
+ reason: "Memory allocation error in cache context",
128
+ };
129
+ }
130
+ }
131
+ return {
132
+ isCache: true,
133
+ category: "unknown",
134
+ confidence: "low",
135
+ reason: "Fallback detection",
136
+ };
137
+ }
138
+ }
7
139
  export class Cache {
8
140
  cache;
9
141
  statsData = {
10
142
  hits: 0,
11
143
  misses: 0,
144
+ cacheErrors: 0,
145
+ nonCacheErrors: 0,
12
146
  };
13
147
  constructor(options = {}) {
14
148
  const { ttl = 3600, // 1 hour default
@@ -33,18 +167,25 @@ export class Cache {
33
167
  });
34
168
  }
35
169
  /**
36
- * Get value from cache
170
+ * Get value from cache with resilient error handling
37
171
  */
38
172
  get(key) {
39
- const value = this.cache.get(key);
40
- if (value !== undefined) {
41
- this.statsData.hits++;
42
- logger.debug(`Cache HIT: ${key}`);
43
- return value;
173
+ try {
174
+ const value = this.cache.get(key);
175
+ if (value !== undefined) {
176
+ this.statsData.hits++;
177
+ logger.debug(`Cache HIT: ${key}`);
178
+ return value;
179
+ }
180
+ else {
181
+ this.statsData.misses++;
182
+ logger.debug(`Cache MISS: ${key}`);
183
+ return undefined;
184
+ }
44
185
  }
45
- else {
186
+ catch (error) {
46
187
  this.statsData.misses++;
47
- logger.debug(`Cache MISS: ${key}`);
188
+ logger.warn(`Cache GET error for ${key}, treating as miss:`, error);
48
189
  return undefined;
49
190
  }
50
191
  }
@@ -105,6 +246,8 @@ export class Cache {
105
246
  misses: this.statsData.misses,
106
247
  keys: this.cache.keys().length,
107
248
  size: this.cache.getStats().keys,
249
+ cacheErrors: this.statsData.cacheErrors,
250
+ nonCacheErrors: this.statsData.nonCacheErrors,
108
251
  };
109
252
  }
110
253
  /**
@@ -114,9 +257,10 @@ export class Cache {
114
257
  return this.cache.getStats();
115
258
  }
116
259
  /**
117
- * Get or set pattern - common caching pattern
260
+ * Get or set pattern with automatic fallback on cache failures
118
261
  */
119
262
  async getOrSet(key, fetchFn, ttl) {
263
+ // Try to get from cache with resilient error handling
120
264
  const cached = this.get(key);
121
265
  if (cached !== undefined) {
122
266
  return cached;
@@ -124,7 +268,13 @@ export class Cache {
124
268
  try {
125
269
  logger.debug(`Cache FETCH: ${key}`);
126
270
  const value = await fetchFn();
127
- this.set(key, value, ttl);
271
+ // Try to cache the result, but don't fail if caching fails
272
+ try {
273
+ this.set(key, value, ttl);
274
+ }
275
+ catch (cacheError) {
276
+ logger.warn(`Cache SET failed for ${key}, continuing without cache:`, cacheError);
277
+ }
128
278
  return value;
129
279
  }
130
280
  catch (error) {
@@ -132,6 +282,36 @@ export class Cache {
132
282
  throw error;
133
283
  }
134
284
  }
285
+ /**
286
+ * Resilient get or set pattern that bypasses cache entirely on cache system failures
287
+ */
288
+ async getOrSetResilient(key, fetchFn, ttl) {
289
+ try {
290
+ // Try normal cache flow first
291
+ return await this.getOrSet(key, fetchFn, ttl);
292
+ }
293
+ catch (error) {
294
+ // Use enhanced error detection to determine if this is a cache-related error
295
+ const errorClassification = CacheErrorDetector.classifyError(error, "getOrSet", key);
296
+ if (errorClassification.isCache) {
297
+ // Track cache error statistics
298
+ this.statsData.cacheErrors++;
299
+ logger.warn(`Cache system error detected for ${key} (${errorClassification.confidence} confidence: ${errorClassification.reason}), bypassing cache entirely`, {
300
+ error: error instanceof Error ? error.message : String(error),
301
+ category: errorClassification.category,
302
+ confidence: errorClassification.confidence,
303
+ key,
304
+ operation: "getOrSet",
305
+ });
306
+ // Bypass cache completely and just fetch the data
307
+ return await fetchFn();
308
+ }
309
+ // Track non-cache errors for debugging
310
+ this.statsData.nonCacheErrors++;
311
+ // Re-throw non-cache errors
312
+ throw error;
313
+ }
314
+ }
135
315
  /**
136
316
  * Cache with tags for group invalidation
137
317
  */
@@ -46,6 +46,9 @@ export declare class TokenBudgetManager implements TokenBudgetManagerInterface {
46
46
  private usedTokens;
47
47
  private batchAllocations;
48
48
  private reservedTokens;
49
+ private preAllocationMode;
50
+ private preAllocatedBatches;
51
+ private batchStates;
49
52
  constructor(totalBudget: number);
50
53
  /**
51
54
  * Allocate tokens for a specific batch
@@ -92,6 +95,31 @@ export declare class TokenBudgetManager implements TokenBudgetManagerInterface {
92
95
  * Reset the budget manager (for testing or reuse)
93
96
  */
94
97
  reset(): void;
98
+ /**
99
+ * Pre-allocate tokens for all batches upfront
100
+ * This ensures all batches have guaranteed token allocation before processing starts
101
+ */
102
+ preAllocateAllBatches(allocations: Map<number, number>): boolean;
103
+ /**
104
+ * Mark a batch as failed and handle cleanup
105
+ */
106
+ markBatchFailed(batchIndex: number, error?: string): void;
107
+ /**
108
+ * Get the current state of a batch
109
+ */
110
+ getBatchState(batchIndex: number): "pending" | "processing" | "completed" | "failed" | undefined;
111
+ /**
112
+ * Check if pre-allocation mode is active
113
+ */
114
+ isPreAllocationMode(): boolean;
115
+ /**
116
+ * Get all batch states for debugging
117
+ */
118
+ getAllBatchStates(): Map<number, "pending" | "processing" | "completed" | "failed">;
119
+ /**
120
+ * Disable pre-allocation mode and clean up
121
+ */
122
+ disablePreAllocationMode(): void;
95
123
  /**
96
124
  * Update the total budget (useful for dynamic adjustment)
97
125
  */
@@ -77,12 +77,18 @@ export class TokenBudgetManager {
77
77
  usedTokens = 0;
78
78
  batchAllocations = new Map();
79
79
  reservedTokens = 0; // Tokens allocated but not yet used
80
+ // NEW: Pre-allocation mode tracking
81
+ preAllocationMode = false;
82
+ preAllocatedBatches = new Set();
83
+ batchStates = new Map();
80
84
  constructor(totalBudget) {
81
85
  if (totalBudget <= 0) {
82
86
  throw new Error("Token budget must be greater than 0");
83
87
  }
84
- this.totalBudget = totalBudget;
85
- logger.debug(`TokenBudgetManager created with budget of ${totalBudget} tokens`);
88
+ // Floor the budget to ensure integer arithmetic and avoid floating-point precision issues.
89
+ // The fractional part is discarded, so the budget is always rounded down.
90
+ this.totalBudget = Math.floor(totalBudget);
91
+ logger.debug(`TokenBudgetManager created with budget of ${this.totalBudget} tokens (original: ${totalBudget})`);
86
92
  }
87
93
  /**
88
94
  * Allocate tokens for a specific batch
@@ -93,8 +99,31 @@ export class TokenBudgetManager {
93
99
  logger.warn(`Invalid token estimate for batch ${batchIndex}: ${estimatedTokens}`);
94
100
  return false;
95
101
  }
96
- // Check if we already have an allocation for this batch
102
+ // NEW: Handle pre-allocation mode
103
+ if (this.preAllocationMode && this.preAllocatedBatches.has(batchIndex)) {
104
+ // Check if batch is already being processed
105
+ const currentState = this.batchStates.get(batchIndex);
106
+ if (currentState === "processing") {
107
+ logger.warn(`Batch ${batchIndex} is already being processed`);
108
+ return false;
109
+ }
110
+ if (currentState !== "pending") {
111
+ logger.warn(`Batch ${batchIndex} is not in pending state (current: ${currentState})`);
112
+ return false;
113
+ }
114
+ // In pre-allocation mode, just mark batch as processing and return success
115
+ this.batchStates.set(batchIndex, "processing");
116
+ logger.debug(`Batch ${batchIndex} using pre-allocated tokens (${this.batchAllocations.get(batchIndex)} tokens)`);
117
+ return true;
118
+ }
119
+ // Check if we already have an allocation for this batch (non-pre-allocation mode)
97
120
  if (this.batchAllocations.has(batchIndex)) {
121
+ // Check if batch is already being processed
122
+ const currentState = this.batchStates.get(batchIndex);
123
+ if (currentState === "processing") {
124
+ logger.warn(`Batch ${batchIndex} is already being processed`);
125
+ return false;
126
+ }
98
127
  logger.warn(`Batch ${batchIndex} already has token allocation`);
99
128
  return false;
100
129
  }
@@ -108,6 +137,7 @@ export class TokenBudgetManager {
108
137
  // Allocate the tokens
109
138
  this.reservedTokens += estimatedTokens;
110
139
  this.batchAllocations.set(batchIndex, estimatedTokens);
140
+ this.batchStates.set(batchIndex, "processing");
111
141
  logger.debug(`Allocated ${estimatedTokens} tokens for batch ${batchIndex} ` +
112
142
  `(${this.getAvailableBudget()} remaining)`);
113
143
  return true;
@@ -122,10 +152,19 @@ export class TokenBudgetManager {
122
152
  logger.warn(`No token allocation found for batch ${batchIndex}`);
123
153
  return;
124
154
  }
155
+ // Update batch state to completed only if not already failed
156
+ const currentState = this.batchStates.get(batchIndex);
157
+ if (currentState !== "failed") {
158
+ this.batchStates.set(batchIndex, "completed");
159
+ }
125
160
  // Move from reserved to used (assuming the tokens were actually used)
126
161
  this.reservedTokens -= allocated;
127
162
  this.usedTokens += allocated;
128
163
  this.batchAllocations.delete(batchIndex);
164
+ // Clean up pre-allocation tracking
165
+ if (this.preAllocationMode) {
166
+ this.preAllocatedBatches.delete(batchIndex);
167
+ }
129
168
  logger.debug(`Released ${allocated} tokens from batch ${batchIndex} ` +
130
169
  `(${this.getAvailableBudget()} now available)`);
131
170
  }
@@ -182,6 +221,72 @@ export class TokenBudgetManager {
182
221
  this.batchAllocations.clear();
183
222
  logger.debug("TokenBudgetManager reset");
184
223
  }
224
+ /**
225
+ * Pre-allocate tokens for all batches upfront
226
+ * This ensures all batches have guaranteed token allocation before processing starts
227
+ */
228
+ preAllocateAllBatches(allocations) {
229
+ const totalRequired = Array.from(allocations.values()).reduce((sum, tokens) => sum + tokens, 0);
230
+ if (totalRequired > this.totalBudget) {
231
+ logger.error(`Pre-allocation failed: total required (${totalRequired}) exceeds budget (${this.totalBudget})`);
232
+ return false;
233
+ }
234
+ // Clear any existing allocations and reset state
235
+ this.batchAllocations.clear();
236
+ this.reservedTokens = 0;
237
+ this.batchStates.clear();
238
+ this.preAllocatedBatches.clear();
239
+ // Enable pre-allocation mode
240
+ this.preAllocationMode = true;
241
+ // Reserve all tokens upfront
242
+ allocations.forEach((tokens, batchIndex) => {
243
+ this.batchAllocations.set(batchIndex, tokens);
244
+ this.reservedTokens += tokens;
245
+ this.preAllocatedBatches.add(batchIndex);
246
+ this.batchStates.set(batchIndex, "pending");
247
+ });
248
+ logger.info(`Pre-allocated ${totalRequired} tokens across ${allocations.size} batches ` +
249
+ `(${this.getAvailableBudget()} remaining)`);
250
+ return true;
251
+ }
252
+ /**
253
+ * Mark a batch as failed and handle cleanup
254
+ */
255
+ markBatchFailed(batchIndex, error) {
256
+ this.batchStates.set(batchIndex, "failed");
257
+ if (error) {
258
+ logger.debug(`Batch ${batchIndex} marked as failed: ${error}`);
259
+ }
260
+ else {
261
+ logger.debug(`Batch ${batchIndex} marked as failed`);
262
+ }
263
+ }
264
+ /**
265
+ * Get the current state of a batch
266
+ */
267
+ getBatchState(batchIndex) {
268
+ return this.batchStates.get(batchIndex);
269
+ }
270
+ /**
271
+ * Check if pre-allocation mode is active
272
+ */
273
+ isPreAllocationMode() {
274
+ return this.preAllocationMode;
275
+ }
276
+ /**
277
+ * Get all batch states for debugging
278
+ */
279
+ getAllBatchStates() {
280
+ return new Map(this.batchStates);
281
+ }
282
+ /**
283
+ * Disable pre-allocation mode and clean up
284
+ */
285
+ disablePreAllocationMode() {
286
+ this.preAllocationMode = false;
287
+ this.preAllocatedBatches.clear();
288
+ logger.debug("Pre-allocation mode disabled");
289
+ }
185
290
  /**
186
291
  * Update the total budget (useful for dynamic adjustment)
187
292
  */
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Retry Manager for Yama
3
+ * Provides intelligent retry logic with exponential backoff for handling transient failures
4
+ */
5
+ export interface RetryOptions {
6
+ maxAttempts?: number;
7
+ baseDelayMs?: number;
8
+ maxDelayMs?: number;
9
+ backoffMultiplier?: number;
10
+ jitterMs?: number;
11
+ retryableErrors?: string[];
12
+ }
13
+ export interface RetryContext {
14
+ operation: string;
15
+ attempt: number;
16
+ maxAttempts: number;
17
+ lastError?: Error;
18
+ totalElapsed: number;
19
+ }
20
+ export declare class RetryManager {
21
+ private static readonly DEFAULT_OPTIONS;
22
+ /**
23
+ * Execute an operation with retry logic
24
+ */
25
+ static withRetry<T>(operation: () => Promise<T>, context: string, options?: RetryOptions): Promise<T>;
26
+ /**
27
+ * Check if an error is retryable based on error patterns
28
+ */
29
+ private static isRetryableError;
30
+ /**
31
+ * Calculate delay with exponential backoff and jitter
32
+ */
33
+ private static calculateDelay;
34
+ /**
35
+ * Sleep for specified milliseconds
36
+ */
37
+ private static sleep;
38
+ /**
39
+ * Create a retry wrapper function for a specific operation
40
+ */
41
+ static createRetryWrapper<T extends any[], R>(fn: (...args: T) => Promise<R>, context: string, options?: RetryOptions): (...args: T) => Promise<R>;
42
+ /**
43
+ * Batch retry operations with individual retry logic
44
+ */
45
+ static batchWithRetry<T>(operations: Array<{
46
+ fn: () => Promise<T>;
47
+ context: string;
48
+ }>, options?: RetryOptions & {
49
+ continueOnError?: boolean;
50
+ }): Promise<Array<{
51
+ success: boolean;
52
+ data?: T;
53
+ error?: Error;
54
+ context: string;
55
+ }>>;
56
+ /**
57
+ * Get retry statistics for monitoring
58
+ */
59
+ static getRetryStats(results: Array<{
60
+ success: boolean;
61
+ context: string;
62
+ }>): {
63
+ total: number;
64
+ successful: number;
65
+ failed: number;
66
+ successRate: number;
67
+ failuresByContext: Record<string, number>;
68
+ };
69
+ /**
70
+ * Create a circuit breaker pattern (simple implementation)
71
+ */
72
+ static createCircuitBreaker<T extends any[], R>(fn: (...args: T) => Promise<R>, context: string, options?: {
73
+ failureThreshold?: number;
74
+ recoveryTimeoutMs?: number;
75
+ retryOptions?: RetryOptions;
76
+ }): (...args: T) => Promise<R>;
77
+ }
78
+ //# sourceMappingURL=RetryManager.d.ts.map