hamok
Version:
Lightweight Distributed Object Storage on RAFT consensus algorithm
312 lines (241 loc) • 10.8 kB
Markdown
## User Manual
[Hamok](./index.md) / [HamokEmitter](./emitter.md) / [HamokMap](./map.md) / [HamokQueue](./queue.md) / [HamokRecord](./record.md) / HamokRemoteMap
## Table of Contents
- [Overview](#overview)
- [Configuration](#configuration)
- [API Reference](#api-reference)
- [Properties](#properties)
- [Events](#events)
- [Methods](#methods)
- [Examples](#examples)
- [FAQ](#faq)
## Overview
`HamokRemoteMap` is a class that provides a distributed storage solution, enabling key-value pair manipulation across multiple instances with event-driven notifications. The primary distinction between `HamokMap` and `HamokRemoteMap` lies in the underlying storage mechanism. `HamokRemoteMap` leverages a `RemoteMap` as its base for storing key-value pairs, which can reside in a remote location (e.g., Redis, a database, etc.). Hamok is then used solely to ensure operational consistency across distributed instances.
This design is ideal for scenarios where a large number of key-value pairs need to be managed by multiple instances. Instead of introducing a distributed locking mechanism, `HamokRemoteMap` utilizes the RAFT consensus algorithm to guarantee consistent operation execution, ensuring consistency and fault tolerance in distributed systems.
### Important Notes
- RemoteMap relies on leader peer, as the leader peer is the only one which can executes action on a remote map. Therefore all peer which potentially can be the leader must create this map.
## Configuration
```typescript
const config: HamokRemoteMapBuilderConfig<number, string> = {
/**
* The unique identifier for the map.
*/
mapId: "remoteMapId",
/**
* Optional. The timeout duration in milliseconds for requests.
*/
requestTimeoutInMs: 5000,
/**
* Optional. A codec for encoding and decoding keys in the map.
*
* DEFAULT: JSON codec
*/
keyCodec: {
encode: (key: number) => (new DataView(new ArrayBuffer(4))).setInt32(0, key)),
decode: (data: Uint8Array) => (new DataView(data)).getInt32(0),
},
/**
* Optional. A codec for encoding and decoding values in the map.
*
* DEFAULT: JSON Codec
*/
valueCodec: valueCodec?: {
encode: (value: V) => Buffer.from(JSON.stringify(value)),
decode: (data: Uint8Array) => JSON.parse(Buffer.from(data).toString()),
},
/**
* Optional. The maximum number of keys allowed in request or response messages.
*
* DEFAULT: 0 means Infinity
*/
maxOutboundMessageKeys: 1000,
/**
* Optional. The maximum number of values allowed in request or response messages.
*
* DEFAULT: 0 means Infinity
*/
maxOutboundMessageValues: 1000,
/**
* The remote map to be used to store the data.
*/
remoteMap: createMyRemoteMap(),
/**
* Flag indicate if the events should be emitted by the event emitter or not.
* It also reduces the communication overhead if not needed, as for emitting events
* the leader should send a message to all followers to emit an event.
* In such case when it's not necessary (like cache maintenance) it can be disabled.
*
* DEFAULT: false
*/
noEvents: false,
/**
* Optional. A function to determine equality between two values.
* Used for custom equality checking.
*/
equalValues: (a: V, b: V) => a === b,
};
const remoteMap = hamok.createRemoteMap<number, string>(config);
```
## API Reference
### Properties
- **`id: string`**: Returns the unique identifier of the storage.
- **`closed: boolean`**: Indicates whether the storage is closed.
### Events
`HamokRemoteMap` extends `EventEmitter` and emits the following events:
- **`insert(key: K, value: V)`**: Emitted when a new entry is inserted.
- **`update(key: K, oldValue: V, newValue: V)`**: Emitted when an entry is updated.
- **`remove(key: K, value: V)`**: Emitted when an entry is removed.
- **`clear()`**: Emitted when the storage is cleared.
- **`close()`**: Emitted when the storage is closed.
### Event Handling
You can listen to these events using the standard `EventEmitter` API:
```typescript
remoteMap.on("insert", (key, value) => {
console.log(`Inserted: ${key} = ${value}`);
});
remoteMap.on("update", (key, oldValue, newValue) => {
console.log(`Updated: ${key} from ${oldValue} to ${newValue}`);
});
remoteMap.on("remove", (key, value) => {
console.log(`Removed: ${key} = ${value}`);
});
```
### Methods
- **`close(): void`**
Closes the storage, disconnecting it from the network and releasing all resources.
- **`size(): Promise<number>`**
Returns the number of entries in the storage.
- **`isEmpty(): Promise<boolean>`**
Returns `true` if the storage is empty, `false` otherwise.
- **`keys(): AsyncIterableIterator<K>`**
Returns an iterator for the keys in the storage.
- **`clear(): Promise<void>`**
Clears all entries from the storage.
- **`get(key: K): Promise<V | undefined>`**
Retrieves the value associated with the specified key.
- **`getAll(keys: IterableIterator<K> | K[]): Promise<ReadonlyMap<K, V>>`**
Retrieves all values associated with the specified keys.
- **`set(key: K, value: V): Promise<V | undefined>`**
Sets a key-value pair in the storage. If the key already exists, the value is updated.
- **`setAll(entries: ReadonlyMap<K, V>): Promise<ReadonlyMap<K, V>>`**
Sets multiple key-value pairs in the storage.
- **`insert(key: K, value: V): Promise<V | undefined>`**
Inserts a key-value pair into the storage. If the key already exists, it will not be updated.
- **`insertAll(entries: ReadonlyMap<K, V> | [K, V][]): Promise<ReadonlyMap<K, V>>`**
Inserts multiple key-value pairs into the storage.
- **`delete(key: K): Promise<boolean>`**
Deletes a key-value pair from the storage by key.
- **`deleteAll(keys: ReadonlySet<K> | K[]): Promise<ReadonlySet<K>>`**
Deletes multiple key-value pairs from the storage by their keys.
- **`remove(key: K): Promise<boolean>`**
Removes a key-value pair from the storage by key.
- **`removeAll(keys: ReadonlySet<K> | K[]): Promise<ReadonlyMap<K, V>>`**
Removes multiple key-value pairs from the storage by their keys.
- **`updateIf(key: K, value: V, oldValue: V): Promise<boolean>`**
Updates a key-value pair in the storage if the current value matches the specified `oldValue`.
- **`iterator(): AsyncIterableIterator<[K, V]>`**
Returns an iterator for the key-value pairs in the storage.
- **`sync(): Promise<void>`**
Synchronizes the storage with the remote peers. (waiting for the commitHead in hamok)
## Examples
- [use redis](https://github.com/balazskreith/hamok-ts/blob/main/examples/src/redis-remote-map-example.ts)
### Add Redis as RemoteMap
```typescript
import { RemoteMap } from "hamok/lib/collections/RemoteMap";
import Redis from "ioredis";
const publisher = new Redis();
const subscriber = new Redis();
type CachedItem = {
id: string;
value: string;
};
function createRemoteMap(mapId: string): RemoteMap<string, CachedItem> {
return {
async set(key, value, callback) {
const oldValue = await publisher.hget(mapId, key);
await publisher.hset(mapId, key, JSON.stringify(value));
callback?.(oldValue ? JSON.parse(oldValue) : undefined);
},
async setAll(entries, callback) {
const inserted: [string, CachedItem][] = [];
const updated: [string, CachedItem, CachedItem][] = [];
for (const [key, value] of entries) {
const oldValue = await publisher.hget(mapId, key);
if (oldValue) {
updated.push([key, JSON.parse(oldValue), value]);
} else {
inserted.push([key, value]);
}
await publisher.hset(mapId, key, JSON.stringify(value));
}
callback?.({ inserted, updated });
},
iterator() {
async function* asyncIterator() {
const keys = await publisher.hkeys(mapId);
for (const key of keys) {
const value = await publisher.hget(mapId, key);
yield [key, value ? JSON.parse(value) : undefined] as [
string,
CachedItem
];
}
}
return asyncIterator();
},
async get(key) {
const value = await publisher.hget(mapId, key);
return value ? JSON.parse(value) : undefined;
},
async keys() {
return (await publisher.hkeys(mapId)).values();
},
async getAll(keys) {
const iteratedKeys = [...keys];
const values = await Promise.all(
iteratedKeys.map((key) => publisher.hget(mapId, key))
);
const entries = iteratedKeys
.map((key, index) => [
key,
values[index] ? JSON.parse(values[index]) : undefined,
])
.filter(([, value]) => value !== undefined);
return new Map(entries as [string, CachedItem][]);
},
async remove(key) {
const value = await publisher.hget(mapId, key);
await publisher.hdel(mapId, key);
return value ? JSON.parse(value) : undefined;
},
async removeAll(keys) {
const iteratedKeys = [...keys];
const values = await Promise.all(
iteratedKeys.map((key) => publisher.hget(mapId, key))
);
const entries = iteratedKeys
.map((key, index) => [
key,
values[index] ? JSON.parse(values[index]) : undefined,
])
.filter(([, value]) => value !== undefined);
await publisher.hdel(mapId, ...iteratedKeys);
return new Map(entries as [string, CachedItem][]);
},
async clear() {
return publisher.del(mapId).then(() => void 0);
},
async size() {
return publisher.hlen(mapId);
},
};
}
```
Note that the serialization and deserialization methods for keys and values differ between Hamok instances and between an instance and a `RemoteMap`. This is because Hamok requires binary serialization and deserialization for its messages, while the requirements for `RemoteMap` are unknown and left to the developer's discretion. In the example above, we used simple JSON serialization and deserialization for keys and values.
## FAQ
### **How does `HamokRemoteMap` ensure data consistency?**
`HamokRemoteMap` uses Hamok which uses the Raft consensus algorithm to manage and replicate logs across distributed nodes, ensuring that all nodes agree on the operation order of the key-value store has to execute.
### **Can I use custom functions to compare values in `HamokRemoteMap`?**
Yes, you can provide a custom equality function when initializing `HamokRemoteMap` to define how values should be compared.
### **What happens when the storage is closed?**
When `HamokRemoteMap` is closed, it disconnects from the network, releases resources, and stops accepting or processing any new operations. All listeners are removed, and the `close` event is emitted.