blob: cd58558eb69bc0e910b605494e05a7e0d51d897c [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { RedirectCache } from '../../src/client/RedirectCache';
import { EndPoint } from '../../src/utils/Config';
describe('RedirectCache', () => {
let cache: RedirectCache;
beforeEach(() => {
cache = new RedirectCache(300000, 10000); // 5 minutes TTL, 10000 max size
});
describe('Basic Operations', () => {
test('should store and retrieve endpoint', () => {
const endpoint: EndPoint = { host: 'node1', port: 6667 };
cache.set('device1', endpoint);
const retrieved = cache.get('device1');
expect(retrieved).toEqual(endpoint);
});
test('should return null for non-existent device', () => {
const retrieved = cache.get('non-existent');
expect(retrieved).toBeNull();
});
test('should remove cached endpoint', () => {
const endpoint: EndPoint = { host: 'node1', port: 6667 };
cache.set('device1', endpoint);
cache.remove('device1');
const retrieved = cache.get('device1');
expect(retrieved).toBeNull();
});
test('should clear all cached endpoints', () => {
cache.set('device1', { host: 'node1', port: 6667 });
cache.set('device2', { host: 'node2', port: 6668 });
cache.set('device3', { host: 'node3', port: 6669 });
cache.clear();
expect(cache.get('device1')).toBeNull();
expect(cache.get('device2')).toBeNull();
expect(cache.get('device3')).toBeNull();
});
test('should update existing endpoint', () => {
const endpoint1: EndPoint = { host: 'node1', port: 6667 };
const endpoint2: EndPoint = { host: 'node2', port: 6668 };
cache.set('device1', endpoint1);
cache.set('device1', endpoint2); // Update
const retrieved = cache.get('device1');
expect(retrieved).toEqual(endpoint2);
});
});
describe('TTL Expiration', () => {
test('should expire entries after TTL', async () => {
const shortTTLCache = new RedirectCache(100, 10000); // 100ms TTL
const endpoint: EndPoint = { host: 'node1', port: 6667 };
shortTTLCache.set('device1', endpoint);
expect(shortTTLCache.get('device1')).toEqual(endpoint);
// Wait for expiration
await new Promise((resolve) => setTimeout(resolve, 150));
const retrieved = shortTTLCache.get('device1');
expect(retrieved).toBeNull();
});
test('should not expire with TTL=0 (no expiration)', async () => {
const noExpiryCache = new RedirectCache(0, 10000); // No TTL
const endpoint: EndPoint = { host: 'node1', port: 6667 };
noExpiryCache.set('device1', endpoint);
// Wait some time
await new Promise((resolve) => setTimeout(resolve, 100));
const retrieved = noExpiryCache.get('device1');
expect(retrieved).toEqual(endpoint);
});
test('should return null for expired entry', async () => {
const shortTTLCache = new RedirectCache(50, 10000); // 50ms TTL
shortTTLCache.set('device1', { host: 'node1', port: 6667 });
await new Promise((resolve) => setTimeout(resolve, 100));
expect(shortTTLCache.get('device1')).toBeNull();
});
});
describe('LRU Eviction', () => {
test('should evict oldest entry when cache is full', () => {
const smallCache = new RedirectCache(300000, 3); // Max 3 entries
smallCache.set('device1', { host: 'node1', port: 6667 });
smallCache.set('device2', { host: 'node2', port: 6668 });
smallCache.set('device3', { host: 'node3', port: 6669 });
let stats = smallCache.getStats();
expect(stats.size).toBe(3);
// Add 4th entry, should evict device1 (oldest)
smallCache.set('device4', { host: 'node4', port: 6670 });
expect(smallCache.get('device1')).toBeNull(); // Evicted
expect(smallCache.get('device2')).not.toBeNull();
expect(smallCache.get('device3')).not.toBeNull();
expect(smallCache.get('device4')).not.toBeNull();
// Get fresh stats after eviction
stats = smallCache.getStats();
expect(stats.size).toBe(3); // Still at max size
});
test('should handle multiple evictions', () => {
const smallCache = new RedirectCache(300000, 2); // Max 2 entries
smallCache.set('device1', { host: 'node1', port: 6667 });
smallCache.set('device2', { host: 'node2', port: 6668 });
smallCache.set('device3', { host: 'node3', port: 6669 });
smallCache.set('device4', { host: 'node4', port: 6670 });
// device1 and device2 should be evicted
expect(smallCache.get('device1')).toBeNull();
expect(smallCache.get('device2')).toBeNull();
expect(smallCache.get('device3')).not.toBeNull();
expect(smallCache.get('device4')).not.toBeNull();
});
test('should maintain LRU order when updating existing entries', () => {
const smallCache = new RedirectCache(300000, 3); // Max 3 entries
// Add 3 entries
smallCache.set('device1', { host: 'node1', port: 6667 });
smallCache.set('device2', { host: 'node2', port: 6668 });
smallCache.set('device3', { host: 'node3', port: 6669 });
// Update device1 (should move it to end of LRU)
smallCache.set('device1', { host: 'node1-updated', port: 6670 });
// Add device4, should evict device2 (now oldest), not device1
smallCache.set('device4', { host: 'node4', port: 6671 });
expect(smallCache.get('device1')).not.toBeNull(); // Still there (recently updated)
expect(smallCache.get('device2')).toBeNull(); // Evicted (oldest)
expect(smallCache.get('device3')).not.toBeNull(); // Still there
expect(smallCache.get('device4')).not.toBeNull(); // Newly added
// Verify device1 has updated endpoint
const device1Endpoint = smallCache.get('device1');
expect(device1Endpoint?.port).toBe(6670);
});
});
describe('Statistics', () => {
test('should return correct cache stats', () => {
cache.set('device1', { host: 'node1', port: 6667 });
cache.set('device2', { host: 'node2', port: 6668 });
const stats = cache.getStats();
expect(stats.size).toBe(2);
expect(stats.maxSize).toBe(10000);
expect(stats.ttl).toBe(300000);
});
test('should update stats after operations', () => {
cache.set('device1', { host: 'node1', port: 6667 });
cache.set('device2', { host: 'node2', port: 6668 });
cache.set('device3', { host: 'node3', port: 6669 });
let stats = cache.getStats();
expect(stats.size).toBe(3);
cache.remove('device2');
stats = cache.getStats();
expect(stats.size).toBe(2);
cache.clear();
stats = cache.getStats();
expect(stats.size).toBe(0);
});
});
describe('Edge Cases', () => {
test('should handle removing non-existent device', () => {
cache.remove('non-existent');
// Should not throw error
expect(cache.get('non-existent')).toBeNull();
});
test('should handle clearing empty cache', () => {
cache.clear();
// Should not throw error
const stats = cache.getStats();
expect(stats.size).toBe(0);
});
test('should handle same host:port for different devices', () => {
const endpoint: EndPoint = { host: 'node1', port: 6667 };
cache.set('device1', endpoint);
cache.set('device2', endpoint); // Same endpoint, different device
expect(cache.get('device1')).toEqual(endpoint);
expect(cache.get('device2')).toEqual(endpoint);
const stats = cache.getStats();
expect(stats.size).toBe(2); // Two separate entries
});
test('should handle special characters in device ID', () => {
const deviceId = 'root.sg.d1.s_temp-01';
const endpoint: EndPoint = { host: 'node1', port: 6667 };
cache.set(deviceId, endpoint);
const retrieved = cache.get(deviceId);
expect(retrieved).toEqual(endpoint);
});
test('should handle very long device IDs', () => {
const longDeviceId = 'root.' + 'sg.'.repeat(100) + 'device';
const endpoint: EndPoint = { host: 'node1', port: 6667 };
cache.set(longDeviceId, endpoint);
const retrieved = cache.get(longDeviceId);
expect(retrieved).toEqual(endpoint);
});
test('should handle maxSize=1', () => {
const tinyCache = new RedirectCache(300000, 1);
tinyCache.set('device1', { host: 'node1', port: 6667 });
expect(tinyCache.get('device1')).not.toBeNull();
tinyCache.set('device2', { host: 'node2', port: 6668 });
expect(tinyCache.get('device1')).toBeNull(); // Evicted
expect(tinyCache.get('device2')).not.toBeNull();
});
});
describe('Concurrent-like Operations', () => {
test('should handle rapid successive sets', () => {
for (let i = 0; i < 100; i++) {
cache.set(`device${i}`, { host: `node${i}`, port: 6667 + i });
}
// All should be retrievable (within maxSize limit)
for (let i = 0; i < 100; i++) {
const retrieved = cache.get(`device${i}`);
if (i < 90) {
// First 90 might be evicted (maxSize is 10000, so all should be there)
expect(retrieved).not.toBeNull();
}
}
});
test('should handle rapid successive gets', () => {
cache.set('device1', { host: 'node1', port: 6667 });
for (let i = 0; i < 100; i++) {
const retrieved = cache.get('device1');
expect(retrieved).toEqual({ host: 'node1', port: 6667 });
}
});
test('should handle mixed operations', () => {
// Mix of set, get, remove operations
cache.set('device1', { host: 'node1', port: 6667 });
expect(cache.get('device1')).not.toBeNull();
cache.set('device2', { host: 'node2', port: 6668 });
cache.remove('device1');
expect(cache.get('device1')).toBeNull();
cache.set('device3', { host: 'node3', port: 6669 });
expect(cache.get('device2')).not.toBeNull();
expect(cache.get('device3')).not.toBeNull();
});
});
});