| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent ccd4e0a commit 39ba987
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -73,6 +73,7 @@ | |||
| 73 | 73 | import com.google.cloud.dataflow.sdk.values.PDone; | |
| 74 | 74 | import com.google.cloud.dataflow.sdk.values.PInput; | |
| 75 | 75 | import com.google.cloud.dataflow.sdk.values.POutput; | |
| 76 | + import com.google.common.annotations.VisibleForTesting; | ||
| 76 | 77 | import com.google.common.base.Preconditions; | |
| 77 | 78 | import com.google.common.base.Strings; | |
| 78 | 79 | import com.google.common.collect.ImmutableMap; | |
@@ -188,7 +189,7 @@ public static DataflowPipelineRunner fromOptions(PipelineOptions options) { | |||
| 188 | 189 | return new DataflowPipelineRunner(dataflowOptions); | |
| 189 | 190 | } | |
| 190 | 191 | ||
| 191 | - private DataflowPipelineRunner(DataflowPipelineOptions options) { | ||
| 192 | + @VisibleForTesting protected DataflowPipelineRunner(DataflowPipelineOptions options) { | ||
| 192 | 193 | this.options = options; | |
| 193 | 194 | this.dataflowClient = options.getDataflowClient(); | |
| 194 | 195 | this.translator = DataflowPipelineTranslator.fromOptions(options); | |
@@ -224,7 +225,9 @@ public <OutputT extends POutput, InputT extends PInput> OutputT apply( | |||
| 224 | 225 | @SuppressWarnings("unchecked") | |
| 225 | 226 | OutputT outputT = (OutputT) PCollection.createPrimitiveOutputInternal( | |
| 226 | 227 | pc.getPipeline(), | |
| 227 | - pc.getWindowingStrategy(), | ||
| 228 | + transform instanceof GroupByKey | ||
| 229 | + ? ((GroupByKey<?, ?>) transform).updateWindowingStrategy(pc.getWindowingStrategy()) | ||
| 230 | + : pc.getWindowingStrategy(), | ||
| 228 | 231 | pc.isBounded()); | |
| 229 | 232 | return outputT; | |
| 230 | 233 | ||
@@ -662,7 +665,7 @@ public PCollection<T> apply(PInput input) { | |||
| 662 | 665 | .setWindowingStrategyInternal(WindowingStrategy.globalDefault()) | |
| 663 | 666 | .apply(Window.<KV<Void, Iterable<Void>>>into(new GlobalWindows())) | |
| 664 | 667 | .apply(ParDo.of(new OutputElements<>(transform.getElements(), coder))) | |
| 665 | - .setCoder(coder); | ||
| 668 | + .setCoder(coder).setIsBoundedInternal(IsBounded.BOUNDED); | ||
| 666 | 669 | } catch (CannotProvideCoderException e) { | |
| 667 | 670 | throw new IllegalArgumentException("Unable to infer a coder and no Coder was specified. " | |
| 668 | 671 | + "Please set a coder by invoking Create.withCoder() explicitly.", e); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -176,18 +176,60 @@ public boolean fewKeys() { | |||
| 176 | 176 | ///////////////////////////////////////////////////////////////////////////// | |
| 177 | 177 | ||
| 178 | 178 | @Override | |
| 179 | - public PCollection<KV<K, Iterable<V>>> apply(PCollection<KV<K, V>> input) { | ||
| 180 | - // This operation groups by the combination of key and window, | ||
| 181 | - // merging windows as needed, using the windows assigned to the | ||
| 182 | - // key/value input elements and the window merge operation of the | ||
| 183 | - // window function associated with the input PCollection. | ||
| 179 | + public void validate(PCollection<KV<K, V>> input) { | ||
| 184 | 180 | WindowingStrategy<?, ?> windowingStrategy = input.getWindowingStrategy(); | |
| 181 | + // Verify that the input PCollection is bounded, or that there is windowing/triggering being | ||
| 182 | + // used. Without this, the watermark (at end of global window) will never be reached. | ||
| 183 | + if (windowingStrategy.getWindowFn() instanceof GlobalWindows | ||
| 184 | + && windowingStrategy.getTrigger().getSpec() instanceof DefaultTrigger | ||
| 185 | + && input.isBounded() != IsBounded.BOUNDED) { | ||
| 186 | + throw new IllegalStateException("GroupByKey cannot be applied to non-bounded PCollection in " | ||
| 187 | + + "the GlobalWindow without a trigger. Use a Window.into or Window.triggering transform " | ||
| 188 | + + "prior to GroupByKey."); | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + // Verify that the input Coder<KV<K, V>> is a KvCoder<K, V>, and that | ||
| 192 | + // the key coder is deterministic. | ||
| 193 | + Coder<K> keyCoder = getKeyCoder(input.getCoder()); | ||
| 194 | + try { | ||
| 195 | + keyCoder.verifyDeterministic(); | ||
| 196 | + } catch (NonDeterministicException e) { | ||
| 197 | + throw new IllegalStateException( | ||
| 198 | + "the keyCoder of a GroupByKey must be deterministic", e); | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + // Validate the window merge function. | ||
| 185 | 202 | if (windowingStrategy.getWindowFn() instanceof InvalidWindows) { | |
| 186 | 203 | String cause = ((InvalidWindows<?>) windowingStrategy.getWindowFn()).getCause(); | |
| 187 | 204 | throw new IllegalStateException( | |
| 188 | 205 | "GroupByKey must have a valid Window merge function. " | |
| 189 | 206 | + "Invalid because: " + cause); | |
| 190 | 207 | } | |
| 208 | + } | ||
| 209 | + | ||
| 210 | + public WindowingStrategy<?, ?> updateWindowingStrategy(WindowingStrategy<?, ?> inputStrategy) { | ||
| 211 | + WindowFn<?, ?> inputWindowFn = inputStrategy.getWindowFn(); | ||
| 212 | + if (!inputWindowFn.isNonMerging()) { | ||
| 213 | + // Prevent merging windows again, without explicit user | ||
| 214 | + // involvement, e.g., by Window.into() or Window.remerge(). | ||
| 215 | + inputWindowFn = new InvalidWindows<>( | ||
| 216 | + "WindowFn has already been consumed by previous GroupByKey", inputWindowFn); | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + // We also switch to the continuation trigger associated with the current trigger. | ||
| 220 | + return inputStrategy | ||
| 221 | + .withWindowFn(inputWindowFn) | ||
| 222 | + .withTrigger(inputStrategy.getTrigger().getSpec().getContinuationTrigger()); | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + @Override | ||
| 226 | + public PCollection<KV<K, Iterable<V>>> apply(PCollection<KV<K, V>> input) { | ||
| 227 | + // This operation groups by the combination of key and window, | ||
| 228 | + // merging windows as needed, using the windows assigned to the | ||
| 229 | + // key/value input elements and the window merge operation of the | ||
| 230 | + // window function associated with the input PCollection. | ||
| 231 | + WindowingStrategy<?, ?> windowingStrategy = input.getWindowingStrategy(); | ||
| 232 | + | ||
| 191 | 233 | // By default, implement GroupByKey[AndWindow] via a series of lower-level | |
| 192 | 234 | // operations. | |
| 193 | 235 | return input | |
@@ -206,7 +248,10 @@ public PCollection<KV<K, Iterable<V>>> apply(PCollection<KV<K, V>> input) { | |||
| 206 | 248 | .apply(new SortValuesByTimestamp<K, V>()) | |
| 207 | 249 | ||
| 208 | 250 | // Group each key's values by window, merging windows as needed. | |
| 209 | - .apply(new GroupAlsoByWindow<K, V>(windowingStrategy)); | ||
| 251 | + .apply(new GroupAlsoByWindow<K, V>(windowingStrategy)) | ||
| 252 | + | ||
| 253 | + // And update the windowing strategy as appropriate. | ||
| 254 | + .setWindowingStrategyInternal(updateWindowingStrategy(windowingStrategy)); | ||
| 210 | 255 | } | |
| 211 | 256 | ||
| 212 | 257 | @Override | |
@@ -383,46 +428,12 @@ public PCollection<KV<K, Iterable<V>>> apply( | |||
| 383 | 428 | public static class GroupByKeyOnly<K, V> | |
| 384 | 429 | extends PTransform<PCollection<KV<K, V>>, | |
| 385 | 430 | PCollection<KV<K, Iterable<V>>>> { | |
| 386 | - @Override | ||
| 387 | - public void validate(PCollection<KV<K, V>> input) { | ||
| 388 | - WindowingStrategy<?, ?> windowingStrategy = input.getWindowingStrategy(); | ||
| 389 | - if (windowingStrategy.getWindowFn() instanceof GlobalWindows | ||
| 390 | - && windowingStrategy.getTrigger().getSpec() instanceof DefaultTrigger | ||
| 391 | - && input.isBounded() != IsBounded.BOUNDED) { | ||
| 392 | - throw new IllegalStateException("Non-bounded PCollection cannot be " | ||
| 393 | - + "processed with GlobalWindow and DefaultTrigger for GroupByKey." | ||
| 394 | - + "Use Window.into transform prior to GroupByKey."); | ||
| 395 | - } | ||
| 396 | - // Verify that the input Coder<KV<K, V>> is a KvCoder<K, V>, and that | ||
| 397 | - // the key coder is deterministic. | ||
| 398 | - Coder<K> keyCoder = getKeyCoder(input.getCoder()); | ||
| 399 | - try { | ||
| 400 | - keyCoder.verifyDeterministic(); | ||
| 401 | - } catch (NonDeterministicException e) { | ||
| 402 | - throw new IllegalStateException( | ||
| 403 | - "the keyCoder of a GroupByKey must be deterministic", e); | ||
| 404 | - } | ||
| 405 | - } | ||
| 406 | 431 | ||
| 407 | 432 | @SuppressWarnings({"rawtypes", "unchecked"}) | |
| 408 | 433 | @Override | |
| 409 | 434 | public PCollection<KV<K, Iterable<V>>> apply(PCollection<KV<K, V>> input) { | |
| 410 | - WindowingStrategy<?, ?> oldWindowingStrategy = input.getWindowingStrategy(); | ||
| 411 | - WindowFn<?, ?> newWindowFn = oldWindowingStrategy.getWindowFn(); | ||
| 412 | - if (!newWindowFn.isNonMerging()) { | ||
| 413 | - // Prevent merging windows again, without explicit user | ||
| 414 | - // involvement, e.g., by Window.into() or Window.remerge(). | ||
| 415 | - newWindowFn = new InvalidWindows( | ||
| 416 | - "WindowFn has already been consumed by previous GroupByKey", newWindowFn); | ||
| 417 | - } | ||
| 418 | - | ||
| 419 | - // We also switch to the continuation trigger associated with the current trigger. | ||
| 420 | - WindowingStrategy<?, ?> newWindowingStrategy = oldWindowingStrategy | ||
| 421 | - .withWindowFn(newWindowFn) | ||
| 422 | - .withTrigger(oldWindowingStrategy.getTrigger().getSpec().getContinuationTrigger()); | ||
| 423 | - | ||
| 424 | 435 | return PCollection.<KV<K, Iterable<V>>>createPrimitiveOutputInternal( | |
| 425 | - input.getPipeline(), newWindowingStrategy, input.isBounded()); | ||
| 436 | + input.getPipeline(), input.getWindowingStrategy(), input.isBounded()); | ||
| 426 | 437 | } | |
| 427 | 438 | ||
| 428 | 439 | /** | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -181,7 +181,6 @@ public IsBounded isBounded() { | |||
| 181 | 181 | return isBounded; | |
| 182 | 182 | } | |
| 183 | 183 | ||
| 184 | - | ||
| 185 | 184 | ///////////////////////////////////////////////////////////////////////////// | |
| 186 | 185 | // Internal details below here. | |
| 187 | 186 | ||
@@ -223,8 +222,10 @@ public PCollection<T> setWindowingStrategyInternal(WindowingStrategy<?, ?> windo | |||
| 223 | 222 | ||
| 224 | 223 | /** | |
| 225 | 224 | * Sets the {@link PCollection.IsBounded} of this {@code PCollection}. | |
| 225 | + * | ||
| 226 | + * <p> For use by internal transformations only. | ||
| 226 | 227 | */ | |
| 227 | - private PCollection<T> setIsBoundedInternal(IsBounded isBounded) { | ||
| 228 | + public PCollection<T> setIsBoundedInternal(IsBounded isBounded) { | ||
| 228 | 229 | this.isBounded = isBounded; | |
| 229 | 230 | return this; | |
| 230 | 231 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,14 +26,22 @@ | |||
| 26 | 26 | import com.google.cloud.dataflow.sdk.coders.KvCoder; | |
| 27 | 27 | import com.google.cloud.dataflow.sdk.coders.MapCoder; | |
| 28 | 28 | import com.google.cloud.dataflow.sdk.coders.StringUtf8Coder; | |
| 29 | + import com.google.cloud.dataflow.sdk.options.DataflowPipelineOptions; | ||
| 30 | + import com.google.cloud.dataflow.sdk.options.DirectPipelineOptions; | ||
| 31 | + import com.google.cloud.dataflow.sdk.options.PipelineOptionsFactory; | ||
| 32 | + import com.google.cloud.dataflow.sdk.runners.DataflowPipelineRunner; | ||
| 33 | + import com.google.cloud.dataflow.sdk.runners.DirectPipelineRunner; | ||
| 29 | 34 | import com.google.cloud.dataflow.sdk.testing.DataflowAssert; | |
| 30 | 35 | import com.google.cloud.dataflow.sdk.testing.RunnableOnService; | |
| 31 | 36 | import com.google.cloud.dataflow.sdk.testing.TestPipeline; | |
| 32 | 37 | import com.google.cloud.dataflow.sdk.transforms.windowing.FixedWindows; | |
| 33 | 38 | import com.google.cloud.dataflow.sdk.transforms.windowing.InvalidWindows; | |
| 34 | 39 | import com.google.cloud.dataflow.sdk.transforms.windowing.Sessions; | |
| 35 | 40 | import com.google.cloud.dataflow.sdk.transforms.windowing.Window; | |
| 41 | + import com.google.cloud.dataflow.sdk.util.NoopPathValidator; | ||
| 42 | + import com.google.cloud.dataflow.sdk.util.WindowingStrategy; | ||
| 36 | 43 | import com.google.cloud.dataflow.sdk.values.KV; | |
| 44 | + import com.google.cloud.dataflow.sdk.values.PBegin; | ||
| 37 | 45 | import com.google.cloud.dataflow.sdk.values.PCollection; | |
| 38 | 46 | ||
| 39 | 47 | import org.joda.time.Duration; | |
@@ -57,7 +65,7 @@ | |||
| 57 | 65 | public class GroupByKeyTest { | |
| 58 | 66 | ||
| 59 | 67 | @Rule | |
| 60 | - public ExpectedException expectedEx = ExpectedException.none(); | ||
| 68 | + public ExpectedException thrown = ExpectedException.none(); | ||
| 61 | 69 | ||
| 62 | 70 | @Test | |
| 63 | 71 | @Category(RunnableOnService.class) | |
@@ -167,8 +175,6 @@ public void testGroupByKeyEmpty() { | |||
| 167 | 175 | ||
| 168 | 176 | @Test | |
| 169 | 177 | public void testGroupByKeyNonDeterministic() throws Exception { | |
| 170 | - expectedEx.expect(IllegalStateException.class); | ||
| 171 | - expectedEx.expectMessage("must be deterministic"); | ||
| 172 | 178 | ||
| 173 | 179 | List<KV<Map<String, String>, Integer>> ungroupedPairs = Arrays.asList(); | |
| 174 | 180 | ||
@@ -180,9 +186,9 @@ public void testGroupByKeyNonDeterministic() throws Exception { | |||
| 180 | 186 | KvCoder.of(MapCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()), | |
| 181 | 187 | BigEndianIntegerCoder.of()))); | |
| 182 | 188 | ||
| 189 | + thrown.expect(IllegalStateException.class); | ||
| 190 | + thrown.expectMessage("must be deterministic"); | ||
| 183 | 191 | input.apply(GroupByKey.<Map<String, String>, Integer>create()); | |
| 184 | - | ||
| 185 | - p.run(); | ||
| 186 | 192 | } | |
| 187 | 193 | ||
| 188 | 194 | @Test | |
@@ -230,9 +236,30 @@ public void testWindowFnInvalidation() { | |||
| 230 | 236 | Duration.standardMinutes(1))))); | |
| 231 | 237 | } | |
| 232 | 238 | ||
| 239 | + /** | ||
| 240 | + * Create a test pipeline that uses the {@link DataflowPipelineRunner} so that {@link GroupByKey} | ||
| 241 | + * is not expanded. This is used for verifying that even without expansion the proper errors show | ||
| 242 | + * up. | ||
| 243 | + */ | ||
| 244 | + private Pipeline createTestServiceRunner() { | ||
| 245 | + DataflowPipelineOptions options = PipelineOptionsFactory.as(DataflowPipelineOptions.class); | ||
| 246 | + options.setRunner(DataflowPipelineRunner.class); | ||
| 247 | + options.setProject("someproject"); | ||
| 248 | + options.setStagingLocation("gs://staging"); | ||
| 249 | + options.setPathValidatorClass(NoopPathValidator.class); | ||
| 250 | + options.setDataflowClient(null); | ||
| 251 | + return Pipeline.create(options); | ||
| 252 | + } | ||
| 253 | + | ||
| 254 | + private Pipeline createTestDirectRunner() { | ||
| 255 | + DirectPipelineOptions options = PipelineOptionsFactory.as(DirectPipelineOptions.class); | ||
| 256 | + options.setRunner(DirectPipelineRunner.class); | ||
| 257 | + return Pipeline.create(options); | ||
| 258 | + } | ||
| 259 | + | ||
| 233 | 260 | @Test | |
| 234 | - public void testInvalidWindows() { | ||
| 235 | - Pipeline p = TestPipeline.create(); | ||
| 261 | + public void testInvalidWindowsDirect() { | ||
| 262 | + Pipeline p = createTestDirectRunner(); | ||
| 236 | 263 | ||
| 237 | 264 | List<KV<String, Integer>> ungroupedPairs = Arrays.asList(); | |
| 238 | 265 | ||
@@ -242,15 +269,30 @@ public void testInvalidWindows() { | |||
| 242 | 269 | .apply(Window.<KV<String, Integer>>into( | |
| 243 | 270 | Sessions.withGapDuration(Duration.standardMinutes(1)))); | |
| 244 | 271 | ||
| 245 | - try { | ||
| 246 | - input | ||
| 247 | - .apply("GroupByKey", GroupByKey.<String, Integer>create()) | ||
| 248 | - .apply("GroupByKeyAgain", GroupByKey.<String, Iterable<Integer>>create()); | ||
| 249 | - Assert.fail("Exception should have been thrown"); | ||
| 250 | - } catch (IllegalStateException e) { | ||
| 251 | - Assert.assertTrue(e.getMessage().startsWith( | ||
| 252 | - "GroupByKey must have a valid Window merge function.")); | ||
| 253 | - } | ||
| 272 | + thrown.expect(IllegalStateException.class); | ||
| 273 | + thrown.expectMessage("GroupByKey must have a valid Window merge function"); | ||
| 274 | + input | ||
| 275 | + .apply("GroupByKey", GroupByKey.<String, Integer>create()) | ||
| 276 | + .apply("GroupByKeyAgain", GroupByKey.<String, Iterable<Integer>>create()); | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + @Test | ||
| 280 | + public void testInvalidWindowsService() { | ||
| 281 | + Pipeline p = createTestServiceRunner(); | ||
| 282 | + | ||
| 283 | + List<KV<String, Integer>> ungroupedPairs = Arrays.asList(); | ||
| 284 | + | ||
| 285 | + PCollection<KV<String, Integer>> input = | ||
| 286 | + p.apply(Create.of(ungroupedPairs) | ||
| 287 | + .withCoder(KvCoder.of(StringUtf8Coder.of(), BigEndianIntegerCoder.of()))) | ||
| 288 | + .apply(Window.<KV<String, Integer>>into( | ||
| 289 | + Sessions.withGapDuration(Duration.standardMinutes(1)))); | ||
| 290 | + | ||
| 291 | + thrown.expect(IllegalStateException.class); | ||
| 292 | + thrown.expectMessage("GroupByKey must have a valid Window merge function"); | ||
| 293 | + input | ||
| 294 | + .apply("GroupByKey", GroupByKey.<String, Integer>create()) | ||
| 295 | + .apply("GroupByKeyAgain", GroupByKey.<String, Iterable<Integer>>create()); | ||
| 254 | 296 | } | |
| 255 | 297 | ||
| 256 | 298 | @Test | |
@@ -278,6 +320,48 @@ public void testRemerge() { | |||
| 278 | 320 | Sessions.withGapDuration(Duration.standardMinutes(1)))); | |
| 279 | 321 | } | |
| 280 | 322 | ||
| 323 | + @Test | ||
| 324 | + public void testGroupByKeyDirectUnbounded() { | ||
| 325 | + Pipeline p = createTestDirectRunner(); | ||
| 326 | + | ||
| 327 | + PCollection<KV<String, Integer>> input = p | ||
| 328 | + .apply(new PTransform<PBegin, PCollection<KV<String, Integer>>>() { | ||
| 329 | + @Override | ||
| 330 | + public PCollection<KV<String, Integer>> apply(PBegin input) { | ||
| 331 | + return PCollection.createPrimitiveOutputInternal(input.getPipeline(), | ||
| 332 | + WindowingStrategy.globalDefault(), PCollection.IsBounded.UNBOUNDED); | ||
| 333 | + } | ||
| 334 | + }); | ||
| 335 | + | ||
| 336 | + thrown.expect(IllegalStateException.class); | ||
| 337 | + thrown.expectMessage( | ||
| 338 | + "GroupByKey cannot be applied to non-bounded PCollection in the GlobalWindow without " | ||
| 339 | + + "a trigger. Use a Window.into or Window.triggering transform prior to GroupByKey."); | ||
| 340 | + | ||
| 341 | + input.apply("GroupByKey", GroupByKey.<String, Integer>create()); | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + @Test | ||
| 345 | + public void testGroupByKeyServiceUnbounded() { | ||
| 346 | + Pipeline p = createTestServiceRunner(); | ||
| 347 | + | ||
| 348 | + PCollection<KV<String, Integer>> input = p | ||
| 349 | + .apply(new PTransform<PBegin, PCollection<KV<String, Integer>>>() { | ||
| 350 | + @Override | ||
| 351 | + public PCollection<KV<String, Integer>> apply(PBegin input) { | ||
| 352 | + return PCollection.createPrimitiveOutputInternal(input.getPipeline(), | ||
| 353 | + WindowingStrategy.globalDefault(), PCollection.IsBounded.UNBOUNDED); | ||
| 354 | + } | ||
| 355 | + }); | ||
| 356 | + | ||
| 357 | + thrown.expect(IllegalStateException.class); | ||
| 358 | + thrown.expectMessage( | ||
| 359 | + "GroupByKey cannot be applied to non-bounded PCollection in the GlobalWindow without " | ||
| 360 | + + "a trigger. Use a Window.into or Window.triggering transform prior to GroupByKey."); | ||
| 361 | + | ||
| 362 | + input.apply("GroupByKey", GroupByKey.<String, Integer>create()); | ||
| 363 | + } | ||
| 364 | + | ||
| 281 | 365 | @Test | |
| 282 | 366 | public void testGroupByKeyGetName() { | |
| 283 | 367 | Assert.assertEquals("GroupByKey", GroupByKey.<String, Integer>create().getName()); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments