-
Notifications
You must be signed in to change notification settings - Fork 15
[TEX] Part 3: TrackingKeyValueService: tracking iterator utils #6336
Conversation
Generate changelog in
|
There was a problem hiding this 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; |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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<>( |
There was a problem hiding this comment.
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());
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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())); |
There was a problem hiding this comment.
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
!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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!
...st/java/com/palantir/atlasdb/transaction/impl/expectations/TrackingClosableIteratorTest.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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!
There was a problem hiding this 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 |
There was a problem hiding this comment.
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<>() { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
...st/java/com/palantir/atlasdb/transaction/impl/expectations/TrackingClosableIteratorTest.java
Outdated
Show resolved
Hide resolved
verify(tracker, times(1)) | ||
.accept(TrackingIteratorTestUtils.STRING_MEASURER.applyAsLong(TrackingIteratorTestUtils.STRING)); | ||
verifyNoMoreInteractions(tracker); | ||
verifyNoMoreInteractions(measurer); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 appreciate the defensiveness
There was a problem hiding this 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); |
There was a problem hiding this comment.
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
Released 0.759.0 |
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