Skip to content

Commit 3b1db11

Browse files
committed
Handle after-processing-time trigger with processing-time timer.
1 parent abdec1b commit 3b1db11

3 files changed

Lines changed: 204 additions & 15 deletions

File tree

sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go

Lines changed: 160 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,7 +1215,9 @@ type stageKind interface {
12151215
// buildEventTimeBundle handles building bundles for the stage per it's kind.
12161216
buildEventTimeBundle(ss *stageState, watermark mtime.Time) (toProcess elementHeap, minTs mtime.Time, newKeys set[string],
12171217
holdsInBundle map[mtime.Time]int, panesInBundle []bundlePane, schedulable bool, pendingAdjustment int)
1218-
1218+
// buildProcessingTimeBundle handles building processing-time bundles for the stage per it's kind.
1219+
buildProcessingTimeBundle(ss *stageState, em *ElementManager, emNow mtime.Time) (toProcess elementHeap, minTs mtime.Time, newKeys set[string],
1220+
holdsInBundle map[mtime.Time]int, schedulable bool)
12191221
// getPaneOrDefault based on the stage state, element metadata, and bundle id.
12201222
getPaneOrDefault(ss *stageState, defaultPane typex.PaneInfo, w typex.Window, keyBytes []byte, bundID string) typex.PaneInfo
12211223
}
@@ -1327,17 +1329,54 @@ func (ss *stageState) injectTriggeredBundlesIfReady(em *ElementManager, window t
13271329
ready := ss.strat.IsTriggerReady(triggerInput{
13281330
newElementCount: 1,
13291331
endOfWindowReached: endOfWindowReached,
1332+
emNow: em.ProcessingTimeNow(),
13301333
}, &state)
13311334

13321335
if ready {
13331336
state.Pane = computeNextTriggeredPane(state.Pane, endOfWindowReached)
1337+
} else {
1338+
if pts := ss.strat.GetAfterProcessingTimeTriggers(); pts != nil {
1339+
for _, t := range pts {
1340+
ts := (&state).getTriggerState(t)
1341+
if ts.extra == nil || t.shouldFire((&state)) {
1342+
// Skipping inserting a processing time timer if the firing time
1343+
// is not set or it already should fire.
1344+
// When the after processing time triggers should fire, there are
1345+
// two scenarios:
1346+
// (1) the entire trigger of this window is ready to fire. In this
1347+
// case, `ready` should be true and we won't reach here.
1348+
// (2) we are still waiting for other triggers (subtriggers) to
1349+
// fire (e.g. AfterAll).
1350+
continue
1351+
}
1352+
firingTime := ts.extra.(afterProcessingTimeState).firingTime
1353+
notYetHolds := map[mtime.Time]int{}
1354+
timer := element{
1355+
window: window,
1356+
timestamp: firingTime,
1357+
holdTimestamp: window.MaxTimestamp(),
1358+
pane: typex.NoFiringPane(),
1359+
transform: ss.ID, // Use stage id to fake transform id
1360+
family: "AfterProcessingTime",
1361+
tag: "",
1362+
sequence: 1,
1363+
elmBytes: nil,
1364+
keyBytes: []byte(key),
1365+
}
1366+
// TODO: how to deal with watermark holds for this implicit processing time timer
1367+
// ss.watermarkHolds.Add(timer.holdTimestamp, 1)
1368+
ss.processingTimeTimers.Persist(firingTime, timer, notYetHolds)
1369+
em.processTimeEvents.Schedule(firingTime, ss.ID)
1370+
em.wakeUpAt(firingTime)
1371+
}
1372+
}
13341373
}
13351374
// Store the state as triggers may have changed it.
13361375
ss.state[LinkID{}][window][key] = state
13371376

13381377
// If we're ready, it's time to fire!
13391378
if ready {
1340-
count += ss.buildTriggeredBundle(em, key, window)
1379+
count += ss.startTriggeredBundle(em, key, window)
13411380
}
13421381
return count
13431382
}
@@ -1524,16 +1563,11 @@ func (ss *stageState) savePanes(bundID string, panesInBundle []bundlePane) {
15241563
}
15251564
}
15261565

1527-
// buildTriggeredBundle must be called with the stage.mu lock held.
1528-
// When in discarding mode, returns 0.
1529-
// When in accumulating mode, returns the number of fired elements to maintain a correct pending count.
1530-
func (ss *stageState) buildTriggeredBundle(em *ElementManager, key string, win typex.Window) int {
1566+
func (ss *stageState) buildTriggeredBundle(em *ElementManager, key string, win typex.Window) ([]element, int) {
15311567
var toProcess []element
15321568
dnt := ss.pendingByKeys[key]
15331569
var notYet []element
15341570

1535-
rb := RunBundle{StageID: ss.ID, BundleID: "agg-" + em.nextBundID(), Watermark: ss.input}
1536-
15371571
// Look at all elements for this key, and only for this window.
15381572
for dnt.elements.Len() > 0 {
15391573
e := heap.Pop(&dnt.elements).(element)
@@ -1564,6 +1598,18 @@ func (ss *stageState) buildTriggeredBundle(em *ElementManager, key string, win t
15641598
heap.Init(&dnt.elements)
15651599
}
15661600

1601+
return toProcess, accumulationDiff
1602+
}
1603+
1604+
// startTriggeredBundle must be called with the stage.mu lock held.
1605+
// When in discarding mode, returns 0.
1606+
// When in accumulating mode, returns the number of fired elements to maintain a correct pending count.
1607+
func (ss *stageState) startTriggeredBundle(em *ElementManager, key string, win typex.Window) int {
1608+
toProcess, accumulationDiff := ss.buildTriggeredBundle(em, key, win)
1609+
if len(toProcess) == 0 {
1610+
return accumulationDiff
1611+
}
1612+
15671613
if ss.inprogressKeys == nil {
15681614
ss.inprogressKeys = set[string]{}
15691615
}
@@ -1575,6 +1621,7 @@ func (ss *stageState) buildTriggeredBundle(em *ElementManager, key string, win t
15751621
},
15761622
}
15771623

1624+
rb := RunBundle{StageID: ss.ID, BundleID: "agg-" + em.nextBundID(), Watermark: ss.input}
15781625
ss.makeInProgressBundle(
15791626
func() string { return rb.BundleID },
15801627
toProcess,
@@ -1927,6 +1974,18 @@ func (ss *stageState) startProcessingTimeBundle(em *ElementManager, emNow mtime.
19271974
ss.mu.Lock()
19281975
defer ss.mu.Unlock()
19291976

1977+
toProcess, minTs, newKeys, holdsInBundle, stillSchedulable := ss.kind.buildProcessingTimeBundle(ss, em, emNow)
1978+
1979+
if len(toProcess) == 0 {
1980+
// If we have nothing
1981+
return "", false, stillSchedulable
1982+
}
1983+
bundID := ss.makeInProgressBundle(genBundID, toProcess, minTs, newKeys, holdsInBundle, nil)
1984+
slog.Debug("started a processing time bundle", "stageID", ss.ID, "bundleID", bundID, "size", len(toProcess), "emNow", emNow)
1985+
return bundID, true, stillSchedulable
1986+
}
1987+
1988+
func (*statefulStageKind) buildProcessingTimeBundle(ss *stageState, em *ElementManager, emNow mtime.Time) (elementHeap, mtime.Time, set[string], map[mtime.Time]int, bool) {
19301989
// TODO: Determine if it's possible and a good idea to treat all EventTime processing as a MinTime
19311990
// Special Case for ProcessingTime handling.
19321991
// Eg. Always queue EventTime elements at minTime.
@@ -1986,19 +2045,92 @@ func (ss *stageState) startProcessingTimeBundle(em *ElementManager, emNow mtime.
19862045
for _, v := range notYet {
19872046
ss.processingTimeTimers.Persist(v.firing, v.timer, notYetHolds)
19882047
em.processTimeEvents.Schedule(v.firing, ss.ID)
2048+
em.wakeUpAt(v.firing)
19892049
}
19902050

19912051
// Add a refresh if there are still processing time events to process.
19922052
stillSchedulable := (nextTime < emNow && nextTime != mtime.MaxTimestamp || len(notYet) > 0)
19932053

1994-
if len(toProcess) == 0 {
1995-
// If we have nothing
1996-
return "", false, stillSchedulable
2054+
return toProcess, minTs, newKeys, holdsInBundle, stillSchedulable
2055+
}
2056+
2057+
func (*ordinaryStageKind) buildProcessingTimeBundle(ss *stageState, em *ElementManager, emNow mtime.Time) (elementHeap, mtime.Time, set[string], map[mtime.Time]int, bool) {
2058+
slog.Error("ordinary stages can't have processing time elements")
2059+
return nil, mtime.MinTimestamp, nil, nil, false
2060+
}
2061+
2062+
func (*aggregateStageKind) buildProcessingTimeBundle(ss *stageState, em *ElementManager, emNow mtime.Time) (elementHeap, mtime.Time, set[string], map[mtime.Time]int, bool) {
2063+
var toProcess []element
2064+
minTs := mtime.MaxTimestamp
2065+
holdsInBundle := map[mtime.Time]int{}
2066+
2067+
var notYet []fireElement
2068+
2069+
nextTime := ss.processingTimeTimers.Peek()
2070+
keyCounts := map[string]int{}
2071+
newKeys := set[string]{}
2072+
2073+
for nextTime <= emNow {
2074+
elems := ss.processingTimeTimers.FireAt(nextTime)
2075+
for _, e := range elems {
2076+
// Check if we're already executing this timer's key.
2077+
if ss.inprogressKeys.present(string(e.keyBytes)) {
2078+
notYet = append(notYet, fireElement{firing: nextTime, timer: e})
2079+
continue
2080+
}
2081+
2082+
// If we are set to have OneKeyPerBundle, and we already have a key for this bundle, we process it later.
2083+
if len(keyCounts) > 0 && OneKeyPerBundle {
2084+
notYet = append(notYet, fireElement{firing: nextTime, timer: e})
2085+
continue
2086+
}
2087+
// If we are set to have OneElementPerKey, and we already have an element for this key we set this to process later.
2088+
if v := keyCounts[string(e.keyBytes)]; v > 0 && OneElementPerKey {
2089+
notYet = append(notYet, fireElement{firing: nextTime, timer: e})
2090+
continue
2091+
}
2092+
keyCounts[string(e.keyBytes)]++
2093+
newKeys.insert(string(e.keyBytes))
2094+
if e.timestamp < minTs {
2095+
minTs = e.timestamp
2096+
}
2097+
// TODO: how to deal with watermark holds for this implicit processing time timer
2098+
// holdsInBundle[e.holdTimestamp]++
2099+
2100+
state := ss.state[LinkID{}][e.window][string(e.keyBytes)]
2101+
endOfWindowReached := e.window.MaxTimestamp() < ss.input
2102+
ready := ss.strat.IsTriggerReady(triggerInput{
2103+
newElementCount: 0,
2104+
endOfWindowReached: endOfWindowReached,
2105+
emNow: emNow,
2106+
}, &state)
2107+
2108+
if ready {
2109+
// We're going to process this trigger!
2110+
elems, _ := ss.buildTriggeredBundle(em, string(e.keyBytes), e.window)
2111+
toProcess = append(toProcess, elems...)
2112+
}
2113+
}
2114+
2115+
nextTime = ss.processingTimeTimers.Peek()
2116+
if nextTime == mtime.MaxTimestamp {
2117+
// Escape the loop if there are no more events.
2118+
break
2119+
}
19972120
}
1998-
bundID := ss.makeInProgressBundle(genBundID, toProcess, minTs, newKeys, holdsInBundle, nil)
19992121

2000-
slog.Debug("started a processing time bundle", "stageID", ss.ID, "bundleID", bundID, "size", len(toProcess), "emNow", emNow)
2001-
return bundID, true, stillSchedulable
2122+
// Reschedule unfired timers.
2123+
notYetHolds := map[mtime.Time]int{}
2124+
for _, v := range notYet {
2125+
ss.processingTimeTimers.Persist(v.firing, v.timer, notYetHolds)
2126+
em.processTimeEvents.Schedule(v.firing, ss.ID)
2127+
em.wakeUpAt(v.firing)
2128+
}
2129+
2130+
// Add a refresh if there are still processing time events to process.
2131+
stillSchedulable := (nextTime < emNow && nextTime != mtime.MaxTimestamp || len(notYet) > 0)
2132+
2133+
return toProcess, minTs, newKeys, holdsInBundle, stillSchedulable
20022134
}
20032135

20042136
// makeInProgressBundle is common code to store a set of elements as a bundle in progress.
@@ -2329,3 +2461,17 @@ func (em *ElementManager) ProcessingTimeNow() (ret mtime.Time) {
23292461
func rebaseProcessingTime(localNow, scheduled mtime.Time) mtime.Time {
23302462
return localNow + (scheduled - mtime.Now())
23312463
}
2464+
2465+
// wakeUpAt schedules a wakeup signal for the bundle processing loop.
2466+
// This is used for processing time timers to ensure the loop re-evaluates
2467+
// stages when a processing time timer is expected to fire.
2468+
func (em *ElementManager) wakeUpAt(t mtime.Time) {
2469+
if em.testStreamHandler == nil && em.config.EnableRTC {
2470+
// only create this goroutine if we have real-time clock enabled and the pipeline does not have TestStream.
2471+
go func(fireAt time.Time) {
2472+
time.AfterFunc(time.Until(fireAt), func() {
2473+
em.refreshCond.Broadcast()
2474+
})
2475+
}(t.ToTime())
2476+
}
2477+
}

sdks/go/pkg/beam/runners/prism/internal/engine/strategy.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,49 @@ func (ws WinStrat) IsNeverTrigger() bool {
7373
return ok
7474
}
7575

76+
func getAfterProcessingTimeTriggers(t Trigger) []*TriggerAfterProcessingTime {
77+
if t == nil {
78+
return nil
79+
}
80+
var triggers []*TriggerAfterProcessingTime
81+
switch at := t.(type) {
82+
case *TriggerAfterProcessingTime:
83+
return []*TriggerAfterProcessingTime{at}
84+
case *TriggerAfterAll:
85+
for _, st := range at.SubTriggers {
86+
triggers = append(triggers, getAfterProcessingTimeTriggers(st)...)
87+
}
88+
return triggers
89+
case *TriggerAfterAny:
90+
for _, st := range at.SubTriggers {
91+
triggers = append(triggers, getAfterProcessingTimeTriggers(st)...)
92+
}
93+
return triggers
94+
case *TriggerAfterEach:
95+
for _, st := range at.SubTriggers {
96+
triggers = append(triggers, getAfterProcessingTimeTriggers(st)...)
97+
}
98+
return triggers
99+
case *TriggerAfterEndOfWindow:
100+
triggers = append(triggers, getAfterProcessingTimeTriggers(at.Early)...)
101+
triggers = append(triggers, getAfterProcessingTimeTriggers(at.Late)...)
102+
return triggers
103+
case *TriggerOrFinally:
104+
triggers = append(triggers, getAfterProcessingTimeTriggers(at.Main)...)
105+
triggers = append(triggers, getAfterProcessingTimeTriggers(at.Finally)...)
106+
return triggers
107+
case *TriggerRepeatedly:
108+
return getAfterProcessingTimeTriggers(at.Repeated)
109+
default:
110+
return nil
111+
}
112+
}
113+
114+
// GetAfterProcessingTimeTriggers returns all AfterProcessingTime triggers within the trigger.
115+
func (ws WinStrat) GetAfterProcessingTimeTriggers() []*TriggerAfterProcessingTime {
116+
return getAfterProcessingTimeTriggers(ws.Trigger)
117+
}
118+
76119
func (ws WinStrat) String() string {
77120
return fmt.Sprintf("WinStrat[AllowedLateness:%v Trigger:%v]", ws.AllowedLateness, ws.Trigger)
78121
}

sdks/go/pkg/beam/runners/prism/internal/jobservices/management.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func (s *Server) Prepare(ctx context.Context, req *jobpb.PrepareJobRequest) (_ *
316316
func hasUnsupportedTriggers(tpb *pipepb.Trigger) bool {
317317
unsupported := false
318318
switch at := tpb.GetTrigger().(type) {
319-
case *pipepb.Trigger_AfterProcessingTime_, *pipepb.Trigger_AfterSynchronizedProcessingTime_:
319+
case *pipepb.Trigger_AfterSynchronizedProcessingTime_:
320320
return true
321321
case *pipepb.Trigger_AfterAll_:
322322
for _, st := range at.AfterAll.GetSubtriggers() {

0 commit comments

Comments
 (0)