Avatar of the person chatting

implement service discovery using redis and typescript

To implement service discovery using Redis and TypeScript, you can follow these steps:

  1. Connect to a Redis instance: You can use a Redis client library for TypeScript to connect to the Redis instance.

  2. Register services: When a service starts, it can register itself by setting a key in Redis with its address and port information.

  3. Discover services: When a service needs to discover other services, it can query Redis for the keys that correspond to the services it is interested in. The values of those keys will contain the addresses and ports of the discovered services.

  4. Remove services: When a service stops, it can remove its key from Redis, indicating that it is no longer available.

Here's an example of how you might use the "ioredis" library to implement service discovery:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); // Register the service await redis.set('service-name', 'service-host:port'); // Discover the service const address = await redis.get('service-name'); console.log(address); // 'service-host:port' // Remove the service await redis.del('service-name');

This is just a basic example and you may need to modify it based on your specific use case and requirements.

Avatar of the person chatting

What if I want to register all instances of the same service?

If you want to register multiple instances of the same service, you can use Redis' set data structure to store multiple values for the same key. Here's an example using the "ioredis" library:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); // Register service instance 1 await redis.sadd('service-name', 'instance1-host:port'); // Register service instance 2 await redis.sadd('service-name', 'instance2-host:port'); // Discover all instances of the service const instances = await redis.smembers('service-name'); console.log(instances); // [ 'instance1-host:port', 'instance2-host:port' ] // Remove an instance of the service await redis.srem('service-name', 'instance1-host:port');

You can use Redis' set data structure to store multiple values for the same key, making it easy to discover all instances of a service. Just remember to use the appropriate set commands (e.g. sadd, smembers, srem) instead of string commands (e.g. set, get, del).

Avatar of the person chatting

Can you also add a heartbeat mechanism, so if one of the servers doesn't send heartbeat in a required timespan, it gets removed from the list

Yes, you can add a heartbeat mechanism to ensure that only active instances of a service are listed. Here's an example using the "ioredis" library:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const heartbeatInterval = 10; // in seconds async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.expire(instance, heartbeatInterval * 2); } async function heartbeat(instance: string) { await redis.expire(instance, heartbeatInterval * 2); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -1) { await redis.srem('service-name', instance); } } } setInterval(removeStaleServiceInstances, heartbeatInterval * 1000);

Each instance of the service will register itself with registerServiceInstance and send heartbeats with heartbeat. The removeStaleServiceInstances function will remove any instances that haven't sent a heartbeat in the past heartbeatInterval * 2 seconds. This function is run every heartbeatInterval seconds using setInterval.

Avatar of the person chatting

How can I keep up to date list of services in the code?

To keep an up-to-date list of services in your code, you can periodically query the Redis database for the current list of services. You can use the smembers command to retrieve all members of a set in Redis, which represents the current list of services.

Here's an example of how you might keep an up-to-date list of services in your code using the "ioredis" library:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds let serviceList: string[] = []; async function updateServiceList() { serviceList = await redis.smembers('service-name'); } setInterval(updateServiceList, updateInterval * 1000);

In this example, the updateServiceList function updates the serviceList with the current list of services stored in Redis. This function is run every updateInterval seconds using setInterval.

You can access the current list of services at any time by accessing the serviceList variable. Note that this list will not reflect changes to the Redis set immediately, but will be updated every updateInterval seconds.

Avatar of the person chatting

Is it possible to get notified when a new service is added or a stale service is removed?

Yes, it is possible to get notified when a new service is added or a stale service is removed. One way to do this is by using Redis' pub/sub feature.

Here's an example using the "ioredis" library:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.expire(instance, heartbeatInterval * 2); redis.publish('service-list-update', 'add'); } async function heartbeat(instance: string) { await redis.expire(instance, heartbeatInterval * 2); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -1) { await redis.srem('service-name', instance); redis.publish('service-list-update', 'remove'); } } } redis.subscribe('service-list-update', async (channel, message) => { switch (message) { case 'add': serviceList.add(instance); break; case 'remove': serviceList.delete(instance); break; } }); setInterval(removeStaleServiceInstances, heartbeatInterval * 1000);

In this example, the registerServiceInstance and removeStaleServiceInstances functions publish updates to the service-list-update channel when a service instance is added or removed, respectively. The code also subscribes to the service-list-update channel and updates the serviceList set accordingly.

This allows your code to be notified of changes to the service list in real-time, rather than having to periodically query Redis for updates.

Avatar of the person chatting

In the heartbeat example, I want registerServiceInstance method to send heartbeats

Here's an updated version of the code that includes the heartbeat mechanism in the registerServiceInstance method:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); redis.publish('service-list-update', 'add'); setInterval(() => { redis.expire(instance, heartbeatInterval * 2); }, heartbeatInterval * 1000); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -1) { await redis.srem('service-name', instance); redis.publish('service-list-update', 'remove'); } } } redis.subscribe('service-list-update', async (channel, message) => { switch (message) { case 'add': serviceList.add(instance); break; case 'remove': serviceList.delete(instance); break; } }); setInterval(removeStaleServiceInstances, heartbeatInterval * 1000);

In this version of the code, the registerServiceInstance method sets up a recurring interval to send heartbeats for the service instance, using the setInterval function. This interval updates the expiration time of the key for the service instance in Redis, using the expire command.

The removeStaleServiceInstances method continues to periodically check for and remove any stale service instances.

Avatar of the person chatting

I dont want to receive notifications anymore, but keep all other features you've implemented so far. Can you give me the final code pieces together

Here's the final code that includes all the features we've discussed, except for the notifications:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); setInterval(() => { redis.expire(instance, heartbeatInterval * 2); }, heartbeatInterval * 1000); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -1) { await redis.srem('service-name', instance); } } } setInterval(removeStaleServiceInstances, updateInterval * 1000);

In this code, the notifications have been removed and the interval for removing stale service instances has been changed to updateInterval seconds. The rest of the code remains the same, including the implementation of the registerServiceInstance method to send heartbeats and the removeStaleServiceInstances method to remove stale service instances.

Avatar of the person chatting

I also want to keep automatic update mechanism

Here's an updated version of the code that includes the automatic update mechanism:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); setInterval(() => { redis.expire(instance, heartbeatInterval * 2); }, heartbeatInterval * 1000); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -1) { await redis.srem('service-name', instance); } } } async function updateServiceList() { const instances = await redis.smembers('service-name'); serviceList.clear(); for (const instance of instances) { serviceList.add(instance); } } setInterval(removeStaleServiceInstances, updateInterval * 1000); setInterval(updateServiceList, updateInterval * 1000);

In this code, the updateServiceList method has been added to periodically update the serviceList set with the current list of service instances stored in Redis. The updateServiceList method clears the current serviceList and then adds the current list of instances from Redis to it. An interval for updating the service list has been set up using the setInterval function.

The rest of the code remains the same, including the implementation of the registerServiceInstance method to send heartbeats and the removeStaleServiceInstances method to remove stale service instances.

Avatar of the person chatting

The only problem is registerServiceInstance redis.expire method works on non existing key instance

Here's an updated version of the code that solves the issue with the redis.expire method:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); setInterval(() => { redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); }, heartbeatInterval * 1000); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -1) { await redis.srem('service-name', instance); } } } async function updateServiceList() { const instances = await redis.smembers('service-name'); serviceList.clear(); for (const instance of instances) { serviceList.add(instance); } } setInterval(removeStaleServiceInstances, updateInterval * 1000); setInterval(updateServiceList, updateInterval * 1000);

In this code, the registerServiceInstance method has been updated to set the value of the instance key to "alive" using the redis.set method, instead of using the redis.expire method. This will ensure that the key is created if it does not already exist. The interval for sending heartbeats has been set up using the setInterval function.

The rest of the code remains the same, including the removeStaleServiceInstances method to remove stale service instances and the updateServiceList method to update the serviceList set with the current list of service instances stored in Redis.

Avatar of the person chatting

One more thing, the redis.ttl command returns -2 which is not handled, can you solve it?

Sure! You can handle the case where redis.ttl returns -2 by simply skipping the instance in the removeStaleServiceInstances method. Here's the updated code:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); setInterval(() => { redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); }, heartbeatInterval * 1000); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -1) { await redis.srem('service-name', instance); } else if (ttl >= 0) { continue; } } } async function updateServiceList() { const instances = await redis.smembers('service-name'); serviceList.clear(); for (const instance of instances) { serviceList.add(instance); } } setInterval(removeStaleServiceInstances, updateInterval * 1000); setInterval(updateServiceList, updateInterval * 1000);

In this code, the removeStaleServiceInstances method has been updated to check if ttl is greater than or equal to 0, and if so, it skips the instance. This ensures that the service instance will not be removed if redis.ttl returns -2.

Avatar of the person chatting

No this code is wrong, it should delete the instance if ttl result is negative

Apologies for that mistake. Here's the updated code:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); setInterval(() => { redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); }, heartbeatInterval * 1000); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -2 || ttl === -1) { await redis.srem('service-name', instance); } } } async function updateServiceList() { const instances = await redis.smembers('service-name'); serviceList.clear(); for (const instance of instances) { serviceList.add(instance); } } setInterval(removeStaleServiceInstances, updateInterval * 1000); setInterval(updateServiceList, updateInterval * 1000);

In this code, the removeStaleServiceInstances method now checks if ttl is equal to -2 or -1 and deletes the service instance in either case.

Avatar of the person chatting

Finally, I also want to have a method for deregistering service instance

Here is the final code, including the deregistering functionality:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); let heartbeatIntervals: NodeJS.Timeout[] = []; async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); heartbeatIntervals.push( setInterval(() => { redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); }, heartbeatInterval * 1000) ); } async function deregisterServiceInstance(instance: string) { await redis.srem('service-name', instance); await redis.del(instance); const intervalIndex = heartbeatIntervals.findIndex( (interval) => interval.ref === instance ); clearInterval(heartbeatIntervals[intervalIndex]); heartbeatIntervals = heartbeatIntervals.filter( (interval, index) => index !== intervalIndex ); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -2 || ttl === -1) { await redis.srem('service-name', instance); } } } async function updateServiceList() { const instances = await redis.smembers('service-name'); serviceList.clear(); for (const instance of instances) { serviceList.add(instance); } } setInterval(removeStaleServiceInstances, updateInterval * 1000); setInterval(updateServiceList, updateInterval * 1000);

In this code, the deregisterServiceInstance method removes the given instance from the service-name set, deletes the instance's key, finds and clears the corresponding heartbeat interval, and updates the heartbeatIntervals array.

Avatar of the person chatting

The above code has an exception in line 24, can you fix it?

Here is the corrected code:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); let heartbeatIntervals: NodeJS.Timeout[] = []; async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); heartbeatIntervals.push( setInterval(() => { redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); }, heartbeatInterval * 1000) ); } async function deregisterServiceInstance(instance: string) { await redis.srem('service-name', instance); await redis.del(instance); const intervalIndex = heartbeatIntervals.findIndex((interval) => { return interval._idleTimeout === instance; }); clearInterval(heartbeatIntervals[intervalIndex]); heartbeatIntervals = heartbeatIntervals.filter( (interval, index) => index !== intervalIndex ); } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -2 || ttl === -1) { await redis.srem('service-name', instance); } } } async function updateServiceList() { const instances = await redis.smembers('service-name'); serviceList.clear(); for (const instance of instances) { serviceList.add(instance); } } setInterval(removeStaleServiceInstances, updateInterval * 1000); setInterval(updateServiceList, updateInterval * 1000);

In this code, the exception in line 24 has been fixed by using the _idleTimeout property to find the corresponding interval in the heartbeatIntervals array.

Avatar of the person chatting

This is wrong again, maybe you can try converting heartbeatIntervals array to an array of objects and use instance name as keys

Here is the corrected code:

javascript
import Redis from 'ioredis'; const redis = new Redis({ host: '127.0.0.1' }); const updateInterval = 5; // in seconds const heartbeatInterval = 10; // in seconds const serviceList: Set<string> = new Set(); const heartbeatIntervals: { [instance: string]: NodeJS.Timeout } = {}; async function registerServiceInstance(instance: string) { await redis.sadd('service-name', instance); await redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); heartbeatIntervals[instance] = setInterval(() => { redis.set(instance, 'alive', 'EX', heartbeatInterval * 2); }, heartbeatInterval * 1000); } async function deregisterServiceInstance(instance: string) { await redis.srem('service-name', instance); await redis.del(instance); clearInterval(heartbeatIntervals[instance]); delete heartbeatIntervals[instance]; } async function removeStaleServiceInstances() { const instances = await redis.smembers('service-name'); for (const instance of instances) { const ttl = await redis.ttl(instance); if (ttl === -2 || ttl === -1) { await redis.srem('service-name', instance); } } } async function updateServiceList() { const instances = await redis.smembers('service-name'); serviceList.clear(); for (const instance of instances) { serviceList.add(instance); } } setInterval(removeStaleServiceInstances, updateInterval * 1000); setInterval(updateServiceList, updateInterval * 1000);

In this code, the heartbeatIntervals array has been converted to an object with the instance name as keys. This allows for the correct interval to be cleared when deregistering a service instance.