| # | |
| # 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. | |
| # | |
| """Provides TestStream for verifying streaming runner semantics. | |
| For internal use only; no backwards-compatibility guarantees. | |
| """ | |
| # pytype: skip-file | |
| from abc import ABCMeta | |
| from abc import abstractmethod | |
| from enum import Enum | |
| from functools import total_ordering | |
| import apache_beam as beam | |
| from apache_beam import coders | |
| from apache_beam import pvalue | |
| from apache_beam.portability import common_urns | |
| from apache_beam.portability.api import beam_interactive_api_pb2 | |
| from apache_beam.portability.api import beam_runner_api_pb2 | |
| from apache_beam.portability.api import endpoints_pb2 | |
| from apache_beam.transforms import PTransform | |
| from apache_beam.transforms import core | |
| from apache_beam.transforms import window | |
| from apache_beam.transforms.timeutil import TimeDomain | |
| from apache_beam.transforms.userstate import TimerSpec | |
| from apache_beam.transforms.userstate import on_timer | |
| from apache_beam.transforms.window import TimestampedValue | |
| from apache_beam.utils import timestamp | |
| from apache_beam.utils.timestamp import MIN_TIMESTAMP | |
| from apache_beam.utils.timestamp import Duration | |
| from apache_beam.utils.timestamp import Timestamp | |
| from apache_beam.utils.windowed_value import WindowedValue | |
| __all__ = [ | |
| 'Event', | |
| 'ElementEvent', | |
| 'WatermarkEvent', | |
| 'ProcessingTimeEvent', | |
| 'TestStream', | |
| ] | |
| @total_ordering | |
| class Event(metaclass=ABCMeta): # type: ignore[misc] | |
| """Test stream event to be emitted during execution of a TestStream.""" | |
| @abstractmethod | |
| def __eq__(self, other): | |
| raise NotImplementedError | |
| @abstractmethod | |
| def __hash__(self): | |
| raise NotImplementedError | |
| @abstractmethod | |
| def __lt__(self, other): | |
| raise NotImplementedError | |
| @abstractmethod | |
| def to_runner_api(self, element_coder): | |
| raise NotImplementedError | |
| @staticmethod | |
| def from_runner_api(proto, element_coder): | |
| if proto.HasField('element_event'): | |
| event = proto.element_event | |
| tag = None if event.tag == 'None' else event.tag | |
| return ElementEvent([ | |
| TimestampedValue( | |
| element_coder.decode(tv.encoded_element), | |
| Timestamp(micros=1000 * tv.timestamp)) | |
| for tv in proto.element_event.elements | |
| ], tag=tag) # yapf: disable | |
| elif proto.HasField('watermark_event'): | |
| event = proto.watermark_event | |
| tag = None if event.tag == 'None' else event.tag | |
| return WatermarkEvent( | |
| Timestamp(micros=1000 * proto.watermark_event.new_watermark), tag=tag) | |
| elif proto.HasField('processing_time_event'): | |
| return ProcessingTimeEvent( | |
| timestamp.Duration( | |
| micros=1000 * proto.processing_time_event.advance_duration)) | |
| else: | |
| raise ValueError( | |
| 'Unknown TestStream Event type: %s' % proto.WhichOneof('event')) | |
| class ElementEvent(Event): | |
| """Element-producing test stream event.""" | |
| def __init__(self, timestamped_values, tag=None): | |
| self.timestamped_values = timestamped_values | |
| self.tag = tag | |
| def __eq__(self, other): | |
| if not isinstance(other, ElementEvent): | |
| return False | |
| return ( | |
| self.timestamped_values == other.timestamped_values and | |
| self.tag == other.tag) | |
| def __hash__(self): | |
| return hash(self.timestamped_values) | |
| def __lt__(self, other): | |
| if not isinstance(other, ElementEvent): | |
| raise TypeError | |
| return self.timestamped_values < other.timestamped_values | |
| def to_runner_api(self, element_coder): | |
| tag = 'None' if self.tag is None else self.tag | |
| return beam_runner_api_pb2.TestStreamPayload.Event( | |
| element_event=beam_runner_api_pb2.TestStreamPayload.Event.AddElements( | |
| elements=[ | |
| beam_runner_api_pb2.TestStreamPayload.TimestampedElement( | |
| encoded_element=element_coder.encode(tv.value), | |
| timestamp=tv.timestamp.micros // 1000) | |
| for tv in self.timestamped_values | |
| ], | |
| tag=tag)) | |
| def __repr__(self): | |
| return 'ElementEvent: <{}, {}>'.format([(e.value, e.timestamp) | |
| for e in self.timestamped_values], | |
| self.tag) | |
| class WatermarkEvent(Event): | |
| """Watermark-advancing test stream event.""" | |
| def __init__(self, new_watermark, tag=None): | |
| self.new_watermark = Timestamp.of(new_watermark) | |
| self.tag = tag | |
| def __eq__(self, other): | |
| if not isinstance(other, WatermarkEvent): | |
| return False | |
| return self.new_watermark == other.new_watermark and self.tag == other.tag | |
| def __hash__(self): | |
| return hash(str(self.new_watermark) + str(self.tag)) | |
| def __lt__(self, other): | |
| if not isinstance(other, WatermarkEvent): | |
| raise TypeError | |
| return self.new_watermark < other.new_watermark | |
| def to_runner_api(self, unused_element_coder): | |
| tag = 'None' if self.tag is None else self.tag | |
| # Assert that no precision is lost. | |
| assert self.new_watermark.micros % 1000 == 0 | |
| return beam_runner_api_pb2.TestStreamPayload.Event( | |
| watermark_event=beam_runner_api_pb2.TestStreamPayload.Event. | |
| AdvanceWatermark( | |
| new_watermark=self.new_watermark.micros // 1000, tag=tag)) | |
| def __repr__(self): | |
| return 'WatermarkEvent: <{}, {}>'.format(self.new_watermark, self.tag) | |
| class ProcessingTimeEvent(Event): | |
| """Processing time-advancing test stream event.""" | |
| def __init__(self, advance_by): | |
| self.advance_by = Duration.of(advance_by) | |
| def __eq__(self, other): | |
| if not isinstance(other, ProcessingTimeEvent): | |
| return False | |
| return self.advance_by == other.advance_by | |
| def __hash__(self): | |
| return hash(self.advance_by) | |
| def __lt__(self, other): | |
| if not isinstance(other, ProcessingTimeEvent): | |
| raise TypeError | |
| return self.advance_by < other.advance_by | |
| def to_runner_api(self, unused_element_coder): | |
| assert self.advance_by.micros % 1000 == 0 | |
| return beam_runner_api_pb2.TestStreamPayload.Event( | |
| processing_time_event=beam_runner_api_pb2.TestStreamPayload.Event. | |
| AdvanceProcessingTime(advance_duration=self.advance_by.micros // 1000)) | |
| def __repr__(self): | |
| return 'ProcessingTimeEvent: <{}>'.format(self.advance_by) | |
| class WindowedValueHolderMeta(type): | |
| """A metaclass that overrides the isinstance check for WindowedValueHolder. | |
| Python does a quick test for exact match. If an instance is exactly of | |
| type WindowedValueHolder, the overridden isinstance check is omitted. | |
| The override is needed because WindowedValueHolder elements encoded then | |
| decoded become Row elements. | |
| """ | |
| def __instancecheck__(cls, other): | |
| """Checks if a beam.Row typed instance is a WindowedValueHolder. | |
| """ | |
| return ( | |
| isinstance(other, beam.Row) and hasattr(other, 'windowed_value') and | |
| hasattr(other, 'urn') and | |
| isinstance(other.windowed_value, WindowedValue) and | |
| other.urn == common_urns.coders.ROW.urn) | |
| class WindowedValueHolder(beam.Row, metaclass=WindowedValueHolderMeta): | |
| """A class that holds a WindowedValue. | |
| This is a special class that can be used by the runner that implements the | |
| TestStream as a signal that the underlying value should be unreified to the | |
| specified window. | |
| """ | |
| # Register WindowedValueHolder to always use RowCoder. | |
| coders.registry.register_coder(WindowedValueHolderMeta, coders.RowCoder) | |
| def __init__(self, windowed_value): | |
| assert isinstance(windowed_value, WindowedValue), ( | |
| 'WindowedValueHolder can only hold %s type. Instead, %s is given.') % ( | |
| WindowedValue, windowed_value) | |
| super().__init__( | |
| **{ | |
| 'windowed_value': windowed_value, 'urn': common_urns.coders.ROW.urn | |
| }) | |
| @classmethod | |
| def from_row(cls, row): | |
| """Converts a beam.Row typed instance to WindowedValueHolder. | |
| """ | |
| if isinstance(row, WindowedValueHolder): | |
| return WindowedValueHolder(row.windowed_value) | |
| assert isinstance(row, beam.Row), 'The given row %s must be a %s type' % ( | |
| row, beam.Row) | |
| assert hasattr(row, 'windowed_value'), ( | |
| 'The given %s must have a windowed_value attribute.') % row | |
| assert isinstance(row.windowed_value, WindowedValue), ( | |
| 'The windowed_value attribute of %s must be a %s type') % ( | |
| row, WindowedValue) | |
| class TestStream(PTransform): | |
| """Test stream that generates events on an unbounded PCollection of elements. | |
| Each event emits elements, advances the watermark or advances the processing | |
| time. After all of the specified elements are emitted, ceases to produce | |
| output. | |
| Applying the PTransform will return a single PCollection if only the default | |
| output or only one output tag has been used. Otherwise a dictionary of output | |
| names to PCollections will be returned. | |
| """ | |
| def __init__( | |
| self, | |
| coder=coders.FastPrimitivesCoder(), | |
| events=None, | |
| output_tags=None, | |
| endpoint=None): | |
| """ | |
| Args: | |
| coder: (apache_beam.Coder) the coder to encode/decode elements. | |
| events: (List[Event]) a list of instructions for the TestStream to | |
| execute. If specified, the events tags must exist in the output_tags. | |
| output_tags: (List[str]) Initial set of outputs. If no event references an | |
| output tag, no output will be produced for that tag. | |
| endpoint: (str) a URL locating a TestStreamService. | |
| """ | |
| super().__init__() | |
| assert coder is not None | |
| self.coder = coder | |
| self.watermarks = {None: timestamp.MIN_TIMESTAMP} | |
| self.output_tags = set(output_tags) if output_tags else set() | |
| self._events = [] if events is None else list(events) | |
| self._endpoint = endpoint | |
| event_tags = set( | |
| e.tag for e in self._events | |
| if isinstance(e, (WatermarkEvent, ElementEvent))) | |
| assert event_tags.issubset(self.output_tags), \ | |
| '{} is not a subset of {}'.format(event_tags, output_tags) | |
| assert not (self._events and self._endpoint), \ | |
| 'Only either events or an endpoint can be given at once.' | |
| def get_windowing(self, unused_inputs): | |
| return core.Windowing(window.GlobalWindows()) | |
| def _infer_output_coder(self, input_type=None, input_coder=None): | |
| return self.coder | |
| def expand(self, pbegin): | |
| assert isinstance(pbegin, pvalue.PBegin) | |
| self.pipeline = pbegin.pipeline | |
| if not self.output_tags: | |
| self.output_tags = {None} | |
| # For backwards compatibility return a single PCollection. | |
| if self.output_tags == {None}: | |
| return pvalue.PCollection( | |
| self.pipeline, is_bounded=False, tag=list(self.output_tags)[0]) | |
| return { | |
| tag: pvalue.PCollection(self.pipeline, is_bounded=False, tag=tag) | |
| for tag in self.output_tags | |
| } | |
| def _add(self, event): | |
| if isinstance(event, ElementEvent): | |
| for tv in event.timestamped_values: | |
| assert tv.timestamp < timestamp.MAX_TIMESTAMP, ( | |
| 'Element timestamp must be before timestamp.MAX_TIMESTAMP.') | |
| elif isinstance(event, WatermarkEvent): | |
| if event.tag not in self.watermarks: | |
| self.watermarks[event.tag] = timestamp.MIN_TIMESTAMP | |
| assert event.new_watermark > self.watermarks[event.tag], ( | |
| 'Watermark must strictly-monotonically advance.') | |
| self.watermarks[event.tag] = event.new_watermark | |
| elif isinstance(event, ProcessingTimeEvent): | |
| assert event.advance_by > 0, ( | |
| 'Must advance processing time by positive amount.') | |
| else: | |
| raise ValueError('Unknown event: %s' % event) | |
| self._events.append(event) | |
| def add_elements(self, elements, tag=None, event_timestamp=None): | |
| """Add elements to the TestStream. | |
| Elements added to the TestStream will be produced during pipeline execution. | |
| These elements can be TimestampedValue, WindowedValue or raw unwrapped | |
| elements that are serializable using the TestStream's specified Coder. When | |
| a TimestampedValue or a WindowedValue element is used, the timestamp of the | |
| TimestampedValue or WindowedValue will be the timestamp of the produced | |
| element; otherwise, the current watermark timestamp will be used for that | |
| element. The windows of a given WindowedValue are ignored by the | |
| TestStream. | |
| """ | |
| self.output_tags.add(tag) | |
| timestamped_values = [] | |
| if tag not in self.watermarks: | |
| self.watermarks[tag] = timestamp.MIN_TIMESTAMP | |
| for element in elements: | |
| if isinstance(element, TimestampedValue): | |
| timestamped_values.append(element) | |
| elif isinstance(element, WindowedValue): | |
| # Drop windows for elements in test stream. | |
| timestamped_values.append( | |
| TimestampedValue(element.value, element.timestamp)) | |
| else: | |
| # Add elements with timestamp equal to current watermark. | |
| if event_timestamp is None: | |
| event_timestamp = self.watermarks[tag] | |
| timestamped_values.append(TimestampedValue(element, event_timestamp)) | |
| self._add(ElementEvent(timestamped_values, tag)) | |
| return self | |
| def advance_watermark_to(self, new_watermark, tag=None): | |
| """Advance the watermark to a given Unix timestamp. | |
| The Unix timestamp value used must be later than the previous watermark | |
| value and should be given as an int, float or utils.timestamp.Timestamp | |
| object. | |
| """ | |
| self.output_tags.add(tag) | |
| self._add(WatermarkEvent(new_watermark, tag)) | |
| return self | |
| def advance_watermark_to_infinity(self, tag=None): | |
| """Advance the watermark to the end of time, completing this TestStream.""" | |
| self.advance_watermark_to(timestamp.MAX_TIMESTAMP, tag) | |
| return self | |
| def advance_processing_time(self, advance_by): | |
| """Advance the current processing time by a given duration in seconds. | |
| The duration must be a positive second duration and should be given as an | |
| int, float or utils.timestamp.Duration object. | |
| """ | |
| self._add(ProcessingTimeEvent(advance_by)) | |
| return self | |
| def to_runner_api_parameter(self, context): | |
| # Sort the output tags so that the order is deterministic and we are able | |
| # to test equality on a roundtrip through the to/from proto apis. | |
| return ( | |
| common_urns.primitives.TEST_STREAM.urn, | |
| beam_runner_api_pb2.TestStreamPayload( | |
| coder_id=context.coders.get_id(self.coder), | |
| events=[e.to_runner_api(self.coder) for e in self._events], | |
| endpoint=endpoints_pb2.ApiServiceDescriptor(url=self._endpoint))) | |
| @staticmethod | |
| @PTransform.register_urn( | |
| common_urns.primitives.TEST_STREAM.urn, | |
| beam_runner_api_pb2.TestStreamPayload) | |
| def from_runner_api_parameter(ptransform, payload, context): | |
| coder = context.coders.get_by_id(payload.coder_id) | |
| output_tags = set( | |
| None if k == 'None' else k for k in ptransform.outputs.keys()) | |
| return TestStream( | |
| coder=coder, | |
| events=[Event.from_runner_api(e, coder) for e in payload.events], | |
| output_tags=output_tags, | |
| endpoint=payload.endpoint.url) | |
| class TimingInfo(object): | |
| def __init__(self, processing_time, watermark): | |
| self._processing_time = Timestamp.of(processing_time) | |
| self._watermark = Timestamp.of(watermark) | |
| @property | |
| def processing_time(self): | |
| return self._processing_time | |
| @property | |
| def watermark(self): | |
| return self._watermark | |
| def __repr__(self): | |
| return '({}, {})'.format(self.processing_time, self.watermark) | |
| class PairWithTiming(PTransform): | |
| """Pairs the input element with timing information. | |
| Input: element; output: KV(element, timing information) | |
| Where timing information := (processing time, watermark) | |
| This is used in the ReverseTestStream implementation to replay watermark | |
| advancements. | |
| """ | |
| URN = "beam:transform:pair_with_timing:v1" | |
| def expand(self, pcoll): | |
| return pvalue.PCollection.from_(pcoll) | |
| class OutputFormat(Enum): | |
| TEST_STREAM_EVENTS = 1 | |
| TEST_STREAM_FILE_RECORDS = 2 | |
| SERIALIZED_TEST_STREAM_FILE_RECORDS = 3 | |
| class ReverseTestStream(PTransform): | |
| """A Transform that can create TestStream events from a stream of elements. | |
| This currently assumes that this the pipeline being run on a single machine | |
| and elements come in order and are outputted in the same order that they came | |
| in. | |
| """ | |
| def __init__( | |
| self, sample_resolution_sec, output_tag, coder=None, output_format=None): | |
| self._sample_resolution_sec = sample_resolution_sec | |
| self._output_tag = output_tag | |
| self._output_format = output_format if output_format \ | |
| else OutputFormat.TEST_STREAM_EVENTS | |
| self._coder = coder if coder else beam.coders.FastPrimitivesCoder() | |
| def expand(self, pcoll): | |
| ret = ( | |
| pcoll | |
| | beam.WindowInto(beam.window.GlobalWindows()) | |
| # First get the initial timing information. This will be used to start | |
| # the periodic timers which will generate processing time and watermark | |
| # advancements every `sample_resolution_sec`. | |
| | 'initial timing' >> PairWithTiming() | |
| # Next, map every element to the same key so that only a single timer is | |
| # started for this given ReverseTestStream. | |
| | 'first key' >> beam.Map(lambda x: (0, x)) | |
| # Next, pass-through each element which will be paired with its timing | |
| # info in the next step. Also, start the periodic timers. We use timers | |
| # in this situation to capture watermark advancements that occur when | |
| # there are no elements being produced upstream. | |
| | beam.ParDo( | |
| _TimingEventGenerator( | |
| output_tag=self._output_tag, | |
| sample_resolution_sec=self._sample_resolution_sec)) | |
| # Next, retrieve the timing information for watermark events that were | |
| # generated in the previous step. This is because elements generated | |
| # through the timers don't have their timing information yet. | |
| | 'timing info for watermarks' >> PairWithTiming() | |
| # Re-key to the same key to keep global state. | |
| | 'second key' >> beam.Map(lambda x: (0, x)) | |
| # Format the events properly. | |
| | beam.ParDo(_TestStreamFormatter(self._coder, self._output_format))) | |
| if self._output_format == OutputFormat.SERIALIZED_TEST_STREAM_FILE_RECORDS: | |
| def serializer(e): | |
| return e.SerializeToString() | |
| ret = ret | 'serializer' >> beam.Map(serializer) | |
| return ret | |
| class _TimingEventGenerator(beam.DoFn): | |
| """Generates ProcessingTimeEvents and WatermarkEvents at a regular cadence. | |
| The runner keeps the state of the clock (which may be faked) and the | |
| watermarks, which are inaccessible to SDKs. This DoFn generates | |
| ProcessingTimeEvents and WatermarkEvents at a specified sampling rate to | |
| capture any clock or watermark advancements between elements. | |
| """ | |
| # Used to return the initial timing information. | |
| EXECUTE_ONCE_STATE = beam.transforms.userstate.BagStateSpec( | |
| name='execute_once_state', coder=beam.coders.FastPrimitivesCoder()) | |
| # A processing time timer in an infinite loop that generates the events that | |
| # will be paired with the TimingInfo from the runner. | |
| TIMING_SAMPLER = TimerSpec('timing_sampler', TimeDomain.REAL_TIME) | |
| def __init__(self, output_tag, sample_resolution_sec=0.1): | |
| self._output_tag = output_tag | |
| self._sample_resolution_sec = sample_resolution_sec | |
| @on_timer(TIMING_SAMPLER) | |
| def on_timing_sampler( | |
| self, | |
| timestamp=beam.DoFn.TimestampParam, | |
| window=beam.DoFn.WindowParam, | |
| timing_sampler=beam.DoFn.TimerParam(TIMING_SAMPLER)): | |
| """Yields an unbounded stream of ProcessingTimeEvents and WatermarkEvents. | |
| The returned events will be paired with the TimingInfo. This loop's only | |
| purpose is to generate these events even when there are no elements. | |
| """ | |
| next_sample_time = (timestamp.micros * 1e-6) + self._sample_resolution_sec | |
| timing_sampler.set(next_sample_time) | |
| # Generate two events, the delta since the last sample and a place-holder | |
| # WatermarkEvent. This is a placeholder because we can't otherwise add the | |
| # watermark from the runner to the event. | |
| yield ProcessingTimeEvent(self._sample_resolution_sec) | |
| yield WatermarkEvent(MIN_TIMESTAMP) | |
| def process( | |
| self, | |
| e, | |
| timestamp=beam.DoFn.TimestampParam, | |
| window=beam.DoFn.WindowParam, | |
| timing_sampler=beam.DoFn.TimerParam(TIMING_SAMPLER), | |
| execute_once_state=beam.DoFn.StateParam(EXECUTE_ONCE_STATE)): | |
| _, (element, timing_info) = e | |
| # Only set the timers once and only send the header once. | |
| first_time = next(execute_once_state.read(), True) | |
| if first_time: | |
| # Generate the initial timing events. | |
| execute_once_state.add(False) | |
| now_sec = timing_info.processing_time.micros * 1e-6 | |
| timing_sampler.set(now_sec + self._sample_resolution_sec) | |
| # Here we capture the initial time offset and initial watermark. This is | |
| # where we emit the TestStreamFileHeader. | |
| yield beam_interactive_api_pb2.TestStreamFileHeader(tag=self._output_tag) | |
| yield ProcessingTimeEvent( | |
| Duration(micros=timing_info.processing_time.micros)) | |
| yield WatermarkEvent(MIN_TIMESTAMP) | |
| yield element | |
| class _TestStreamFormatter(beam.DoFn): | |
| """Formats the events to the specified output format. | |
| """ | |
| # In order to generate the processing time deltas, we need to keep track of | |
| # the previous clock time we got from the runner. | |
| PREV_SAMPLE_TIME_STATE = beam.transforms.userstate.BagStateSpec( | |
| name='prev_sample_time_state', coder=beam.coders.FastPrimitivesCoder()) | |
| def __init__(self, coder, output_format): | |
| self._coder = coder | |
| self._output_format = output_format | |
| def start_bundle(self): | |
| self.elements = [] | |
| self.timing_events = [] | |
| self.header = None | |
| def finish_bundle(self): | |
| """Outputs all the buffered elements. | |
| """ | |
| if self._output_format == OutputFormat.TEST_STREAM_EVENTS: | |
| return self._output_as_events() | |
| return self._output_as_records() | |
| def process( | |
| self, | |
| e, | |
| timestamp=beam.DoFn.TimestampParam, | |
| prev_sample_time_state=beam.DoFn.StateParam(PREV_SAMPLE_TIME_STATE)): | |
| """Buffers elements until the end of the bundle. | |
| This buffers elements instead of emitting them immediately to keep elements | |
| that come in the same bundle to be outputted in the same bundle. | |
| """ | |
| _, (element, timing_info) = e | |
| if isinstance(element, beam_interactive_api_pb2.TestStreamFileHeader): | |
| self.header = element | |
| elif isinstance(element, WatermarkEvent): | |
| # WatermarkEvents come in with a watermark of MIN_TIMESTAMP. Fill in the | |
| # correct watermark from the runner here. | |
| element.new_watermark = timing_info.watermark.micros | |
| if element not in self.timing_events: | |
| self.timing_events.append(element) | |
| elif isinstance(element, ProcessingTimeEvent): | |
| # Because the runner holds the clock, calculate the processing time delta | |
| # here. The TestStream may have faked out the clock, and thus the | |
| # delta calculated in the SDK with time.time() will be wrong. | |
| prev_sample = next(prev_sample_time_state.read(), Timestamp()) | |
| prev_sample_time_state.clear() | |
| prev_sample_time_state.add(timing_info.processing_time) | |
| advance_by = timing_info.processing_time - prev_sample | |
| element.advance_by = advance_by | |
| self.timing_events.append(element) | |
| else: | |
| self.elements.append(TimestampedValue(element, timestamp)) | |
| def _output_as_events(self): | |
| """Outputs buffered elements as TestStream events. | |
| """ | |
| if self.timing_events: | |
| yield WindowedValue( | |
| self.timing_events, timestamp=0, windows=[beam.window.GlobalWindow()]) | |
| if self.elements: | |
| yield WindowedValue([ElementEvent(self.elements)], | |
| timestamp=0, | |
| windows=[beam.window.GlobalWindow()]) | |
| def _output_as_records(self): | |
| """Outputs buffered elements as TestStreamFileRecords. | |
| """ | |
| if self.header: | |
| yield WindowedValue( | |
| self.header, timestamp=0, windows=[beam.window.GlobalWindow()]) | |
| if self.timing_events: | |
| timing_events = self._timing_events_to_records(self.timing_events) | |
| for r in timing_events: | |
| yield WindowedValue( | |
| r, timestamp=0, windows=[beam.window.GlobalWindow()]) | |
| if self.elements: | |
| elements = self._elements_to_record(self.elements) | |
| yield WindowedValue( | |
| elements, timestamp=0, windows=[beam.window.GlobalWindow()]) | |
| def _timing_events_to_records(self, timing_events): | |
| """Returns given timing_events as TestStreamFileRecords. | |
| """ | |
| records = [] | |
| for e in self.timing_events: | |
| if isinstance(e, ProcessingTimeEvent): | |
| processing_time_event = beam_runner_api_pb2.\ | |
| TestStreamPayload.Event.AdvanceProcessingTime( | |
| advance_duration=e.advance_by.micros) | |
| records.append( | |
| beam_interactive_api_pb2.TestStreamFileRecord( | |
| recorded_event=beam_runner_api_pb2.TestStreamPayload.Event( | |
| processing_time_event=processing_time_event))) | |
| elif isinstance(e, WatermarkEvent): | |
| watermark_event = beam_runner_api_pb2.\ | |
| TestStreamPayload.Event.AdvanceWatermark( | |
| new_watermark=int(e.new_watermark)) | |
| records.append( | |
| beam_interactive_api_pb2.TestStreamFileRecord( | |
| recorded_event=beam_runner_api_pb2.TestStreamPayload.Event( | |
| watermark_event=watermark_event))) | |
| return records | |
| def _elements_to_record(self, elements): | |
| """Returns elements as TestStreamFileRecords. | |
| """ | |
| elements = [] | |
| for tv in self.elements: | |
| element_timestamp = tv.timestamp.micros | |
| element = beam_runner_api_pb2.TestStreamPayload.TimestampedElement( | |
| encoded_element=self._coder.encode(tv.value), | |
| timestamp=element_timestamp) | |
| elements.append(element) | |
| element_event = beam_runner_api_pb2.TestStreamPayload.Event.AddElements( | |
| elements=elements) | |
| return beam_interactive_api_pb2.TestStreamFileRecord( | |
| recorded_event=beam_runner_api_pb2.TestStreamPayload.Event( | |
| element_event=element_event)) |