Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

[TEX] Part 3: TrackingKeyValueService: tracking iterator utils #6336

Merged
merged 10 commits into from
Nov 9, 2022

Conversation

ergo14
Copy link
Contributor

@ergo14 ergo14 commented Oct 30, 2022

General

Before this PR:

After this PR:
Adding some wrapper iterator classes for data tracking.

==COMMIT_MSG==
==COMMIT_MSG==

Priority:

Concerns / possible downsides (what feedback would you like?):

Is documentation needed?:

Compatibility

Does this PR create any API breaks (e.g. at the Java or HTTP layers) - if so, do we have compatibility?:

Does this PR change the persisted format of any data - if so, do we have forward and backward compatibility?:

The code in this PR may be part of a blue-green deploy. Can upgrades from previous versions safely coexist? (Consider restarts of blue or green nodes.):

Does this PR rely on statements being true about other products at a deployment - if so, do we have correct product dependencies on these products (or other ways of verifying that these statements are true)?:

Does this PR need a schema migration?

Testing and Correctness

What, if any, assumptions are made about the current state of the world? If they change over time, how will we find out?:

What was existing testing like? What have you done to improve it?:

If this PR contains complex concurrent or asynchronous code, is it correct? The onus is on the PR writer to demonstrate this.:

If this PR involves acquiring locks or other shared resources, how do we ensure that these are always released?:

Execution

How would I tell this PR works in production? (Metrics, logs, etc.):

Has the safety of all log arguments been decided correctly?:

Will this change significantly affect our spending on metrics or logs?:

How would I tell that this PR does not work in production? (monitors, etc.):

If this PR does not work as expected, how do I fix that state? Would rollback be straightforward?:

If the above plan is more complex than “recall and rollback”, please tag the support PoC here (if it is the end of the week, tag both the current and next PoC):

Scale

Would this PR be expected to pose a risk at scale? Think of the shopping product at our largest stack.:

Would this PR be expected to perform a large number of database calls, and/or expensive database calls (e.g., row range scans, concurrent CAS)?:

Would this PR ever, with time and scale, become the wrong thing to do - and if so, how would we know that we need to do something differently?:

Development Process

Where should we start reviewing?:
Tracking

If this PR is in excess of 500 lines excluding versions lock-files, why does it not make sense to split it?:
N/A

@changelog-app
Copy link

changelog-app bot commented Oct 30, 2022

Generate changelog in changelog/@unreleased

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Transactional Expectations Part 1d1: TrackingKeyValueService: tracking iterator utils

Check the box to generate changelog(s)

  • Generate changelog entry

Copy link
Contributor

@Sam-Kramer Sam-Kramer left a comment

Choose a reason for hiding this comment

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

Just a couple of clarifications otherwise looks good!

import java.util.function.Function;

public class TrackingIterator<T, I extends Iterator<T>> extends ForwardingIterator<T> {
I delegate;
Copy link
Contributor

Choose a reason for hiding this comment

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

make these private variables :)

import java.util.function.Consumer;
import java.util.function.Function;

public class TrackingRowColumnRangeIterator extends TrackingIterator<Map.Entry<Cell, Value>, RowColumnRangeIterator>
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we think TrackingRowColumnRangeIterator will be extended or not? If not, it may make sense to make this class final.

verify(iterator, times(1)).close();
}

private static ClosableIterator<String> spawnClosableIterator() {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: createClosableIterator()

@Test
public void trackingClosableStringIteratorDelegatesClose() {
ClosableIterator<String> iterator = spy(spawnClosableIterator());
TrackingClosableIterator<String> trackingIterator = new TrackingClosableIterator<>(iterator, noOp(), MEASURER);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit (non-blocking): I think we should add one test here which actually validates that our tracker and measurer were passed to TrackingClosableIterator correctly. I'd just do this by creating a one-item iterator, iterate over it, and then check and see that tracker/measurer are called with the right things.


@Test
public void trackingStringIteratorTracksData() {
Iterator<String> iterator = spawnIterator();
Copy link
Contributor

Choose a reason for hiding this comment

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

this is unused!

public void trackingStringIteratorTracksData() {
Iterator<String> iterator = spawnIterator();

TrackingIterator<String, Iterator<String>> trackingIterator = new TrackingIterator<>(
Copy link
Contributor

@Sam-Kramer Sam-Kramer Nov 2, 2022

Choose a reason for hiding this comment

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

can do this instead:

        Iterator<String> iterator = spawnIterator();
        TrackingIterator<String, Iterator<String>> trackingIterator = new TrackingIterator<>(
                spawnIterator(), bytes -> assertThat(bytes).isEqualTo(MEASURER.apply(iterator.next())), MEASURER);
        trackingIterator.forEachRemaining(noOp());

Copy link
Contributor

Choose a reason for hiding this comment

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

Also be good to add a test that ensures that the tracker has been called (plus has the expected total amount)

return STRINGS.stream().iterator();
}

private static <T> Consumer<T> noOp() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can also make this static if you want

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 suppose you mean making this a static field instead of a static method - the reason I am having it this way is so it can be used as a consumer of any generic type. Is there any other way of doing that?


@Test
public void trackingStringIteratorForwardsData() {
Iterator<String> iterator = spawnIterator();
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 also test this as:

        Iterator<String> iterator = spawnIterator();
        TrackingIterator<String, Iterator<String>> trackingIterator =
                new TrackingIterator<>(spawnIterator(), noOp(), MEASURER);
        assertThatIterator(trackingIterator).toIterable().containsExactlyElementsOf(ImmutableSet.copyOf(iterator));

import java.util.function.ToLongFunction;
import org.junit.Test;

public final class TrackingClosableIteratorTest extends AbstractTrackingIteratorTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be good for the second reviewer, but in my experience I have typically avoided using abstract classes for tests except for integration/ete tests (a good example of this is the AbstractKeyValueService test, which has a strong argument).

Considering that all the methods are static and final anyways, I'd push for a utility class. It also makes the test a bit more easier to reason about.

Cell.create(new byte[1], new byte[1]),
Value.create(PtBytes.EMPTY_BYTE_ARRAY, Value.INVALID_VALUE_TIMESTAMP));

// wrapped for mocking
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: maybe clarify that this has to be an anonymous inner class rather than a lambda in order to spy.

new TrackingIterator<>(createStringIterator(), tracker, STRING_MEASURER);

trackingIterator.forEachRemaining(string -> {
inOrder.verify(tracker).accept(STRING_MEASURER.applyAsLong(string));
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 is fine, but wanted to flag as it's one of my design biases: I typically try to avoid verify unless I really have to (as it's a class out of our control, etc), what you could do here instead is simply have your track be ArrayList::add), and verify that the in-order list is equal to what you expect


package com.palantir.atlasdb.transaction.impl.expectations;

import static com.palantir.atlasdb.transaction.impl.expectations.TrackingIteratorTestUtils.STRING_MEASURER;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit -- outside of assert4j and mockito, static imports are frowned upon for readability


assertThatIterator(trackingIterator)
.toIterable()
.containsExactlyElementsOf(consumeIteratorIntoList(createStringIterator()));
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for consumeIteratorIntoList, you can just use ImmutableList.copyOf!

Copy link
Contributor

Choose a reason for hiding this comment

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

You can also just reference the underlying list the iterator is backed by -- imo both are acceptable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also we can just use assertThat which will delegate to assertThatIterator under the hood

trackingIterator.forEachRemaining(noOp());

assertThatIterable(consumed)
.containsExactlyElementsOf(consumeIteratorIntoList(createStringIterator()).stream()
Copy link
Contributor

Choose a reason for hiding this comment

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

Can just do StreamEx.of(createStringIterator()).mapToLong(STRING_MEASURER).boxed().toList() instead! IMO my preference is to separate this onto another line to make it more readable, but to be clear, this is a preference, so feel free to ignore :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried to separate it into one line but my intellij did not cooperate 😅

ClosableIterators.wrapWithEmptyClose(Iterator.of(STRING)), tracker, measurer);

assertThatIterator(trackingIterator).toIterable().containsExactlyElementsOf(ImmutableSet.of(STRING));
trackingIterator.forEachRemaining(noOp());
Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't be needed due to the above line!

Copy link
Contributor

@Sam-Kramer Sam-Kramer left a comment

Choose a reason for hiding this comment

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

LGTM, will wait for the other reviewer to plus one!

@ergo14 ergo14 changed the title [TEX] Part 1d1: TrackingKeyValueService: tracking iterator utils [TEX] Part 3: TrackingKeyValueService: tracking iterator utils Nov 4, 2022
Copy link
Contributor

@jeremyk-91 jeremyk-91 left a comment

Choose a reason for hiding this comment

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

Generally this looks reasonable. There are a bunch of slightly unidiomatic patterns I think around mocks and the tests - it looks like you've gone through a lot of effort to avoid duplication, but the coupling between the Utils class and the resulting tests isn't really something I'm a fan of. I've scheduled some time to talk through this.


public final class TrackingIteratorTestUtils {
public static final String STRING = "test";
// this has to be an anonymous inner class rather than lambda in order to spy
Copy link
Contributor

Choose a reason for hiding this comment

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

On one hand, this is a good comment!

However, I'm not sure I like this pattern as opposed to duplication, honestly. This creates a lot of coupling between the test utilities: now the utility class needs to know whether it's being spied or not, and/or if the tests no longer spy the function for some reason, how would we know that we need to also change this?

Also, while I realise this may be consistent with existing code, if this is strictly intended for use with the tracking iterators, it should be package visibility.

public final class TrackingIteratorTestUtils {
public static final String STRING = "test";
// this has to be an anonymous inner class rather than lambda in order to spy
public static final ToLongFunction<String> STRING_MEASURER = new ToLongFunction<>() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be named something different: how would I know that what's being measured is the length as opposed to something else?

import java.util.function.Consumer;
import java.util.function.ToLongFunction;

public class TrackingIterator<T, I extends Iterator<T>> extends ForwardingIterator<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense - might be good to explain what the tracker is supposed to do / give a high level overview of how this is supposed to work

assertThat(trackingIterator).toIterable().containsExactlyElementsOf(List.of(TrackingIteratorTestUtils.STRING));
verify(measurer, times(1)).applyAsLong(TrackingIteratorTestUtils.STRING);
verify(tracker, times(1))
.accept(TrackingIteratorTestUtils.STRING_MEASURER.applyAsLong(TrackingIteratorTestUtils.STRING));
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: times(1) is usually not required as that's default. I'd probably also call this test method delegatesToTrackerAndMeasurer rather than IsWiredCorrectly as well.

verify(tracker, times(1))
.accept(TrackingIteratorTestUtils.STRING_MEASURER.applyAsLong(TrackingIteratorTestUtils.STRING));
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(measurer);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this takes varargs IIRC

verify(tracker, times(1))
.accept(TrackingIteratorTestUtils.STRING_MEASURER.applyAsLong(TrackingIteratorTestUtils.STRING));
verifyNoMoreInteractions(tracker);
verifyNoMoreInteractions(measurer);
Copy link
Contributor

Choose a reason for hiding this comment

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

as above: I usually prefer describing behaviour as opposed to "is wired correctly", times(1) isn't needed, and vNMI takes varargs I think.


private static Value createValue(int size) {
Preconditions.checkArgument(size >= Long.BYTES, "size should be at least the number of bytes in one long");
return Value.create(new byte[size - Long.BYTES], Value.INVALID_VALUE_TIMESTAMP);
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 appreciate the defensiveness

Copy link
Contributor

@jeremyk-91 jeremyk-91 left a comment

Choose a reason for hiding this comment

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

👍 Thanks!

@@ -57,16 +60,15 @@ public long applyAsLong(Entry<Cell, Value> value) {

@Test
public void oneElementTrackingRowColumnRangeIteratorIsWiredCorrectly() {
Consumer<Long> tracker = spy(TrackingIteratorTestUtils.noOp());
Consumer<Long> tracker = spy(NO_OP);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit / non actionable: I don't normally like using constants if there is only one thing, though I'm fine either way

@bulldozer-bot bulldozer-bot bot merged commit 4b09509 into develop Nov 9, 2022
@bulldozer-bot bulldozer-bot bot deleted the tex-pr-1d1 branch November 9, 2022 10:58
@svc-autorelease
Copy link
Collaborator

Released 0.759.0

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

Successfully merging this pull request may close these issues.

5 participants