Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"ReplyError: ERR unknown command evalsha, with args beginning with:" with redis sentinels #304

Open
AJackTi opened this issue Aug 28, 2024 · 1 comment

Comments

@AJackTi
Copy link

AJackTi commented Aug 28, 2024

My package's version:
redlock: 5.0.0-beta.2
ioredis: 5.4.1
redis server: 7.4.0
node: v22.6.0

My code:

const express = require("express");
const Redlock = require("redlock").default;
const { promisify } = require("util");
const Client = require("ioredis");
require("log-timestamp");

// Create and configure a webserver.
const app = express();
app.use(express.json());

const host = "192.168.1.211";
const redis1 = new Client({
  port: 26377,
  host,
});
const redis2 = new Client({
  port: 26378,
  host,
});
const redis3 = new Client({
  port: 26379,
  host,
});

const redlock = new Redlock([redis1, redis2, redis3], {
  driftFactor: 0.01,
  retryCount: 5,
  retryDelay: 200,
  retryJitter: 200,
});

redlock.on("clientError", function (err) {
  console.error("A redis error has occurred:", err);
});

// Adding a simple function to wait some time.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// Create and endpoint to lock a key value paire and set the value.
app.post("/lockAndSetValue", async (req, res) => {
  console.log("Request received!");
  if (req.body.key && req.body.value) {
    try {
      const resource = `lock:${req.body.key}:${Date.now()}`;
      console.log(`Resource: ${resource}`);
      const ttl = 2000;
      redlock.acquire([resource], ttl).then(async function (lock) {
        console.log("Lock acquired!");
        await redisSet(req.body.key, req.body.value);
        console.log(`SET key=${req.body.key} value=${req.body.value}`);
        console.log("Waiting some time...");
        await sleep(1000);
        console.log("Time finished, key unlocked!");
        return lock.unlock().catch(function (err) {
          console.error(err);
        });
      });

      // redlock.using([resource], ttl, async (signal) => {
      //   console.log(`Lock acquired for ${resource}`);
      //   try {
      //     console.log("Do something...");
      //   } catch (error) {
      //     console.error(error);
      //   } finally {
      //     console.log(`Lock released for ${resource}`);
      //   }
      // });

      console.log("Sending response!");
      res.send(resource);
    } catch (e) {
      res.json(e);
    }
  } else {
    res.status(400).json({ error: "Wrong input." });
  }
});

// Create an endpoint to set a key value pair.
app.post("/setValue", async (req, res) => {
  if (req.body.key && req.body.value) {
    try {
      await redisSet(req.body.key, req.body.value);
      console.log(`SET key=${req.body.key} value=${req.body.value}`);
      res.send();
    } catch (e) {
      res.json(e);
    }
  } else {
    res.status(400).json({ error: "Wrong input." });
  }
});

// Create an endpoint to get a key value pair.
app.get("/getValue/:key", async (req, res) => {
  if (!req.params.key) {
    return res.status(400).json({ error: "Wrong input." });
  }

  try {
    const value = await redisGet(req.params.key);
    console.log(`GET key=${req.params.key} value=${value}`);
    res.json(value);
  } catch (e) {
    res.json(e);
  }
});

// Start the webserver.
app.listen(3000, () => {
  console.log("Server is up on port 3000");
});

My docker-compose file to run redis sentinels:

services:
  redis-master:
    image: redis:6
    container_name: redis-master
    hostname: redis-master
    ports:
      - "6379:6379"
    volumes:
      - ./data/master:/data
    command:
      [
        "redis-server",
        "--appendonly",
        "yes",
        "--repl-diskless-load",
        "on-empty-db",
        "--replica-announce-ip",
        "${HOST_IP}",
        "--replica-announce-port",
        "6379",
        "--protected-mode",
        "no"
      ]
    networks:
      redis-net:
        ipv4_address: 172.21.0.3


  redis-slave-1:
    image: redis:6
    container_name: redis-slave-1
    hostname: redis-slave-1
    depends_on:
      - redis-master
    ports:
      - "6380:6379"
    volumes:
      - ./data/slave1:/data
    command:
      [
        "redis-server",
        "--appendonly",
        "yes",
        "--replicaof",
        "redis-master",
        "6379",
        "--repl-diskless-load",
        "on-empty-db",
        "--replica-announce-ip",
        "${HOST_IP}",
        "--replica-announce-port",
        "6380",
        "--protected-mode",
        "no"
      ]
    networks:
      redis-net:
        ipv4_address: 172.21.0.4


  redis-slave-2:
    image: redis:6
    container_name: redis-slave-2
    hostname: redis-slave-2
    depends_on:
      - redis-master
    ports:
      - "6381:6379"
    volumes:
      - ./data/slave2:/data
    command:
      [
        "redis-server",
        "--appendonly",
        "yes",
        "--replicaof",
        "redis-master",
        "6379",
        "--repl-diskless-load",
        "on-empty-db",
        "--replica-announce-ip",
        "${HOST_IP}",
        "--replica-announce-port",
        "6381",
        "--protected-mode",
        "no"
      ]
    networks:
      redis-net:
        ipv4_address: 172.21.0.5


  sentinel-1:
    image: redis:6
    container_name: sentinel-1
    hostname: sentinel-1
    depends_on:
      - redis-master
    ports:
      - "26379:26379"
    command: >
      sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
            echo "sentinel monitor mymaster ${HOST_IP} 6379 2" >> /etc/sentinel.conf &&
            echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
            echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
            echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
            echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
            redis-sentinel /etc/sentinel.conf'
    networks:
      redis-net:
        ipv4_address: 172.21.0.6

  sentinel-2:
    image: redis:6
    container_name: sentinel-2
    hostname: sentinel-2
    depends_on:
      - redis-master
    ports:
      - "26378:26379"
    command: >
      sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
            echo "sentinel monitor mymaster ${HOST_IP} 6379 2" >> /etc/sentinel.conf &&
            echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
            echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
            echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
            echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
            redis-sentinel /etc/sentinel.conf'
    networks:
      redis-net:
        ipv4_address: 172.21.0.7

  sentinel-3:
    image: redis:6
    container_name: sentinel-3
    hostname: sentinel-3
    depends_on:
      - redis-master
    ports:
      - "26377:26379"
    command: >
      sh -c 'echo "bind 0.0.0.0" > /etc/sentinel.conf &&
            echo "sentinel monitor mymaster ${HOST_IP} 6379 2" >> /etc/sentinel.conf &&
            echo "sentinel resolve-hostnames yes" >> /etc/sentinel.conf &&
            echo "sentinel down-after-milliseconds mymaster 10000" >> /etc/sentinel.conf &&
            echo "sentinel failover-timeout mymaster 10000" >> /etc/sentinel.conf &&
            echo "sentinel parallel-syncs mymaster 1" >> /etc/sentinel.conf &&
            redis-sentinel /etc/sentinel.conf'
    networks:
      redis-net:
        ipv4_address: 172.21.0.8

networks:
  redis-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.21.0.0/16

When I run curl:

curl --location 'localhost:3000/lockAndSetValue' \
--header 'Content-Type: application/json' \
--data '{
    "key": "name",
    "value": "Roberto"
}'

I got the error message:

/Users/ajackti/Downloads/Code/Nodejs/node-redis-example-5/webservice-ver2/node_modules/redlock/dist/cjs/index.js:296
                throw new ExecutionError("The operation was unable to achieve a quorum during its retry window.", attempts);
                      ^

ExecutionError: The operation was unable to achieve a quorum during its retry window.
    at Redlock._execute (/Users/ajackti/Downloads/Code/Nodejs/node-redis-example-5/webservice-ver2/node_modules/redlock/dist/cjs/index.js:296:23)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Redlock.acquire (/Users/ajackti/Downloads/Code/Nodejs/node-redis-example-5/webservice-ver2/node_modules/redlock/dist/cjs/index.js:213:34) {
  attempts: [
    Promise {
      {
        membershipSize: 3,
        quorumSize: 2,
        votesFor: Set(0) {},
        votesAgainst: Map(3) {
          Commander {
            options: [Object],
            scriptsSet: {},
            addedBuiltinSet: Set(0) {},
            status: 'ready',
            isCluster: false,
            reconnectTimeout: null,
            connectionEpoch: 1,
            retryAttempts: 0,
            manuallyClosing: false,
            _autoPipelines: Map(0) {},
            _runningAutoPipelines: Set(0) {},
            _events: [Object: null prototype] {},
            _eventsCount: 0,
            _maxListeners: undefined,
            commandQueue: [Denque],
            offlineQueue: [Denque],
            connector: [StandaloneConnector],
            condition: [Object],
            stream: [Socket],
            [Symbol(shapeMode)]: false,
            [Symbol(kCapture)]: false
          } => ReplyError: ERR unknown command `evalsha`, with args beginning with: `96da70f7716f27d278a5218544df37fd8b0a5e4c`, `1`, `{resource}lock:name:1724811013590`, `beb20712d5d04dafa8e4b62f2b51a6fc`, `2000`, 
              at parseError (/Users/ajackti/Downloads/Code/Nodejs/node-redis-example-5/webservice-ver2/node_modules/redis-parser/lib/parser.js:179:12)
              at parseType (/Users/ajackti/Downloads/Code/Nodejs/node-redis-example-5/webservice-ver2/node_modules/redis-parser/lib/parser.js:302:14) {
            command: [Object]
          },
          Commander {
            options: [Object],
            scriptsSet: {},
            addedBuiltinSet: Set(0) {},
            status: 'ready',
            isCluster: false,
            reconnectTimeout: null,
            connectionEpoch: 1,
            retryAttempts: 0,
            manuallyClosing: false,
            _autoPipelines: Map(0) {},
            _runningAutoPipelines: Set(0) {},
            _events: [Object: null prototype] {},
            _eventsCount: 0,
            _maxListeners: undefined,
            commandQueue: [Denque],
            offlineQueue: [Denque],
            connector: [StandaloneConnector],
            condition: [Object],
            stream: [Socket],
            [Symbol(shapeMode)]: false,
            [Symbol(kCapture)]: false
          }

Please help me. I spent several days trying to address this, and my task’s deadline is approaching. I really need your help.

@anteqkois
Copy link

@AJackTi
I have resolved this issue with these changes. The key is to overwrite release method and check if lock expired.

import Redlock from 'redlock'
import { redis } from '../storage/redis'

export const redlock = new Redlock([redis], {
  // The expected clock drift; for more details see:
  // http://redis.io/topics/distlock
  driftFactor: 0.01, // multiplied by lock ttl to determine drift time
  // The max number of times Redlock will attempt to lock a resource
  // before erroring.
  retryCount: 30,
  // the time in ms between attempts
  retryDelay: 2000, // time in ms
  // the max time in ms randomly added to retries
  // to improve performance under high contention
  // see https://www.awsarchitectureblog.com/2015/03/backoff.html
  retryJitter: 200, // time in ms
  // The minimum remaining time on a lock before an extension is automatically
  // attempted with the `using` API.
  automaticExtensionThreshold: 500, // time in ms
})

// Save the original acquire method
const originalAcquire = Redlock.prototype.acquire

// Override the release method for all lock instances
Redlock.prototype.acquire = async function (...args) {
  const duration = args[1] // this is a duration value

  // use the duration to create additional settings
  args[2] = {
    retryCount: Math.ceil((duration / 2_000) * 1.5),
    retryDelay: 2_000,
    ...args[2],
  }

  return originalAcquire.apply(this, args) // Call the original release method
}

// Save the original release method
const originalRelease = Redlock.prototype.release

// Override the release method for all lock instances
Redlock.prototype.release = async function (...args) {
  const now = new Date().getTime()

  if (args[0] && args[0].expiration > now) {
    // Check if the lock still exists
    return originalRelease.apply(this, args) // Call the original release method
  }

  return {
    attempts: [],
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants