UNPKG

hamok

Version:

Lightweight Distributed Object Storage on RAFT consensus algorithm

128 lines (98 loc) 5.4 kB
import { Hamok, setHamokLogLevel } from 'hamok'; import * as pino from 'pino'; import { HamokMessageHub } from './utils/HamokMessageHub'; const logger = pino.pino({ name: 'map-insert-get-example', level: 'debug', }); export async function run() { const server_1 = new Hamok(); const server_2 = new Hamok(); const messageHub = new HamokMessageHub(); const storage_1 = server_1.createMap<string, number>({ mapId: 'my-replicated-storage', }); const storage_2 = server_2.createMap<string, number>({ mapId: 'my-replicated-storage', }); messageHub.add(server_1, server_2); await Promise.all([ server_1.join(), server_2.join(), ]); const value_1 = 1; const value_2 = 2; logger.debug(`Inserting values into replicated storage. Candidates to insert from server1: ${value_1}, server2: ${value_2}`); const [ reply_1, reply_2 ] = await Promise.all([ storage_1.insert('key', value_1), storage_2.insert('key', value_2), ]).catch(err => { logger.error('Error inserting values into replicated storage: %s', `${err}`); throw err; }); // the insert works in a way that the first server that insert the value got undefined as response // and the second server that try to insert the value got the value that was already inserted const succeededServer = reply_1 ? 'Server_2' : 'Server_1'; const insertedValue = reply_1 ?? reply_2; logger.info(`${succeededServer} inserted value ${insertedValue}`); // we can di in batches logger.info('Inserting values in batch. Server_1 tries to insert "key-1" and "key-2", Server_2 tries to insert "key-2" and "key-3"'); const [existing_1, existing_2 ] = await Promise.all([ storage_1.insertAll(new Map([['key-1', 1], ['key-2', 2]])), storage_2.insertAll(new Map([['key-2', 3], ['key-3', 4]])), ]); logger.info(`Server_1 inserted value for "key-1": ${!existing_1.get('key-1')}`); logger.info(`Server_1 inserted value for "key-2": ${!existing_1.get('key-2')}`); logger.info(`Server_2 inserted value for "key-2": ${!existing_2.get('key-2')}`); logger.info(`Server_2 inserted value for "key-3": ${!existing_2.get('key-3')}`); logger.info(`Server_1 get value for key: "key": ${storage_1.get('key')}`); logger.info(`Server_2 get value for key: "key": ${storage_2.get('key')}`); logger.info(`Server_1 get value for key: "key-1": ${storage_1.get('key-1')}`); logger.info(`Server_2 get value for key: "key-1": ${storage_2.get('key-1')}`); logger.info(`Server_1 get value for key: "key-2": ${storage_1.get('key-2')}`); logger.info(`Server_2 get value for key: "key-2": ${storage_2.get('key-2')}`); logger.info(`Server_1 get value for key: "key-3": ${storage_1.get('key-3')}`); logger.info(`Server_2 get value for key: "key-3": ${storage_2.get('key-3')}`); logger.info('Setting key-4 to 3 by Server 1'); await storage_1.set('key-4', 3); logger.info('Getting key-4 by server_1 %d', storage_1.get('key-4')); // it can happen that the leader server get the value faster than the follower. // the storage will not be inconsistent ever, becasue the RAFT logs are applied in the same order // in all the servers. // what we face here is the following situation: server_1 is the leader, and server_2 is the follower // hence storage_1 submits the value to the leader and the leader applies at once it got an acknoeldgement // from the majority of the followers, and then the leader commits the value up, and notify the followers // about the new commit index in the next heartbeat. // so in case you query the value in the follower before the leader heartbeat send the message about a new commit index // you will get the old value, but the value is already committed in the RAFT logs, and the follower // will apply the value in order. // if you apply a new value in server_2 it blocks the thread until the value is not set, // consequently the previous commit will also be applied. await storage_2.set('meaningless', 0); // alternatively you can wait until the follower get the new commit index // await server_2.waitUntilCommitHead(); // or you can just wait one heartbeat // await new Promise(resolve => setTimeout(resolve, server_2.raft.config.heartbeatInMs)); logger.info('Getting key-4 by server_2 %d', storage_2.get('key-4')); // we want to use the follower storage, becasue the leader storage get the // faster than any of the follower, so it can happen that the replicated storage // at the follower have different value, but it's not becasue it is inconsistent, // as the RAFT logs appears and the same operations are executed exactly in the same order const storage = server_1.leader ? storage_2 : storage_1; const updatedValue = Math.random(); logger.debug(`Updating value in replicated storage. We want to update the value to : ${updatedValue}`); await storage.set('key', updatedValue); logger.debug(`After updated getting value from server1: ${storage_1.get('key')}`); logger.debug(`After updated getting value from server2: ${storage_2.get('key')}`); logger.debug(`Deleting value from replicated storage`); await storage.delete('key'); logger.debug(`After deleted getting value from server1: ${storage_1.get('key')}`); logger.debug(`After deleted getting value from server2: ${storage_2.get('key')}`); server_1.close(); server_2.close(); } if (require.main === module) { logger.info('Running from module file'); setHamokLogLevel('info'); run(); }