Skip to content

Commit 56ca36f

Browse files
authored
Add a flag in PeriodicImpulse to rebase timestamp. (#35569)
* Add a flag in PeriodicImpulse to rebase timestamp. * Add a unit test. * Change the boolean flag to an enum. * Clarify the test that raises exception.
1 parent cd72a25 commit 56ca36f

2 files changed

Lines changed: 70 additions & 5 deletions

File tree

sdks/python/apache_beam/transforms/periodicsequence.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# limitations under the License.
1616
#
1717

18+
import enum
1819
import math
1920
import time
2021
import warnings
@@ -216,6 +217,21 @@ def expand(self, pcoll):
216217
| 'MapToTimestamped' >> beam.Map(lambda tt: TimestampedValue(tt, tt)))
217218

218219

220+
class RebaseMode(enum.Enum):
221+
'''Controls how the start and stop timestamps are rebased to execution time.
222+
223+
Attributes:
224+
REBASE_NONE: Timestamps are not changed.
225+
REBASE_ALL: Both start and stop timestamps are rebased, preserving the
226+
original duration.
227+
REBASE_START: Only the start timestamp is rebased; the stop timestamp
228+
is unchanged.
229+
'''
230+
REBASE_NONE = 0
231+
REBASE_ALL = 1
232+
REBASE_START = 2
233+
234+
219235
class PeriodicImpulse(PTransform):
220236
'''
221237
PeriodicImpulse transform generates an infinite sequence of elements with
@@ -270,7 +286,8 @@ def __init__(
270286
stop_timestamp: TimestampTypes = MAX_TIMESTAMP,
271287
fire_interval: float = 360.0,
272288
apply_windowing: bool = False,
273-
data: Optional[Sequence[Any]] = None):
289+
data: Optional[Sequence[Any]] = None,
290+
rebase: RebaseMode = RebaseMode.REBASE_NONE):
274291
'''
275292
:param start_timestamp: Timestamp for first element.
276293
:param stop_timestamp: Timestamp at or after which no elements will be
@@ -301,21 +318,36 @@ def __init__(
301318
sufficient to cover the duration defined by `start_timestamp`,
302319
`stop_timestamp`, and `fire_interval`; otherwise, a `ValueError` is
303320
raised.
321+
322+
:param rebase: Controls how the start and stop timestamps are rebased to
323+
execution time. See `RebaseMode` for more details. Defaults to
324+
`REBASE_NONE`.
304325
'''
305326
self.start_ts = start_timestamp
306327
self.stop_ts = stop_timestamp
307328
self.interval = fire_interval
308329
self.apply_windowing = apply_windowing
309330
self.data = data
331+
self.rebase = rebase
310332

311333
if self.data:
312334
self._validate_and_adjust_duration()
313335

314336
def expand(self, pbegin):
337+
if self.rebase == RebaseMode.REBASE_ALL:
338+
duration = Timestamp.of(self.stop_ts) - Timestamp.of(self.start_ts)
339+
impulse_element = pbegin | beam.Impulse() | beam.Map(
340+
lambda _:
341+
[Timestamp.now(), Timestamp.now() + duration, self.interval])
342+
elif self.rebase == RebaseMode.REBASE_START:
343+
impulse_element = pbegin | beam.Impulse() | beam.Map(
344+
lambda _: [Timestamp.now(), self.stop_ts, self.interval])
345+
else:
346+
impulse_element = pbegin | 'ImpulseElement' >> beam.Create(
347+
[(self.start_ts, self.stop_ts, self.interval)])
348+
315349
result = (
316-
pbegin
317-
| 'ImpulseElement' >> beam.Create(
318-
[(self.start_ts, self.stop_ts, self.interval)])
350+
impulse_element
319351
| 'GenSequence' >> beam.ParDo(ImpulseSeqGenDoFn(self.data)))
320352

321353
if not self.data:

sdks/python/apache_beam/transforms/periodicsequence_test.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@
3232
from apache_beam.testing.test_pipeline import TestPipeline
3333
from apache_beam.testing.util import assert_that
3434
from apache_beam.testing.util import equal_to
35+
from apache_beam.testing.util import is_empty
3536
from apache_beam.transforms import trigger
3637
from apache_beam.transforms import window
3738
from apache_beam.transforms.periodicsequence import PeriodicImpulse
3839
from apache_beam.transforms.periodicsequence import PeriodicSequence
40+
from apache_beam.transforms.periodicsequence import RebaseMode
3941
from apache_beam.transforms.periodicsequence import _sequence_backlog_bytes
4042
from apache_beam.transforms.window import FixedWindows
4143
from apache_beam.utils.timestamp import Timestamp
@@ -285,7 +287,6 @@ def test_fuzzy_length_at_minimal_interval(self):
285287
times = 30
286288
for _ in range(times):
287289
seed = int(time.time() * 1000)
288-
seed = 1751135957975
289290
random.seed(seed)
290291
n = int(random.randint(1, 100))
291292
data = list(range(n))
@@ -334,6 +335,38 @@ def test_timestamp_type_input(self):
334335
assert_that(
335336
ret, equal_to(expected, lambda x, y: type(x) is type(y) and x == y))
336337

338+
def test_rebase_timestamp(self):
339+
class CheckTimeStamp(beam.DoFn):
340+
def process(self, elem):
341+
ts = Timestamp.of(elem)
342+
now = Timestamp.now()
343+
# When rebase is enabled, the timestamp should be closer to now than the
344+
# original start.
345+
if (ts - Timestamp.of(1)) < (now - ts):
346+
yield "wrong"
347+
348+
with TestPipeline() as p:
349+
ret = (
350+
p | PeriodicImpulse(
351+
start_timestamp=Timestamp.of(1),
352+
stop_timestamp=Timestamp.of(5),
353+
fire_interval=1,
354+
rebase=RebaseMode.REBASE_ALL)
355+
| beam.ParDo(CheckTimeStamp()))
356+
assert_that(ret, is_empty())
357+
358+
def test_rebase_timestamp_with_wrong_setting(self):
359+
with self.assertRaises(Exception):
360+
# exception is raised because start_timestamp is rebased to the pipeline
361+
# execution time, but the stop_timestamp is 5 seconds after unix epoch.
362+
with TestPipeline() as p:
363+
_ = (
364+
p | PeriodicImpulse(
365+
start_timestamp=Timestamp.of(1),
366+
stop_timestamp=Timestamp.of(5),
367+
fire_interval=1,
368+
rebase=RebaseMode.REBASE_START))
369+
337370

338371
if __name__ == '__main__':
339372
unittest.main()

0 commit comments

Comments
 (0)