优化数据库读写
This commit is contained in:
1 parent
f264103417
commit
56f1d7a273
11 files changed
+254
-21
No files matched your search
Vendored
+55
-10
@@ -5,26 +5,69 @@ function positiveInteger(value, fallback, maximum = Number.MAX_SAFE_INTEGER) {
|
||||
return Number.isInteger(parsed) && parsed > 0 ? Math.min(parsed, maximum) : fallback;
|
||||
}
|
||||
|
||||
function disabledCache(status = 'disabled') {
|
||||
function disabledCache(status = 'disabled', { ttlSeconds = 60, maxEntries = 200 } = {}) {
|
||||
const values = new Map();
|
||||
const pending = new Map();
|
||||
const generations = new Map();
|
||||
|
||||
const cacheKey = (namespace, key) => `${namespace}:${key}`;
|
||||
const generation = namespace => generations.get(namespace) || 0;
|
||||
const prune = () => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of values) if (entry.expiresAt <= now) values.delete(key);
|
||||
while (values.size > maxEntries) values.delete(values.keys().next().value);
|
||||
};
|
||||
|
||||
return {
|
||||
enabled: false,
|
||||
status,
|
||||
async remember(_namespace, _key, loader) {
|
||||
return loader();
|
||||
async remember(namespace, key, loader, options = {}) {
|
||||
const fullKey = cacheKey(namespace, key);
|
||||
const cached = values.get(fullKey);
|
||||
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
||||
if (cached) values.delete(fullKey);
|
||||
const startedGeneration = generation(namespace);
|
||||
const active = pending.get(fullKey);
|
||||
if (active?.generation === startedGeneration) return active.promise;
|
||||
|
||||
const loading = Promise.resolve(loader()).then(value => {
|
||||
if (generation(namespace) === startedGeneration) {
|
||||
const lifetime = positiveInteger(options.ttlSeconds, ttlSeconds, 86400);
|
||||
values.set(fullKey, { value, expiresAt: Date.now() + lifetime * 1000 });
|
||||
prune();
|
||||
}
|
||||
return value;
|
||||
}).finally(() => {
|
||||
if (pending.get(fullKey)?.promise === loading) pending.delete(fullKey);
|
||||
});
|
||||
pending.set(fullKey, { generation: startedGeneration, promise: loading });
|
||||
return loading;
|
||||
},
|
||||
async invalidate() { return false; },
|
||||
async close() {}
|
||||
async invalidate(namespace) {
|
||||
generations.set(namespace, generation(namespace) + 1);
|
||||
const prefix = `${namespace}:`;
|
||||
for (const key of values.keys()) if (key.startsWith(prefix)) values.delete(key);
|
||||
// Preserve the public meaning of this return value: no Redis namespace
|
||||
// was refreshed, even though the local fallback was invalidated.
|
||||
return false;
|
||||
},
|
||||
async close() {
|
||||
values.clear();
|
||||
pending.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRedisCache({ env = process.env, logger = console, clientFactory = createClient } = {}) {
|
||||
const url = String(env.REDIS_URL || '').trim();
|
||||
if (!url) return disabledCache();
|
||||
const defaultTtlSeconds = positiveInteger(env.REDIS_CACHE_TTL_SECONDS, 60, 86400);
|
||||
const localMaxEntries = positiveInteger(env.LOCAL_CACHE_MAX_ENTRIES, 200, 5000);
|
||||
if (!url) return disabledCache('disabled', { ttlSeconds: defaultTtlSeconds, maxEntries: localMaxEntries });
|
||||
const fallback = disabledCache('unavailable', { ttlSeconds: defaultTtlSeconds, maxEntries: localMaxEntries });
|
||||
|
||||
const prefix = String(env.REDIS_CACHE_PREFIX || 'exam-information')
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9:_-]/g, '-') || 'exam-information';
|
||||
const defaultTtlSeconds = positiveInteger(env.REDIS_CACHE_TTL_SECONDS, 60, 86400);
|
||||
const connectTimeout = positiveInteger(env.REDIS_CONNECT_TIMEOUT_MS, 1500, 30000);
|
||||
const pending = new Map();
|
||||
let warningReported = false;
|
||||
@@ -54,7 +97,7 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
} catch (error) {
|
||||
warn(error);
|
||||
if (client.isOpen) client.destroy();
|
||||
return disabledCache('unavailable');
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const versionKey = namespace => `${prefix}:namespace:${namespace}`;
|
||||
@@ -75,7 +118,7 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
return client.isReady ? 'ready' : 'unavailable';
|
||||
},
|
||||
async remember(namespace, key, loader, { ttlSeconds = defaultTtlSeconds } = {}) {
|
||||
if (!client.isReady) return loader();
|
||||
if (!client.isReady) return fallback.remember(namespace, key, loader, { ttlSeconds });
|
||||
try {
|
||||
const version = await namespaceVersion(namespace);
|
||||
const cacheKey = `${prefix}:${namespace}:${version}:${key}`;
|
||||
@@ -99,10 +142,11 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
return loading;
|
||||
} catch (error) {
|
||||
warn(error);
|
||||
return loader();
|
||||
return fallback.remember(namespace, key, loader, { ttlSeconds });
|
||||
}
|
||||
},
|
||||
async invalidate(namespace) {
|
||||
await fallback.invalidate(namespace);
|
||||
if (!client.isReady) return false;
|
||||
try {
|
||||
await client.incr(versionKey(namespace));
|
||||
@@ -113,6 +157,7 @@ export async function createRedisCache({ env = process.env, logger = console, cl
|
||||
}
|
||||
},
|
||||
async close() {
|
||||
await fallback.close();
|
||||
if (client.isOpen) await client.quit();
|
||||
}
|
||||
};
|
||||
|
||||
+18
-1
@@ -1,4 +1,6 @@
|
||||
export async function api(path, options = {}) {
|
||||
const pendingReads = new Map();
|
||||
|
||||
async function request(path, options) {
|
||||
const binaryBody = options.body instanceof ArrayBuffer || options.body instanceof Blob || options.body instanceof FormData;
|
||||
const response = await fetch(path, {
|
||||
credentials: 'same-origin',
|
||||
@@ -15,3 +17,18 @@ export async function api(path, options = {}) {
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function api(path, options = {}) {
|
||||
const method = String(options.method || 'GET').toUpperCase();
|
||||
if (method !== 'GET' || options.body != null || options.signal) return request(path, options);
|
||||
|
||||
// A quick double click or repeated render must not download and parse the
|
||||
// same large JSON response more than once while the first request is active.
|
||||
const key = String(path);
|
||||
if (pendingReads.has(key)) return pendingReads.get(key);
|
||||
const loading = request(path, options).finally(() => {
|
||||
if (pendingReads.get(key) === loading) pendingReads.delete(key);
|
||||
});
|
||||
pendingReads.set(key, loading);
|
||||
return loading;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { synchronizeMysqlPartitions } from './partition-storage.mjs';
|
||||
|
||||
import { createStateCache } from './state-cache.mjs';
|
||||
|
||||
export function createMysqlAdapter(context) {
|
||||
const {
|
||||
mkdir,
|
||||
@@ -408,7 +410,9 @@ export function createMysqlAdapter(context) {
|
||||
partitionConnection.release();
|
||||
}
|
||||
|
||||
let stateCache;
|
||||
const transaction = async operations => {
|
||||
stateCache?.invalidate();
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
@@ -419,10 +423,11 @@ export function createMysqlAdapter(context) {
|
||||
await connection.rollback();
|
||||
throw error;
|
||||
} finally {
|
||||
stateCache?.invalidate();
|
||||
connection.release();
|
||||
}
|
||||
};
|
||||
const read = async () => {
|
||||
const loadState = async () => {
|
||||
const connection = await pool.getConnection();
|
||||
try {
|
||||
await connection.beginTransaction();
|
||||
@@ -436,10 +441,11 @@ export function createMysqlAdapter(context) {
|
||||
connection.release();
|
||||
}
|
||||
};
|
||||
stateCache = createStateCache({ load: loadState });
|
||||
return createRepository({
|
||||
client: 'mysql',
|
||||
location: connectionUrl ? 'DATABASE_URL' : `${process.env.MYSQL_HOST}:${process.env.MYSQL_PORT || 3306}/${database}`,
|
||||
read,
|
||||
read: stateCache.read,
|
||||
transaction,
|
||||
close: async () => pool.end()
|
||||
});
|
||||
|
||||
Loaded 3 of 11 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user