Skip to content

KAFKA-7223: add tests in preparation for suppression#5687

Merged
guozhangwang merged 8 commits into
apache:trunkfrom
vvcephei:KAFKA-7223-in-mem-suppress-unit-tests
Sep 25, 2018
Merged

KAFKA-7223: add tests in preparation for suppression#5687
guozhangwang merged 8 commits into
apache:trunkfrom
vvcephei:KAFKA-7223-in-mem-suppress-unit-tests

Conversation

@vvcephei

@vvcephei vvcephei commented Sep 24, 2018

Copy link
Copy Markdown
Contributor

This is Part 2 of suppression.
Part 1 was #5567

In an effort to control the scope of the review, this PR is just the tests for buffered suppression.

Committer Checklist (excluded from commit message)

  • Verify design and implementation
  • Verify test coverage and CI build status
  • Verify documentation (including upgrade notes)

@vvcephei

Copy link
Copy Markdown
Contributor Author

@guozhangwang @bbejeck ,

When you have a chance please take a look at this PR.

As the description says, it contains only the unit tests that we will use for buffered suppressions.
Clearly, since it's not implemented yet, the tests can't all pass, so you'll notice that many have been configured to expect exceptions. This expectation will be removed when I add the implementation.

I'm hoping that this progression is more reviewable than one large chunk, and also that it makes sense in its resemblence to TDD. I'd like to think of this as writing/reviewing the tests first, so we agree on the expected behavior. With that agreement in place, we'll be able to concentrate on the implementation in a follow-on PR.

WDYT?


import static java.util.Objects.requireNonNull;

public class FullChangeSerde<T> implements Serde<Change<T>> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unlike the source/sink nodes, the suppress processor needs to be able to handle Changes in which both new and old are non-null.

public KTable<K, V> suppress(final Suppressed<K> suppressed) {
final String name = builder.newProcessorName(SUPPRESS_NAME);

// TODO: follow-up pr to forward the k/v serdes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This will be fixed in part 3

}

static class NotImplementedException extends RuntimeException {
public static class NotImplementedException extends RuntimeException {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just so we can assert that it happens in the scenario tests (which are in a different package)

private static final int SCALE_FACTOR = COMMIT_INTERVAL * 2;
private static final long TIMEOUT_MS = 30_000L;

@Ignore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note these tests are ignored because they can't run until Part 3.

}
}

@Test(expected = ProcessorStateException.class)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note that we're expecting exceptions for now. This will be removed in Part 3.

drainProducerRecords(driver, "output-suppressed", STRING_DESERIALIZER, LONG_DESERIALIZER),
asList(
// TODO: it's not strictly necessary to emit these in final mode, but it's also not harmful... maybe?
new KeyValueTimestamp<>("[k1@0/0]", null, 1L),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The todo refers to emitting this null result, which is a result of session windows merging.

I suppose the suppression operator could drop results with null values if (and only if) it's in "final results" mode. But on the other hand, I worry that it's too much magic.

WDYT?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here's my thinking: for session windows, when we merge the window we will send negating old values downstream, but with final output we should not send any data yet before merging and sending the final result, so there is no point sending null, and hence in this case we should remove it in the suppressed result as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, it's a little more complex to maintain as a special case, but it does seem better not to burden our users with mysteries for the sake of convenience.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with dropping null values

@vvcephei vvcephei changed the title KAFKA-7223: add unit tests in preparation for suppression KAFKA-7223: add tests in preparation for suppression Sep 24, 2018
@vvcephei vvcephei mentioned this pull request Sep 24, 2018
3 tasks
if (newBytes != null) {
buffer.put(newBytes);
}
return buffer.array();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we cannot always return buffer.array() since it just returns the backing byte array of this ByteBuffer, and since multiple ByteBuffer may potentially sits on the same byte array, this array() may actually be much larger than the ByteBuffer's capacity. To be safer we need to do sth. in the ByteBufferSerializer:

        if (data.hasArray()) {
            byte[] arr = data.array();
            if (data.arrayOffset() == 0 && arr.length == data.remaining()) {
                return arr;
            }
        }

        byte[] ret = new byte[data.remaining()];
        data.get(ret, 0, ret.length);
        data.rewind();
        return ret;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, thanks. I'm (clearly) not too familiar with the API. What you say makes sense.

final Serde<K> keySerde,
final Serde<Change<V>> valueSerde) {
this.suppress = requireNonNull(suppress);
this.keySerde = keySerde;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where are these two serdes used in this PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They aren't. They will be in part 3, so the tests need to call this constructor. I added this constructor and the dummy fields just so the (ignored) tests can compile.

It's a bit awkward, I know.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ack.

private static final LongDeserializer LONG_DESERIALIZER = new LongDeserializer();
private static final int COMMIT_INTERVAL = 100;
private static final int SCALE_FACTOR = COMMIT_INTERVAL * 2;
private static final long TIMEOUT_MS = 30_000L;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why SCALE_FACTOR is COMMIT_INTERVAL * 2, is it just to make sure at least one commit is called during this interval of time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. It doesn't actually matter whether or not a commit in called in between the various times used in tests (I've tested it both ways). But it seemed like a reasonable concern that the commit might mess up the suppression in some way, so I went ahead and made sure the tests included commits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds good.

verifyOutput(
outputSuppressed,
asList(
new KeyValueTimestamp<>("v2", 1L, scaledTime(1L)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why the supressed result will not have tick without filtering?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Only because it isn't time to emit it yet. The tick is the newest event, at time 5, so it's still stream-time 5. Based on the suppression config, we won't emit it until after time 7.

The only reason I filtered it out of the "raw" output is that I felt it made the tests more confusing to read. Do you think I should also filter it from the suppressed output (even though it doesn't matter), just to avoid distracting readers with the question you raised?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The reasoning makes sense.

I think not explicitly filtering tick and expect in the output raw is actually fine, and to me it is better exposing this behavior to make sure the raw data did include the tick value while the suppressed one does not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, it's easy enough to put it back in.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I had the same question about tick I also think it makes sense to have it in the raw data.

if (newBytes != null) {
buffer.put(newBytes);
}
return byteBufferSerializer.serialize(null, buffer);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@guozhangwang , rather than copy/paste the non-trivial transformation logic, what do you think about just delegating to the actual ByteBufferSerializer?

The deserializer is trivial, but I went ahead and used it in my deserializer, too, for symmetry.

I'm thinking that as long as the ByteBufferSerializer is stable, and it doesn't do anything weird in the future, this should be fine. And that should be the case, since future versions of the code are going to have to be compatible with data written by older versions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sounds good.

@guozhangwang guozhangwang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The tests LGTM, most of the comments are minor.

One meta comment though: from the unit tests in seems we are going to register one scheduled punctuator for each key in time-based intermediate suppression, and one punctuator for each window in final result suppression in window store, is that right? I'm a bit concerned about the punctuator overhead and how we can maintain it during failover, but I understand it is for part 3 not this PR.

cleanStateBeforeTest(input, outputRaw, outputSuppressed);

final StreamsBuilder builder = new StreamsBuilder();
final KTable<String, Long> valueCounts = builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we share the logic of creating the valueCounts as well?

new KeyValueTimestamp<>("k1", "v1", scaledTime(0L)),
new KeyValueTimestamp<>("k1", "v2", scaledTime(1L)),
new KeyValueTimestamp<>("k2", "v1", scaledTime(2L)),
new KeyValueTimestamp<>("tick", "tick", scaledTime(5L))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd suggest 1) adding another record at 7L and 2) add a filter before line 111 to filter this record out, so that effectively this record will not be processed at all but only moves the stream time. And we can then verify that the final result does not have any outputs from this input, but the tick record did emit because of the stream time advances.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think I might have misunderstood you. As I read it, the role proposed for this extra record is exactly what tick is there for. It exists only to advance stream time, and it gets filtered from the output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fwiw, I think your earlier suggestion to not filter it is fine. I'm also adding a couple of comments explaining what's happening with it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ack

new KeyValueTimestamp<>("k1", "v1", scaledTime(2L)),
new KeyValueTimestamp<>("k1", "v1", scaledTime(1L)),
new KeyValueTimestamp<>("k1", "v1", scaledTime(0L)),
new KeyValueTimestamp<>("k1", "v1", scaledTime(4L))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add another late record after this to be falling out of the grace period, and verify that it will not be reflected in the final result and be skipped?

driver.pipeInput(recordFactory.create("input", "k1", "v1", 2L));
driver.pipeInput(recordFactory.create("input", "k1", "v1", 1L));
driver.pipeInput(recordFactory.create("input", "k1", "v1", 0L));
driver.pipeInput(recordFactory.create("input", "k1", "v1", 5L));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ditto as in the integration test.

drainProducerRecords(driver, "output-suppressed", STRING_DESERIALIZER, LONG_DESERIALIZER),
asList(
// TODO: it's not strictly necessary to emit these in final mode, but it's also not harmful... maybe?
new KeyValueTimestamp<>("[k1@0/0]", null, 1L),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here's my thinking: for session windows, when we merge the window we will send negating old values downstream, but with final output we should not send any data yet before merging and sending the final result, so there is no point sending null, and hence in this case we should remove it in the suppressed result as well?


@SuppressWarnings("PointlessArithmeticExpression")
public class KTableSuppressProcessorTest {
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why remove the comments here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They just seemed unnecessary, as the variable names should make it obvious.

final KTableSuppressProcessor<Windowed<String>, Long> processor =
new KTableSuppressProcessor<>(finalResults(ofMillis(1)));
@Test(expected = KTableSuppressProcessor.NotImplementedException.class)
public void finalResultsSuppressionShouldBufferAndEmitLater() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the difference between this test and the finalResultsWith0GraceBeforeWindowEndShouldBufferAndEmitLater below? Besides grace they seems exactly the same.

In that case, could we just merge these two, to randomly choose one as the grace period, and with a significant amount of likelihood that the random number is 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They are similar, but targeting different behaviors:

  • finalResultsSuppressionShouldBufferAndEmitLater is the "main" unit test: verifying that the buffer saves up all the data and emits at the grace period expiration.
  • finalResultsWith0GraceBeforeWindowEndShouldBufferAndEmitLater is somewhat of a "regression" test. I added it during implementation because I initially failed to account for this behavior: even when the grace period is 0, the final results buffer should wait until the end of the window before emitting (instead of immediately emitting). I think I can name the method better and add a comment to make this clear.

As far as randomized testing goes, I'd like to experiment with a fuzzing framework in the future, rather than hacking out a half-baked one right now. There are some important features, such as a mechanism to reproduce failing tests, that are necessary to make it a "randomized test", rather than a "flaky test".

For now, I'd rather just stick with hand-written scenarios.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Makes sense.

@vvcephei

Copy link
Copy Markdown
Contributor Author

@guozhangwang , thanks for the review. I think I've addressed all your comments.

About the punctuation overhead, I share your concern, and I think we can optimize it, but I agree we should table this discussion until part 3.

@bbejeck bbejeck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @vvcephei, I made a pass and looks good overall, just a few minor comments.

One additional comment should we add unit tests to KTableSuppressProcessorTest with records falling inside the grace period as well as outside of it?

public class FullChangeSerde<T> implements Serde<Change<T>> {
private final Serde<T> inner;

public FullChangeSerde(final Serde<T> inner) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we get a unit test for FullChangeSerde?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh! good catch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for this. I added one, and it turns out I caught a couple of bugs!

verifyOutput(
outputSuppressed,
asList(
new KeyValueTimestamp<>("v2", 1L, scaledTime(1L)),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I had the same question about tick I also think it makes sense to have it in the raw data.

final KTableSuppressProcessor<Windowed<String>, Long> processor =
new KTableSuppressProcessor<>(finalResults(ofMillis(0)));
@Test(expected = KTableSuppressProcessor.NotImplementedException.class)
public void finalResultsWith0GraceBeforeWindowEndShouldBufferAndEmitLater() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: I realize this was here in a previous commit, but in this test and the next spell out Zero vs using the number, it's hard to read.

new KeyValueTimestamp<>("v1", 1L, 2L)
)
);
driver.pipeInput(recordFactory.create("input", "tick", "tick", 5L));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here as with the integration test, maybe don't filter tick from raw to show that suppression is keeping it from being emitted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, I've removed the filter, and added expectations as well as explanatory comments for "tick"

drainProducerRecords(driver, "output-suppressed", STRING_DESERIALIZER, LONG_DESERIALIZER),
asList(
// TODO: it's not strictly necessary to emit these in final mode, but it's also not harmful... maybe?
new KeyValueTimestamp<>("[k1@0/0]", null, 1L),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree with dropping null values

@guozhangwang

Copy link
Copy Markdown
Contributor

@vvcephei Thanks for your replies. I do not have further comments. And once @bbejeck is happy with the current patch we can merge it.

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;

public class FullChangeSerdeTest {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@bbejeck Here's the test you asked for.

@vvcephei

Copy link
Copy Markdown
Contributor Author

Ok @bbejeck, I believe I've addressed all your and @guozhangwang 's comments.
Do you mind making a final pass?

@bbejeck bbejeck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@vvcephei thanks for the updates, LGTM

@guozhangwang
guozhangwang merged commit f712ce6 into apache:trunk Sep 25, 2018
@vvcephei

Copy link
Copy Markdown
Contributor Author

Once again, my sincere thanks for the reviews, @guozhangwang and @bbejeck

@vvcephei
vvcephei deleted the KAFKA-7223-in-mem-suppress-unit-tests branch September 25, 2018 20:42
@mjsax mjsax added the streams label Sep 25, 2018
guozhangwang pushed a commit that referenced this pull request Oct 3, 2018
This is Part 4 of suppression (durability)
Part 1 was #5567 (the API)
Part 2 was #5687 (the tests)
Part 3 was #5693 (in-memory buffering)

Implement a changelog for the suppression buffer so that the buffer state may be recovered on restart or recovery.
As of this PR, suppression is suitable for general usage.

Reviewers: Bill Bejeck <bill@confluent.io>, Guozhang Wang <guozhang@confluent.io>, Matthias J. Sax <matthias@confluent.io>
pengxiaolong pushed a commit to pengxiaolong/kafka that referenced this pull request Jun 14, 2019
This is Part 2 of suppression.
Part 1 was apache#5567

In an effort to control the scope of the review, this PR is just the tests for buffered suppression.

Reviewers: Bill Bejeck <bill@confluent.io>, Guozhang Wang <wangguoz@gmail.com>
pengxiaolong pushed a commit to pengxiaolong/kafka that referenced this pull request Jun 14, 2019
This is Part 4 of suppression (durability)
Part 1 was apache#5567 (the API)
Part 2 was apache#5687 (the tests)
Part 3 was apache#5693 (in-memory buffering)

Implement a changelog for the suppression buffer so that the buffer state may be recovered on restart or recovery.
As of this PR, suppression is suitable for general usage.

Reviewers: Bill Bejeck <bill@confluent.io>, Guozhang Wang <guozhang@confluent.io>, Matthias J. Sax <matthias@confluent.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants