Skip to content

perf: use hscan and scan for retained messages #119

@robertsLando

Description

@robertsLando

actually we are using keys method to retrieve retained messages from redis. This is generally a bad approach as stated here.

I also think we could someway improving scan to do some pattern matching (when possible).

Example:

async getRetainedMessages(patterns) {
    const qOpts = qlobberOptions;

    const packets = [];

    const wildcardPatterns = [];

    const qlobber = new QlobberTrue(qlobberOptions);

    for (const p of patterns) {
      if (!p.includes(qOpts.wildcard_some) || !p.includes(qOpts.wildcard_one)) {
        const packet = await this.getRetainedMessage(p);
        if (packet) {
          packets.push(packet);
        }
      } else {
        qlobber.add(p);
        wildcardPatterns.push(p);
      }
    }

    patterns = wildcardPatterns;

    if (patterns.length === 0) return packets;

    // use hscan to iterate over all RETAINEDKEY keys
    let cursor = '0';

    const patternsKey = this.getPatternsKey(patterns);

    do {
      // in NON-CLUSTER mode we store using `HSET` so we have to use `HSCAN` to iterate over the keys
      const [newCursor, keys] = this.isCluster
        ? await this.redisClient.scanBuffer(cursor, 'MATCH', patternsKey)
        : await this.redisClient.hscanBuffer(
            RETAINEDKEY,
            cursor,
            'MATCH',
            patternsKey
          );

      cursor = newCursor.toString();

      for (let i = 0; i < keys.length; i += 2) {
        const topic = this.isCluster
          ? decodeURIComponent(keys[i].toString().split(':')[1])
          : keys[i];
        if (qlobber.test(topic.toString())) {
          const p = this.decodeRetainedPacket(keys[i + 1]);
          if (p) {
            packets.push(p);
          }
        }
      }
    } while (cursor !== '0');

    return packets;
  }

/**
   * Returns an optimized redis key to look for when matching provided patterns
   * prevents to iterate over ALL retained keys when looking for a pattern
   */
  getPatternsKey(patterns): string {
    let topic = '';

    let stop = false;
    let charIndex = 0;

    while (!stop) {
      const char = patterns[0][charIndex];
      // ensure all patterns have the same char at the same index
      // stop if pattern ends or char is different or is a wildcard
      for (const pattern of patterns) {
        if (
          pattern.length <= charIndex ||
          pattern[charIndex] !== char ||
          [
            qlobberOptions.wildcard_one,
            qlobberOptions.wildcard_some,
          ].includes(char)
        ) {
          stop = true;
          break;
        }
      }

      if (!stop) {
        topic += char;
        charIndex++;
      }
    }

    return (this.isCluster ? this.retainedKey(topic) : topic) + '*';
  }

@seriousme thoughts on this?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions