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

CASSANDRA-19428 #3194

Conversation

ekaterinadimitrova2
Copy link
Contributor

Thanks for sending a pull request! Here are some tips if you're new here:

  • Ensure you have added or run the appropriate tests for your PR.
  • Be sure to keep the PR description updated to reflect all changes.
  • Write your PR title to summarize what this PR proposes.
  • If possible, provide a concise example to reproduce the issue for a faster review.
  • Read our contributor guidelines
  • If you're making a documentation change, see our guide to documentation contribution

Commit messages should follow the following format:

<One sentence description, usually Jira title or CHANGES.txt summary>

<Optional lengthier description (context on patch)>

patch by <Authors>; reviewed by <Reviewers> for CASSANDRA-#####

Co-authored-by: Name1 <email1>
Co-authored-by: Name2 <email2>

The Cassandra Jira

* replace KeyRangeConcatIterator's PriorityQeueu with List
* remove KeyRangeIterator.current and simplify
* remove injected exception and tests - not relevant to the current implementation
* expand randomized testing
* inline getCurrent() -> peek(); rename getCount to getMaxKeys
* redefine skipTo contract to not return a value (which saves unnecessary work when skipTo is called multiple times in a row)
* calling hasNext in skipTo is a pessimization; if the iterator is in DONE state, then skipTo will see it and avoid further effort; if it is not, then we are computing a next value that we're just going to throw away
* Fix a bug in QueryController#getIndexResults error handling
@ekaterinadimitrova2
Copy link
Contributor Author

Suggestion to change SingleNodeQueryFailureTest (passes locally for me)

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.cassandra.index.sai.disk;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.datastax.driver.core.exceptions.ReadFailureException;
import org.apache.cassandra.index.sai.SAITester;
import org.apache.cassandra.index.sai.disk.v1.postings.PostingListRangeIterator;
import org.apache.cassandra.index.sai.disk.v1.segment.LiteralIndexSegmentTermsReader;
import org.apache.cassandra.inject.Injection;
import org.apache.cassandra.inject.Injections;
import org.apache.cassandra.utils.Throwables;

import static org.apache.cassandra.inject.ActionBuilder.newActionBuilder;
import static org.apache.cassandra.inject.Expression.quote;
import static org.apache.cassandra.inject.InvokePointBuilder.newInvokePoint;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class SingleNodeQueryFailureTest extends SAITester
{
    private static final String CREATE_TABLE_TEMPLATE = "CREATE TABLE %s (id text PRIMARY KEY, v1 int, v2 text) WITH " +
            "compaction = {'class' : 'SizeTieredCompactionStrategy', 'enabled' : false }";

    @Before
    public void setup()
    {
        requireNetwork();
    }

    @After
    public void teardown()
    {
        Injections.deleteAll();
    }

    @Test
    public void testFailedRangeIteratorOnMultiIndexesQuery() throws Throwable
    {
        testFailedMultiIndexesQuery("range_iterator", PostingListRangeIterator.class, "getNextRowId");
    }

    @Test
    public void testFailedTermsReaderOnMultiIndexesQuery() throws Throwable
    {
        testFailedMultiIndexesQuery("terms_reader", LiteralIndexSegmentTermsReader.TermQuery.class, "lookupPostingsOffset");
    }

    private void testFailedMultiIndexesQuery(String name, Class<?> targetClass, String targetMethod) throws Throwable
    {
        Injection injection = Injections.newCustom(name)
                .add(newInvokePoint().onClass(targetClass).onMethod(targetMethod))
                .add(newActionBuilder().actions().doThrow(RuntimeException.class, quote("Injected failure!")))
                .build();

        createTable(CREATE_TABLE_TEMPLATE);
        createIndex(String.format(CREATE_INDEX_TEMPLATE, "v1"));
        createIndex(String.format(CREATE_INDEX_TEMPLATE, "v2"));

        execute("INSERT INTO %s (id, v1, v2) VALUES ('1', 0, '0')");
        flush();
        execute("INSERT INTO %s (id, v1, v2) VALUES ('2', 1, '1')");
        flush();
        execute("INSERT INTO %s (id, v1, v2) VALUES ('3', 2, '2')");
        flush();

        try
        {
            Injections.inject(injection);

            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 < 1 and v2 = '0'"))
                    .isInstanceOf(ReadFailureException.class);

            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 >= 1 and v2 = '1'"))
                    .isInstanceOf(ReadFailureException.class);

            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 >= 2 and v2 = '2'"))
                    .isInstanceOf(ReadFailureException.class);
        }
        catch (Exception e)
        {
            throw Throwables.unchecked(e);
        }
        finally
        {
            injection.disable();
        }

        Assert.assertEquals(3, executeNet("SELECT id FROM %s WHERE v1 >= 0").all().size());
        Assert.assertEquals(1, executeNet("SELECT id FROM %s WHERE v2 = '0'").all().size());
        Assert.assertEquals(1, executeNet("SELECT id FROM %s WHERE v2 = '1'").all().size());
        Assert.assertEquals(1, executeNet("SELECT id FROM %s WHERE v2 = '2'").all().size());
    }
}

{
return getRandom().nextDouble();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at these three methods, it seems like nextDouble() and randomDouble() produce the same result, since 0 + (1 - 0) * getRandom().nextDouble() == 1 * getRandom().nextDouble() == getRandom().nextDouble() right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Totally! Let's leave nextDouble in and remove randomDouble ?

// all sstable indexes in view have been referenced, need to clean up when exception is thrown
builder.cleanup();
queryView.referencedIndexes.forEach(SSTableIndex::releaseQuietly);
if (rangeCount == 0)
queryView.referencedIndexes.forEach(SSTableIndex::releaseQuietly);
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

throw new IllegalArgumentException("Cannot concatenate empty list of ranges");

this.ranges = ranges.iterator();
this.currentRange = this.ranges.next();
this.toRelease = new ArrayList<>(ranges);
Copy link
Contributor

Choose a reason for hiding this comment

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

After looking at this, I think it might be worth trying to use the original approach I proposed in CASSANDRA-18165. We should be able to avoid creating a new iterator, and while we're here, I'm not really sure why we defensive copy ranges for toRelease when we've already created a new list in the builder. (If we used my implementation, we could rename toRelease to just ranges I guess?)

I know getCurrent() is gone, so it can't be a direct port, but I think we can still do this without all the extra object creation...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@maedhroz
Copy link
Contributor

On SingleNodeQueryFailureTest, I think we agreed to test both single and multi-index query failures.

@@ -129,6 +129,16 @@ public static int between(int min, int max)
return getRandom().nextIntBetween(min, max - 1);
}

public static double nextDouble()
{
return randomDoubleBetween(0, 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think this could also just be getRandom().nextDouble()?

Copy link
Contributor Author

@ekaterinadimitrova2 ekaterinadimitrova2 Mar 29, 2024

Choose a reason for hiding this comment

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

randomDoubleBetween calls getRandom().nextDouble() under the hood. It seems more clear to me to use the randomDoubleBetween with the max and min explicitly stated. Though, now as you mentioned, I noticed that nextBoolean uses directly getRandom().nextBoolean(). I don't have strong preference for one or the other. I can change it

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, it's equivalent...just seemed less complicated to go directly to getRandom().nextDouble()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

this.ranges = ranges;
this.toRelease = new ArrayList<>(ranges);
this.current = 0;
this.ranges = new ArrayList<>(ranges);
Copy link
Contributor

@maedhroz maedhroz Mar 28, 2024

Choose a reason for hiding this comment

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

nit: Do we even need the defensive copy? ranges is private and never accessible by users of the iterator, and there is already an ArrayList created in the builder, which is also private, right? The only way I could imagine we get in trouble is if someone adds to the builds after calling buildIterator(), but that's definitely incorrect usage/we should never do it :)

Copy link
Contributor Author

@ekaterinadimitrova2 ekaterinadimitrova2 Mar 29, 2024

Choose a reason for hiding this comment

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

I guess I did not understand correctly this comment:

If we used my implementation, we could rename toRelease to just ranges I guess?

So you suggest it to be this.ranges = ranges? (I just checked that this is what we already do in other iterators too)

Copy link
Contributor

Choose a reason for hiding this comment

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

So you suggest it to be this.ranges = ranges?

Yeah, basically...the defensive copy just seems paranoid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

@maedhroz maedhroz left a comment

Choose a reason for hiding this comment

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

Left a couple tiny nits, but LGTM!

@ekaterinadimitrova2
Copy link
Contributor Author

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