@joystick.js/db-canary
Version:
JoystickDB - A minimalist database server for the Joystick framework
256 lines (197 loc) • 7.33 kB
JavaScript
import test from 'ava';
import { get_write_queue, shutdown_write_queue } from '../../../src/server/lib/write_queue.js';
test.beforeEach(async (t) => {
await shutdown_write_queue();
t.context.write_queue = get_write_queue();
});
test.afterEach(async (t) => {
await shutdown_write_queue();
});
test('write queue should process operations sequentially', async (t) => {
const write_queue = t.context.write_queue;
const execution_order = [];
const create_operation = (id, delay = 10) => {
return async () => {
await new Promise(resolve => setTimeout(resolve, delay));
execution_order.push(id);
return { id };
};
};
const promises = [
write_queue.enqueue_write_operation(create_operation('op1'), { test: 'op1' }),
write_queue.enqueue_write_operation(create_operation('op2'), { test: 'op2' }),
write_queue.enqueue_write_operation(create_operation('op3'), { test: 'op3' })
];
const results = await Promise.all(promises);
t.deepEqual(execution_order, ['op1', 'op2', 'op3']);
t.is(results.length, 3);
t.is(results[0].id, 'op1');
t.is(results[1].id, 'op2');
t.is(results[2].id, 'op3');
});
test('write queue should handle operation failures', async (t) => {
const write_queue = t.context.write_queue;
const failing_operation = async () => {
throw new Error('Operation failed');
};
const successful_operation = async () => {
return { success: true };
};
const error = await t.throwsAsync(
write_queue.enqueue_write_operation(failing_operation, { test: 'failing' })
);
t.is(error.message, 'Operation failed');
const result = await write_queue.enqueue_write_operation(
successful_operation,
{ test: 'successful' }
);
t.deepEqual(result, { success: true });
});
test('write queue should retry retryable errors', async (t) => {
const write_queue = t.context.write_queue;
let attempt_count = 0;
const retryable_operation = async () => {
attempt_count++;
if (attempt_count < 3) {
const error = new Error('MDB_MAP_FULL: Database map is full');
throw error;
}
return { success: true, attempts: attempt_count };
};
const result = await write_queue.enqueue_write_operation(
retryable_operation,
{ test: 'retryable' }
);
t.is(attempt_count, 3);
t.deepEqual(result, { success: true, attempts: 3 });
});
test('write queue should not retry non-retryable errors', async (t) => {
const write_queue = t.context.write_queue;
let attempt_count = 0;
const non_retryable_operation = async () => {
attempt_count++;
throw new Error('Invalid document format');
};
const error = await t.throwsAsync(
write_queue.enqueue_write_operation(non_retryable_operation, { test: 'non_retryable' })
);
t.is(attempt_count, 1);
t.is(error.message, 'Invalid document format');
});
test('write queue should track statistics correctly', async (t) => {
const write_queue = t.context.write_queue;
const successful_operation = async () => {
await new Promise(resolve => setTimeout(resolve, 50));
return { success: true };
};
const failing_operation = async () => {
throw new Error('Operation failed');
};
await write_queue.enqueue_write_operation(successful_operation, { test: 'success1' });
await write_queue.enqueue_write_operation(successful_operation, { test: 'success2' });
await t.throwsAsync(
write_queue.enqueue_write_operation(failing_operation, { test: 'failure' })
);
const stats = write_queue.get_stats();
t.is(stats.total_operations, 3);
t.is(stats.completed_operations, 2);
t.is(stats.failed_operations, 1);
t.is(stats.current_queue_depth, 0);
t.true(stats.avg_wait_time_ms >= 0);
t.true(stats.avg_processing_time_ms >= 0);
t.is(stats.success_rate, 67);
});
test('write queue should handle concurrent enqueuing', async (t) => {
const write_queue = t.context.write_queue;
const execution_order = [];
const create_operation = (id) => {
return async () => {
execution_order.push(id);
return { id };
};
};
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(
write_queue.enqueue_write_operation(
create_operation(`op${i}`),
{ test: `op${i}` }
)
);
}
const results = await Promise.all(promises);
t.is(results.length, 10);
t.is(execution_order.length, 10);
for (let i = 0; i < 10; i++) {
t.is(execution_order[i], `op${i}`);
t.is(results[i].id, `op${i}`);
}
});
test('write queue should clear statistics', async (t) => {
const write_queue = t.context.write_queue;
const operation = async () => ({ success: true });
await write_queue.enqueue_write_operation(operation, { test: 'clear_stats' });
let stats = write_queue.get_stats();
t.is(stats.total_operations, 1);
t.is(stats.completed_operations, 1);
write_queue.clear_stats();
stats = write_queue.get_stats();
t.is(stats.total_operations, 0);
t.is(stats.completed_operations, 0);
t.is(stats.failed_operations, 0);
});
test('write queue should handle shutdown gracefully', async (t) => {
const write_queue = t.context.write_queue;
const quick_operation = async () => {
return { success: true };
};
const promise1 = write_queue.enqueue_write_operation(quick_operation, { test: 'shutdown1' });
const result1 = await promise1;
t.deepEqual(result1, { success: true });
write_queue.shutdown();
const promise2 = write_queue.enqueue_write_operation(quick_operation, { test: 'shutdown2' });
const error = await t.throwsAsync(promise2);
t.is(error.message, 'Server shutting down');
});
test('write queue should calculate backoff delay correctly', async (t) => {
const write_queue = t.context.write_queue;
const delay1 = write_queue.calculate_backoff_delay(1);
const delay2 = write_queue.calculate_backoff_delay(2);
const delay3 = write_queue.calculate_backoff_delay(3);
t.true(delay1 >= 100 && delay1 <= 200);
t.true(delay2 >= 200 && delay2 <= 400);
t.true(delay3 >= 400 && delay3 <= 800);
const delay_max = write_queue.calculate_backoff_delay(10);
t.true(delay_max <= 5000);
});
test('write queue should identify retryable errors correctly', async (t) => {
const write_queue = t.context.write_queue;
const retryable_errors = [
new Error('MDB_MAP_FULL: Database map is full'),
new Error('MDB_TXN_FULL: Transaction is full'),
new Error('MDB_READERS_FULL: Too many readers'),
{ code: 'EAGAIN', message: 'Resource temporarily unavailable' },
{ code: 'EBUSY', message: 'Resource busy' }
];
const non_retryable_errors = [
new Error('Invalid document format'),
new Error('Collection not found'),
{ code: 'ENOENT', message: 'File not found' }
];
for (const error of retryable_errors) {
t.true(write_queue.is_retryable_error(error));
}
for (const error of non_retryable_errors) {
t.false(write_queue.is_retryable_error(error));
}
});
test('write queue should generate unique operation IDs', async (t) => {
const write_queue = t.context.write_queue;
const ids = new Set();
for (let i = 0; i < 100; i++) {
const id = write_queue.generate_operation_id();
t.false(ids.has(id));
ids.add(id);
}
t.is(ids.size, 100);
});