bunsane 0.2.3 → 0.2.4
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.
package/config/cache.config.ts
CHANGED
|
@@ -66,7 +66,9 @@ export class CacheFactory {
|
|
|
66
66
|
retryStrategy: config.redis.retryStrategy,
|
|
67
67
|
maxRetriesPerRequest: 3,
|
|
68
68
|
lazyConnect: false,
|
|
69
|
-
enableReadyCheck: true
|
|
69
|
+
enableReadyCheck: true,
|
|
70
|
+
connectTimeout: config.redis.connectTimeout,
|
|
71
|
+
commandTimeout: config.redis.commandTimeout
|
|
70
72
|
};
|
|
71
73
|
|
|
72
74
|
const { password: _pw, ...safeConfig } = redisConfig;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CacheManager } from './CacheManager.js';
|
|
2
2
|
import { SchedulerManager } from '../SchedulerManager.js';
|
|
3
|
+
import { Entity } from '../Entity.js';
|
|
4
|
+
import { logger } from '../Logger.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* CacheWarmer preloads frequently accessed data into the cache to improve
|
|
@@ -34,7 +36,7 @@ export class CacheWarmer {
|
|
|
34
36
|
let warmed = 0;
|
|
35
37
|
let failed = 0;
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
logger.info({ msg: `Starting entity cache warming`, count: entityIds.length, entityType });
|
|
38
40
|
|
|
39
41
|
// Process entities in batches to avoid overwhelming the database
|
|
40
42
|
const batchSize = 10;
|
|
@@ -46,7 +48,7 @@ export class CacheWarmer {
|
|
|
46
48
|
const entities = await this.loadEntitiesBatch(batch, entityType);
|
|
47
49
|
warmed += entities.length;
|
|
48
50
|
} catch (error) {
|
|
49
|
-
|
|
51
|
+
logger.warn({ msg: 'Failed to warm batch of entities', error });
|
|
50
52
|
failed += batch.length;
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -55,7 +57,7 @@ export class CacheWarmer {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
const duration = Date.now() - startTime;
|
|
58
|
-
|
|
60
|
+
logger.info({ msg: 'Entity cache warming completed', warmed, failed, duration });
|
|
59
61
|
|
|
60
62
|
return { success: failed === 0, warmed, failed, duration };
|
|
61
63
|
}
|
|
@@ -71,7 +73,7 @@ export class CacheWarmer {
|
|
|
71
73
|
enabled?: boolean;
|
|
72
74
|
}): void {
|
|
73
75
|
if (!config.enabled) {
|
|
74
|
-
|
|
76
|
+
logger.debug({ msg: 'Cache warming job disabled', name: config.name });
|
|
75
77
|
return;
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -80,18 +82,18 @@ export class CacheWarmer {
|
|
|
80
82
|
|
|
81
83
|
const job = this.scheduler.scheduleJob(config.name, config.cronExpression, async () => {
|
|
82
84
|
try {
|
|
83
|
-
|
|
85
|
+
logger.info({ msg: 'Running scheduled cache warming', name: config.name });
|
|
84
86
|
|
|
85
87
|
if (config.type === 'entity') {
|
|
86
88
|
await this.warmEntityCache(config.config.entityIds, config.config.entityType);
|
|
87
89
|
}
|
|
88
90
|
} catch (error) {
|
|
89
|
-
|
|
91
|
+
logger.error({ msg: 'Scheduled cache warming failed', name: config.name, error });
|
|
90
92
|
}
|
|
91
93
|
});
|
|
92
94
|
|
|
93
95
|
this.warmingJobs.set(config.name, job);
|
|
94
|
-
|
|
96
|
+
logger.info({ msg: 'Scheduled cache warming job', name: config.name, cron: config.cronExpression });
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
/**
|
|
@@ -102,7 +104,7 @@ export class CacheWarmer {
|
|
|
102
104
|
if (job) {
|
|
103
105
|
job.cancel();
|
|
104
106
|
this.warmingJobs.delete(name);
|
|
105
|
-
|
|
107
|
+
logger.info({ msg: 'Cancelled cache warming job', name });
|
|
106
108
|
return true;
|
|
107
109
|
}
|
|
108
110
|
return false;
|
|
@@ -126,11 +128,17 @@ export class CacheWarmer {
|
|
|
126
128
|
}> {
|
|
127
129
|
const startTime = Date.now();
|
|
128
130
|
|
|
129
|
-
// Warm
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
// Warm all entity groups
|
|
132
|
+
let entityResults = { success: true, warmed: 0, failed: 0, duration: 0 };
|
|
133
|
+
if (config.entities) {
|
|
134
|
+
for (const entry of config.entities) {
|
|
135
|
+
const result = await this.warmEntityCache(entry.entityIds, entry.entityType);
|
|
136
|
+
entityResults.warmed += result.warmed;
|
|
137
|
+
entityResults.failed += result.failed;
|
|
138
|
+
entityResults.duration += result.duration;
|
|
139
|
+
if (!result.success) entityResults.success = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
134
142
|
|
|
135
143
|
const totalDuration = Date.now() - startTime;
|
|
136
144
|
|
|
@@ -141,17 +149,31 @@ export class CacheWarmer {
|
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
/**
|
|
144
|
-
* Loads a batch of entities
|
|
152
|
+
* Loads a batch of entities from the database and populates the cache.
|
|
153
|
+
* Uses Entity.FindById to load each entity with all its components,
|
|
154
|
+
* then writes the entity and its components into cache via CacheManager.
|
|
145
155
|
*/
|
|
146
|
-
private async loadEntitiesBatch(entityIds: string[], entityType: string): Promise<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
private async loadEntitiesBatch(entityIds: string[], entityType: string): Promise<Entity[]> {
|
|
157
|
+
const loaded: Entity[] = [];
|
|
158
|
+
|
|
159
|
+
const results = await Promise.allSettled(
|
|
160
|
+
entityIds.map(id => Entity.FindById(id))
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
for (const result of results) {
|
|
164
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
165
|
+
const entity = result.value;
|
|
166
|
+
loaded.push(entity);
|
|
167
|
+
|
|
168
|
+
await this.cacheManager.setEntityWriteThrough(entity);
|
|
169
|
+
const components = entity.componentList();
|
|
170
|
+
if (components.length > 0) {
|
|
171
|
+
await this.cacheManager.setComponentWriteThrough(entity.id, components);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
153
175
|
|
|
154
|
-
|
|
155
|
-
return
|
|
176
|
+
logger.debug({ msg: 'Loaded entity batch', entityType, requested: entityIds.length, loaded: loaded.length });
|
|
177
|
+
return loaded;
|
|
156
178
|
}
|
|
157
179
|
}
|
package/core/cache/RedisCache.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface RedisCacheConfig {
|
|
|
27
27
|
maxRetriesPerRequest?: number;
|
|
28
28
|
lazyConnect?: boolean;
|
|
29
29
|
enableReadyCheck?: boolean;
|
|
30
|
+
connectTimeout?: number;
|
|
31
|
+
commandTimeout?: number;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -60,7 +62,8 @@ export class RedisCache implements CacheProvider {
|
|
|
60
62
|
maxRetriesPerRequest: config.maxRetriesPerRequest || 3,
|
|
61
63
|
lazyConnect: config.lazyConnect || false,
|
|
62
64
|
enableReadyCheck: config.enableReadyCheck || false,
|
|
63
|
-
|
|
65
|
+
connectTimeout: config.connectTimeout ?? 5000,
|
|
66
|
+
commandTimeout: config.commandTimeout ?? 3000,
|
|
64
67
|
enableOfflineQueue: true,
|
|
65
68
|
};
|
|
66
69
|
|
|
@@ -194,19 +197,20 @@ export class RedisCache implements CacheProvider {
|
|
|
194
197
|
const prefixedKeys = keys.map(k => this.prefixKey(k));
|
|
195
198
|
const values = await this.client.mget(...prefixedKeys);
|
|
196
199
|
|
|
197
|
-
return values.map((value, index) => {
|
|
200
|
+
return await Promise.all(values.map(async (value, index) => {
|
|
198
201
|
if (value === null) {
|
|
199
202
|
this.stats.misses++;
|
|
200
203
|
return null;
|
|
201
204
|
}
|
|
202
205
|
this.stats.hits++;
|
|
203
206
|
try {
|
|
204
|
-
|
|
207
|
+
const parsed = JSON.parse(value);
|
|
208
|
+
return await CompressionUtils.decompress(parsed) as T;
|
|
205
209
|
} catch (parseError) {
|
|
206
210
|
logger.error({ error: parseError, key: keys[index], msg: 'Failed to parse cached value' });
|
|
207
211
|
return null;
|
|
208
212
|
}
|
|
209
|
-
});
|
|
213
|
+
}));
|
|
210
214
|
} catch (error) {
|
|
211
215
|
logger.error({ error, msg: 'Redis getMany error' });
|
|
212
216
|
return new Array(keys.length).fill(null);
|
|
@@ -222,7 +226,8 @@ export class RedisCache implements CacheProvider {
|
|
|
222
226
|
|
|
223
227
|
for (const entry of entries) {
|
|
224
228
|
const prefixedKey = this.prefixKey(entry.key);
|
|
225
|
-
const
|
|
229
|
+
const compressedValue = await CompressionUtils.compress(entry.value);
|
|
230
|
+
const serializedValue = JSON.stringify(compressedValue);
|
|
226
231
|
|
|
227
232
|
if (entry.ttl) {
|
|
228
233
|
pipeline.setex(prefixedKey, Math.floor(entry.ttl / 1000), serializedValue);
|