feat: المجموعة G — Redis خط أول والقاعدة خط احتياط
الأساس: common/cache — كاش-جانبي فوق Redis. مبدأ صارم: فشل Redis لا يُسقط الطلب (يُسجَّل ويُرجَع للقاعدة) — الكاش تحسين أداء لا مصدر حقيقة. - G1: لغة المستخدم في Redis (كانت استعلاماً قبل كل إشعار)؛ تُكتب عند PATCH /users/me فيصير الإصابة دائمة - G2/G3: توكنات FCM في Redis — يُبطَل عند register وعند اكتشاف توكن ميت. البث الجماعي صار بلا استعلامات قاعدة - G4: كاش tenant resolve/config + إبطال عند الإنشاء - G5: كاش التعرفة و ride-types + إبطال صريح عند أي إنشاء - G6: التقييم يتراكم في Redis، والقاعدة تُكتب مرة واحدة يومياً لكل طرف (حجز الكتابة ذرّي بـLua). التأخير مقصود ليبعد الاحتكاك بين الطرفين. أُضيف ratings.target_user_id و users.rating — تقييم السائق للراكب كان يُخزَّن بلا هدف فلا يُجمَّع أبداً - كاش الخرائط (route/reverse/geocode) بإحداثيات مقرَّبة 4 خانات (~11م): نداء انطلق كان يهيمن على p50 (2.9s). لا يُخزَّن الرجوع لخط مستقيم ولا استجابة فاشلة تصحيح: ادّعيت سابقاً أن tenants.resolve يعمل على كل request — خطأ. tenant_id يأتي من الـJWT؛ resolve يُستدعى عند الدخول و3 كنترولرات فقط. الحمل الأثقل فعلياً هو رفع موقع السائق (المجموعة H). هجرة: RatingsBothParties. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
857652404b
commit
a83dd7e5e5
+68
@@ -0,0 +1,68 @@
|
||||
import RedisMock from 'ioredis-mock';
|
||||
import { CacheService, CacheKeys } from './cache.service';
|
||||
|
||||
describe('CacheService', () => {
|
||||
let redis: any;
|
||||
let cache: CacheService;
|
||||
|
||||
beforeEach(() => {
|
||||
redis = new RedisMock({ keyPrefix: 'tripz:' });
|
||||
cache = new CacheService(redis);
|
||||
});
|
||||
|
||||
it('يخزّن ويقرأ قيمة مركّبة', async () => {
|
||||
await cache.set('k', { a: 1, b: ['x'] }, 60);
|
||||
expect(await cache.get<{ a: number; b: string[] }>('k')).toEqual({ a: 1, b: ['x'] });
|
||||
});
|
||||
|
||||
it('يرجع null لمفتاح غائب', async () => {
|
||||
expect(await cache.get('nope')).toBeNull();
|
||||
});
|
||||
|
||||
it('wrap: يحمّل من المصدر مرة واحدة ثم يخدم من الكاش', async () => {
|
||||
const loader = jest.fn().mockResolvedValue({ id: 't1' });
|
||||
|
||||
expect(await cache.wrap('k', 60, loader)).toEqual({ id: 't1' });
|
||||
expect(await cache.wrap('k', 60, loader)).toEqual({ id: 't1' });
|
||||
expect(loader).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('wrap: لا يخزّن null — الغياب المؤقّت لا يُثبَّت', async () => {
|
||||
const loader = jest.fn().mockResolvedValue(null);
|
||||
|
||||
expect(await cache.wrap('k', 60, loader)).toBeNull();
|
||||
expect(await cache.wrap('k', 60, loader)).toBeNull();
|
||||
expect(loader).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('del يُبطل المفتاح فيعود المصدر', async () => {
|
||||
const loader = jest.fn().mockResolvedValue('v1');
|
||||
await cache.wrap('k', 60, loader);
|
||||
await cache.del('k');
|
||||
await cache.wrap('k', 60, loader);
|
||||
expect(loader).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('mget يقرأ عدة مفاتيح ويعطي null للغائب', async () => {
|
||||
await cache.set('a', 1, 60);
|
||||
await cache.set('c', 3, 60);
|
||||
expect(await cache.mget<number>(['a', 'b', 'c'])).toEqual([1, null, 3]);
|
||||
});
|
||||
|
||||
it('فشل Redis لا يُسقط الطلب — يرجع للمصدر', async () => {
|
||||
const broken = {
|
||||
get: jest.fn().mockRejectedValue(new Error('redis down')),
|
||||
set: jest.fn().mockRejectedValue(new Error('redis down')),
|
||||
} as any;
|
||||
const c = new CacheService(broken);
|
||||
const loader = jest.fn().mockResolvedValue('from-db');
|
||||
|
||||
expect(await c.wrap('k', 60, loader)).toBe('from-db');
|
||||
expect(loader).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('المفاتيح معزولة بالمستأجر', () => {
|
||||
expect(CacheKeys.userLang('t1', 'u1')).not.toBe(CacheKeys.userLang('t2', 'u1'));
|
||||
expect(CacheKeys.tariff('t1', 'amman', 'economy')).toBe('tariff:t1:amman:economy');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user