Skip to content

Conversation

@ringles
Copy link
Contributor

@ringles ringles commented Jul 15, 2021

Addresses GEODE-9378 to implement the ZRANGEBYSCORE command for Geode Radish, the compatible-with-Redis API.

@ringles ringles added the redis Issues related to the geode-for-redis module label Jul 15, 2021
@ringles ringles requested a review from DonalEvans July 15, 2021 16:56
public List<byte[]> zrangebyscore(RedisKey key, SortedSetRangeOptions rangeOptions,
boolean withScores) {
return stripedExecute(key,
() -> getRedisSortedSet(key, true).zrangebyscore(rangeOptions, withScores));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed you are calling getRedisSortedSet with updatesStats set to true. This seems reasonable. But I also noticed that a bunch of other methods on this class (zadd, zincrby, zrange, zrem) call it with false. Do you know when to set it to true vs false?

Copy link
Contributor

@jdeppe-pivotal jdeppe-pivotal Jul 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that Redis' approach is to update the stats for commands that don't update data. In your sample list, zrange actually does call it with true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see ZRANGE calling it with true. In general, operations that read data update stats, operations that change data do not. So zadd, zrem, zincrby change data and therefore don't update stats.

List<byte[]> result = new ArrayList<>();
AbstractOrderedSetEntry minEntry =
new DummyOrderedSetEntry(rangeOptions.getMinDouble(), rangeOptions.isMinExclusive(), true);
long minIndex = scoreSet.indexOf(minEntry);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is minIndex and maxIndex typed as long? indexOf returns an int and later we cast it to an int. Seems like if you typed it as "int" you could get rid of the cast later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're typed as long because they're essentially the same code as in ZCOUNT. But it makes more sense for them to be int here, as you say.

count = Integer.MAX_VALUE;
}
} else {
throw new Exception();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make this a more specific exception? When the higher level catches Exception it could be catching all kinds of things (like NullPointerException) and reporting them as something else

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IllegalArgumentException seems to fit. Done.


// Count 1 <= score <= 1
assertThat(jedis.zrangeByScore(KEY, score, score))
.containsExactlyInAnyOrderElementsOf(map.keySet());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should also be using containsExactly since the order of the returned values should be lexicographically sorted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tweaked; unfortunately the map.keySet() isn't in the order we want, but it's fixable.


import org.apache.geode.redis.RedisIntegrationTest;

public abstract class AbstractZRangeByScoreIntegrationTest implements RedisIntegrationTest {
Copy link
Contributor

@jdeppe-pivotal jdeppe-pivotal Jul 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of boundary cases tested, but please would you also add a basic test for exclusivity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add tests to confirm the behaviour of ZRANGEBYSCORE with multiple instances of WITHSCORES and LIMIT, and with incorrectly formatted LIMIT arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope the new one is the kind of thing you have in mind...

Comment on lines 342 to 345
if (skip < offset) {
skip++;
continue;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could avoid use of skip here and just count down on offset:

if (offset > 0) {
  offset--;
  continue;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice; I think I can do that with 'count' as well!

}

@Test
public void shouldReturnCount_givenRangeIncludingScore() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test name should probably be "shouldReturnMember_givenRangeIncludingScore" or something similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, missed one.

public void shouldReturnZero_givenMinGreaterThanMax() {
jedis.zadd(KEY, 1, "member");

// Count +inf <= score <= -inf
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment (and others in this class) is a little misleading, as these tests aren't returning a count, but rather a range.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, thought I got all those. Quick fix.


// Count -inf < score <= +inf
assertThat(jedis.zrangeByScore(KEY, "(-inf", "+inf"))
.containsExactlyElementsOf(Arrays.asList("member2", "member3"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to .containsExactly("member2", "member3"). This also applies to other places in this test where Arrays.asList() and containsExactlyElementsOf() are used together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got a few of them earlier, all done now!

Comment on lines 218 to 219
ZREVRANGE(new ZRevRangeExecutor(), SUPPORTED, new MinimumParameterRequirements(4)
.and(new MaximumParameterRequirements(5, ERROR_SYNTAX))),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change should be reverted as ZREVRANGE should return a syntax error if more than 5 arguments are passed to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines 331 to 336
Iterator<AbstractOrderedSetEntry> entryIterator =
scoreSet.getIndexRange(minIndex, maxIndex, false);
if (rangeOptions.hasLimit()) {
count = rangeOptions.getCount();
offset = rangeOptions.getOffset();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than getting the range iterator and then skipping to the offset, might it be neater to add the offset to minIndex and then retrieve the range iterator? Similarly, I think you should be able to pass Math.min(count, maxIndex - minIndex) as the second argument of getIndexRange() so that you get an iterator that's automatically the right size. Then you can just iterate the entire length and return that with no further checks needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short answer: Yes.
Long answer: Yes, and we can eliminate another variable, too.


import org.apache.geode.redis.RedisIntegrationTest;

public abstract class AbstractZRangeByScoreIntegrationTest implements RedisIntegrationTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add tests to confirm the behaviour of ZRANGEBYSCORE with multiple instances of WITHSCORES and LIMIT, and with incorrectly formatted LIMIT arguments?

Comment on lines +95 to +96
offset = narrowLongToInt(bytesToLong(commandElements.get(commandIndex + 1)));
count = narrowLongToInt(bytesToLong(commandElements.get(commandIndex + 2)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With an improperly provided LIMIT argument (one or both of offset or count not supplied) this code will throw an ArrayIndexOutOfBoundsException which is not handled, so no syntax error will be returned to the client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests and fixes added for these.

public static final byte[] bRADISH_DUMP_HEADER = stringToBytes("RADISH");

@MakeImmutable
public static final byte[] bRADISH_WITHSCORES = stringToBytes("WITHSCORES");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a byte array constant for WITHSCORES further up in this class (line 180), so this one is not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed!

public static final byte[] bRADISH_WITHSCORES = stringToBytes("WITHSCORES");

@MakeImmutable
public static final byte[] bRADISH_LIMIT = stringToBytes("LIMIT");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be renamed "bLIMIT" to match the naming of other byte array constants used in executors, and moved further up in this class to be with the other executor-related constants?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also corrected!

@ringles ringles marked this pull request as ready for review July 19, 2021 19:09
Copy link
Contributor

@jdeppe-pivotal jdeppe-pivotal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this!

@ringles ringles merged commit 0726d5a into apache:develop Jul 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

redis Issues related to the geode-for-redis module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants