github-actions[bot] commented on code in PR #41075:
URL: https://github.com/apache/beam/pull/41075#discussion_r4504896829


##########
sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go:
##########
@@ -2069,126 +2159,138 @@ func (ss *stageState) timeSortedDataReadyLocked(ts, 
watermark mtime.Time) bool {
 // buildEventTimeBundle for aggregation stages, processes all elements that 
are within the watermark for completed windows.
 func (*aggregateStageKind) buildEventTimeBundle(ss *stageState, watermark 
mtime.Time) (toProcess elementHeap, _ mtime.Time, _ set[string], _ 
map[mtime.Time]int, panesInBundle []bundlePane, schedulable bool, 
pendingAdjustment int) {
        minTs := mtime.MaxTimestamp
-       // TODO: Allow configurable limit of keys per bundle, and elements per 
key to improve parallelism.
-       // TODO: when we do, we need to ensure that the stage remains 
schedualable for bundle execution, for remaining pending elements and keys.
-       // With the greedy approach, we don't need to since "new data" triggers 
a refresh, and so should completing processing of a bundle.
        newKeys := set[string]{}
-
        holdsInBundle := map[mtime.Time]int{}
-
        accumulatingPendingAdjustment := 0
+
+       candidateKeys := set[string]{}
+       for k := range ss.pendingByKeys {
+               candidateKeys.insert(k)
+       }
+       for k := range ss.pendingOnTime {
+               candidateKeys.insert(k)
+       }
+
 keysPerBundle:
-       for k, dnt := range ss.pendingByKeys {
+       for k := range candidateKeys {
                if ss.inprogressKeys.present(k) {
                        continue
                }
-               newKeys.insert(k)
-               // Track the min-timestamp for later watermark handling.
-               if dnt.elements[0].timestamp < minTs {
-                       minTs = dnt.elements[0].timestamp
-               }
 
-               var toProcessForKey []element
-               var toSkipForKey []element
-               // Can we pre-compute this bit when adding to pendingByKeys?
-               // startBundle is in run in a single scheduling goroutine, so 
moving per-element code
-               // to be computed by the bundle parallel goroutines will speed 
things up a touch.
-               for dnt.elements.Len() > 0 {
-                       // For aggregations, only include elements for this key
-                       // if we're after the end of window, or after the 
window expiry deadline.
-                       // We will only ever trigger aggregations by watermark 
at most twice, once the watermark passes the window ends for OnTime completion,
-                       // and once for when the window is closing.
-                       elm := heap.Pop(&dnt.elements).(element)
-                       shouldSkip := false
-
-                       if watermark <= elm.window.MaxTimestamp() {
-                               // The watermark hasn't passed the end of the 
window yet, we do nothing.
-                               shouldSkip = true
-                       }
-                       // Watermark is past the end of this window. Have we 
fired an OnTime pane yet?
-                       state := 
ss.state[LinkID{}][elm.window][string(elm.keyBytes)]
-                       // If this is not the ontime firing for this key.
-
-                       if !shouldSkip && state.Pane.Timing != typex.PaneEarly 
&& watermark <= ss.strat.EarliestCompletion(elm.window) {
-                               // The watermark is still before the earliest 
final completion for this window.
-                               // Do not add further data for this firing.
-                               // If this is the Never trigger, we also don't 
fire OnTime until after the earliest completion.
-                               shouldSkip = true
-                       }
-                       if !shouldSkip && ss.strat.IsNeverTrigger() && 
watermark <= ss.strat.EarliestCompletion(elm.window) {
-                               // The NeverTrigger only has a single firing at 
the end of window + allowed lateness.
-                               shouldSkip = true
+               var firingWindows []typex.Window
+               firingSet := set[typex.Window]{}
+               var newByWin map[typex.Window][]element
+
+               if dnt := ss.pendingByKeys[k]; dnt != nil {
+                       var toSkipForKey []element
+                       for dnt.elements.Len() > 0 {
+                               // Aggregate by watermark at most twice per 
window: once at window end
+                               // for OnTime, and once at window closing after 
allowed lateness.
+                               elm := heap.Pop(&dnt.elements).(element)
+                               shouldSkip := false
+
+                               if watermark <= elm.window.MaxTimestamp() {
+                                       shouldSkip = true
+                               }
+                               state := ss.state[LinkID{}][elm.window][k]
+                               onTimePassed := state.Pane.Timing != 
typex.PaneEarly || elm.window.MaxTimestamp() < ss.output
+                               if !shouldSkip && onTimePassed && watermark <= 
ss.strat.EarliestCompletion(elm.window) {
+                                       shouldSkip = true
+                               }
+                               if !shouldSkip && ss.strat.IsNeverTrigger() && 
watermark <= ss.strat.EarliestCompletion(elm.window) {
+                                       shouldSkip = true
+                               }
+
+                               if shouldSkip {
+                                       toSkipForKey = append(toSkipForKey, elm)
+                                       continue
+                               }
+                               if newByWin == nil {
+                                       newByWin = map[typex.Window][]element{}
+                               }
+                               newByWin[elm.window] = 
append(newByWin[elm.window], elm)
+                               if !firingSet.present(elm.window) {
+                                       firingSet.insert(elm.window)
+                                       firingWindows = append(firingWindows, 
elm.window)
+                               }
                        }
 
-                       if shouldSkip {
-                               toSkipForKey = append(toSkipForKey, elm)
-                       } else {
-                               toProcessForKey = append(toProcessForKey, elm)
+                       for _, elm := range toSkipForKey {
+                               heap.Push(&dnt.elements, elm)
+                       }
+                       if dnt.elements.Len() == 0 {
+                               delete(ss.pendingByKeys, k)
                        }
                }
 
-               // Re-add skipped elements to the heap.
-               // This ensures that `dnt.elements` reflects only the truly 
skipped elements for future processing.
-               for _, elm := range toSkipForKey {
-                       heap.Push(&dnt.elements, elm)
+               for win := range ss.pendingOnTime[k] {
+                       if watermark <= win.MaxTimestamp() {
+                               continue
+                       }
+                       if ss.strat.IsNeverTrigger() && watermark <= 
ss.strat.EarliestCompletion(win) {
+                               continue
+                       }
+                       if !firingSet.present(win) {
+                               firingSet.insert(win)
+                               firingWindows = append(firingWindows, win)
+                       }
                }
 
-               // Get the pane for the aggregation correct, only mutate it 
once per key and window.
-               handledWindows := set[typex.Window]{}
-               for _, elm := range toProcessForKey {
-                       state := 
ss.state[LinkID{}][elm.window][string(elm.keyBytes)]
-                       if handledWindows.present(elm.window) {
-                               // The pane is already correct for this key + 
window + firing.
-                               if ss.strat.Accumulating && !state.Pane.IsLast {
-                                       // If this isn't the last pane, then we 
must add the element back to the pending store for subsequent firings.
-                                       heap.Push(&dnt.elements, elm)
-                                       accumulatingPendingAdjustment++
-                               }
+               var toProcessForKey []element
+               for _, win := range firingWindows {
+                       ss.initAggStateLocked(win, k)
+                       state := ss.state[LinkID{}][win][k]
+                       removed := ss.takePendingOnTimeLocked(win, k, 
holdsInBundle)
+                       newElems := newByWin[win]
+                       priorCount := len(state.Accumulated)
+                       if priorCount+len(newElems) == 0 {
                                continue
                        }
-                       handledWindows.insert(elm.window)
 
-                       state.Pane = computeNextWatermarkPane(state.Pane)
-                       // Determine if this is the last pane.
-                       // Check if this is the post closing firing, which will 
be the last one.
-                       // Unless it's the ontime pane, at which point it can 
never be last.
-                       if watermark > ss.strat.EarliestCompletion(elm.window) 
&& state.Pane.Timing != typex.PaneOnTime {
+                       winElems := make([]element, 0, priorCount+len(newElems))
+                       winElems = append(winElems, state.Accumulated...)
+                       winElems = append(winElems, newElems...)
+
+                       state.Pane = computeNextWatermarkPane(state.Pane, 
win.MaxTimestamp() < ss.output)
+                       if watermark > ss.strat.EarliestCompletion(win) && 
state.Pane.Timing != typex.PaneOnTime {
                                state.Pane.IsLast = true
                        }
                        if ss.strat.AllowedLateness == 0 || 
ss.strat.IsNeverTrigger() {
-                               // If the allowed lateness is zero, then this 
will be the last pane.
-                               // If this is the NeverTrigger, it's the last 
pane.
                                state.Pane.IsLast = true
                        }
-                       ss.state[LinkID{}][elm.window][string(elm.keyBytes)] = 
state
+                       if ss.strat.Accumulating && !state.Pane.IsLast && 
watermark <= ss.strat.EarliestCompletion(win) {
+                               nextAcc := make([]element, len(winElems))
+                               copy(nextAcc, winElems)
+                               state.Accumulated = nextAcc
+                       } else {
+                               state.Accumulated = nil
+                       }
+                       ss.state[LinkID{}][win][k] = state
 
-                       // Save latest PaneInfo for this window + key pair. It 
will be used in PersistBundle.
                        panesInBundle = append(panesInBundle, bundlePane{
-                               win:  elm.window,
-                               key:  string(elm.keyBytes),
-                               pane: 
ss.state[LinkID{}][elm.window][string(elm.keyBytes)].Pane,
+                               win:  win,
+                               key:  k,
+                               pane: state.Pane,
                        })
-
-                       // The pane is already correct for this key + window + 
firing.
-                       if ss.strat.Accumulating && !state.Pane.IsLast {
-                               // If this isn't the last pane, then we must 
add the element back to the pending store for subsequent firings.
-                               heap.Push(&dnt.elements, elm)
-                               accumulatingPendingAdjustment++
-                       }
+                       accumulatingPendingAdjustment += priorCount - removed

Review Comment:
   **🔴 Critical**: `accumulatingPendingAdjustment` (returned as 
`pendingAdjustment` from `startEventTimeBundle` and 
`startProcessingTimeBundle`) can be negative, or non-zero even when 
`len(toProcess) == 0`.
   
   However, in `ElementManager.Bundles` (lines 487-489 and 512-514), 
`pendingAdjustment` is only applied when `ok && pendingAdjustment > 0`:
   ```go
   if ok {
        if pendingAdjustment > 0 {
                em.addPending(pendingAdjustment)
        }
   ```
   Consider what happens when an early pane fires in **discarding** mode (or 
when a trigger finishes before any `Accumulated` elements are retained) after 
`addPendingOnTimeLocked` added `+1` to `em.pendingElements`, or if 
`pendingOnTime` is taken with `priorCount == 0`:
   - `priorCount - removed` (or `priorCount + pendingOnTimeDelta` in 
`buildTriggeredBundle`) evaluates to `0 - 1 = -1`.
   - Because `Bundles()` checks `pendingAdjustment > 0`, the `-1` adjustment is 
dropped when the bundle starts, or if `ok == false`.
   - When the bundle finishes, `PersistBundle` calls 
`em.addPending(-len(completed.es))`.
   
   Even in the common accumulating case with `priorCount == 1` and `removed == 
1` (`accumulatingPendingAdjustment == 0`), if a single bundle processes **two** 
windows where one has `priorCount == 0, removed == 1` and or in 
`buildProcessingTimeBundle` where `endOfWindowReached` is true and `priorCount 
== 0, pendingOnTimeDelta == -1`, `pendingAdjustment` is `-1`. Dropping negative 
`pendingAdjustment` leaves `em.pendingElements` permanently off by 1 (causing 
the pipeline to hang at quiescence with `nothing in progress and no refreshes 
with non zero pending elements`), or in other cases causes `em.pendingElements` 
to hit zero early / panic with negative WaitGroup counter.
   
   `Bundles()` should apply any non-zero `pendingAdjustment != 0` regardless of 
sign (and even when `!ok` if `takePendingOnTimeLocked` removed an entry without 
producing elements).



##########
sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go:
##########
@@ -1732,68 +1810,80 @@ func (ss *stageState) savePanes(bundID string, 
panesInBundle []bundlePane) {
        }
 }
 
-func (ss *stageState) buildTriggeredBundle(em *ElementManager, key string, win 
typex.Window) ([]element, int) {
-       var toProcess []element
+func (ss *stageState) buildTriggeredBundle(key string, win typex.Window, 
endOfWindowReached bool, holdsInBundle map[mtime.Time]int) ([]element, int) {
        dnt := ss.pendingByKeys[key]
        if dnt == nil {
-               // If we set an after-processing-time trigger, but some other 
triggers fire or
-               // the end of window is reached before the first trigger could 
fire, then
-               // the pending elements are processed in other bundles, leaving 
a nil when
-               // we try to build this triggered bundle.
-               return toProcess, 0
+               return nil, 0
        }
+       var newElems []element
        var notYet []element
 
-       // Look at all elements for this key, and only for this window.
        for dnt.elements.Len() > 0 {
                e := heap.Pop(&dnt.elements).(element)
                if e.window != win {
                        notYet = append(notYet, e)
                        continue
                }
-               toProcess = append(toProcess, e)
-       }
-
-       // accumulationDiff adjusts the pending elements count to include
-       // the accumulated elements, which would be the new elements, but
-       // also all previous elements as well, which are duplicated at
-       // at every pane.
-       accumulationDiff := 0
-       if ss.strat.Accumulating {
-               // When accumulating, we need to retain all elements until the 
last pane firing.
-               for _, e := range toProcess {
-                       heap.Push(&dnt.elements, e)
-               }
-               accumulationDiff += len(toProcess)
+               newElems = append(newElems, e)
        }
        dnt.elements = append(dnt.elements, notYet...)
        if dnt.elements.Len() == 0 {
                delete(ss.pendingByKeys, key)
        } else {
-               // Ensure the heap invariants are maintained.
                heap.Init(&dnt.elements)
        }
+       if len(newElems) == 0 {
+               return nil, 0
+       }
+
+       state := ss.state[LinkID{}][win][key]
+       priorCount := len(state.Accumulated)
+       toProcess := make([]element, 0, priorCount+len(newElems))
+       toProcess = append(toProcess, state.Accumulated...)
+       toProcess = append(toProcess, newElems...)
+
+       triggerFinished := ss.isTriggerFinished(win, key)
+       pendingOnTimeDelta := 0
+       if ss.strat.Accumulating && !triggerFinished {
+               nextAcc := make([]element, len(toProcess))
+               copy(nextAcc, toProcess)
+               state.Accumulated = nextAcc
+               if !endOfWindowReached && !ss.strat.IsNeverTrigger() {

Review Comment:
   **🟡 Medium**: If a composite trigger such as `TriggerAfterAll` or 
`TriggerAfterEach` or `TriggerOrFinally` is used in **discarding** mode 
(`ss.strat.Accumulating == false`), an early firing does not call 
`addPendingOnTimeLocked`. However, if an early firing happens and no more 
elements arrive before the end of the window, Beam's trigger model still 
requires an empty `ON_TIME` pane at the watermark unless the trigger is 
finished (to produce the `ON_TIME` pane for the window). Conversely, if 
`addPendingOnTimeLocked` is only for accumulating mode here, note that when a 
later early firing finishes the trigger (`triggerFinished == true`) after an 
earlier firing added `pendingOnTime`, `takePendingOnTimeLocked` is called here: 
if `priorCount == 0`, `priorCount + pendingOnTimeDelta` becomes `-1`, which in 
`injectTriggeredBundlesIfReady` is added to `count` and returned to 
`PersistBundle` (`em.addPending(count)`), which works for `AddPending`, but in 
`buildProcessingTimeBundle` (line 24
 20) it is returned as `pendingAdjustment = -1` to `Bundles()`, where `if 
pendingAdjustment > 0` ignores it!



##########
sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go:
##########
@@ -2069,126 +2159,138 @@ func (ss *stageState) timeSortedDataReadyLocked(ts, 
watermark mtime.Time) bool {
 // buildEventTimeBundle for aggregation stages, processes all elements that 
are within the watermark for completed windows.
 func (*aggregateStageKind) buildEventTimeBundle(ss *stageState, watermark 
mtime.Time) (toProcess elementHeap, _ mtime.Time, _ set[string], _ 
map[mtime.Time]int, panesInBundle []bundlePane, schedulable bool, 
pendingAdjustment int) {
        minTs := mtime.MaxTimestamp
-       // TODO: Allow configurable limit of keys per bundle, and elements per 
key to improve parallelism.
-       // TODO: when we do, we need to ensure that the stage remains 
schedualable for bundle execution, for remaining pending elements and keys.
-       // With the greedy approach, we don't need to since "new data" triggers 
a refresh, and so should completing processing of a bundle.
        newKeys := set[string]{}
-
        holdsInBundle := map[mtime.Time]int{}
-
        accumulatingPendingAdjustment := 0
+
+       candidateKeys := set[string]{}
+       for k := range ss.pendingByKeys {
+               candidateKeys.insert(k)
+       }
+       for k := range ss.pendingOnTime {
+               candidateKeys.insert(k)
+       }
+
 keysPerBundle:
-       for k, dnt := range ss.pendingByKeys {
+       for k := range candidateKeys {
                if ss.inprogressKeys.present(k) {
                        continue
                }
-               newKeys.insert(k)
-               // Track the min-timestamp for later watermark handling.
-               if dnt.elements[0].timestamp < minTs {
-                       minTs = dnt.elements[0].timestamp
-               }
 
-               var toProcessForKey []element
-               var toSkipForKey []element
-               // Can we pre-compute this bit when adding to pendingByKeys?
-               // startBundle is in run in a single scheduling goroutine, so 
moving per-element code
-               // to be computed by the bundle parallel goroutines will speed 
things up a touch.
-               for dnt.elements.Len() > 0 {
-                       // For aggregations, only include elements for this key
-                       // if we're after the end of window, or after the 
window expiry deadline.
-                       // We will only ever trigger aggregations by watermark 
at most twice, once the watermark passes the window ends for OnTime completion,
-                       // and once for when the window is closing.
-                       elm := heap.Pop(&dnt.elements).(element)
-                       shouldSkip := false
-
-                       if watermark <= elm.window.MaxTimestamp() {
-                               // The watermark hasn't passed the end of the 
window yet, we do nothing.
-                               shouldSkip = true
-                       }
-                       // Watermark is past the end of this window. Have we 
fired an OnTime pane yet?
-                       state := 
ss.state[LinkID{}][elm.window][string(elm.keyBytes)]
-                       // If this is not the ontime firing for this key.
-
-                       if !shouldSkip && state.Pane.Timing != typex.PaneEarly 
&& watermark <= ss.strat.EarliestCompletion(elm.window) {
-                               // The watermark is still before the earliest 
final completion for this window.
-                               // Do not add further data for this firing.
-                               // If this is the Never trigger, we also don't 
fire OnTime until after the earliest completion.
-                               shouldSkip = true
-                       }
-                       if !shouldSkip && ss.strat.IsNeverTrigger() && 
watermark <= ss.strat.EarliestCompletion(elm.window) {
-                               // The NeverTrigger only has a single firing at 
the end of window + allowed lateness.
-                               shouldSkip = true
+               var firingWindows []typex.Window
+               firingSet := set[typex.Window]{}
+               var newByWin map[typex.Window][]element
+
+               if dnt := ss.pendingByKeys[k]; dnt != nil {
+                       var toSkipForKey []element
+                       for dnt.elements.Len() > 0 {
+                               // Aggregate by watermark at most twice per 
window: once at window end
+                               // for OnTime, and once at window closing after 
allowed lateness.
+                               elm := heap.Pop(&dnt.elements).(element)
+                               shouldSkip := false
+
+                               if watermark <= elm.window.MaxTimestamp() {
+                                       shouldSkip = true
+                               }
+                               state := ss.state[LinkID{}][elm.window][k]
+                               onTimePassed := state.Pane.Timing != 
typex.PaneEarly || elm.window.MaxTimestamp() < ss.output
+                               if !shouldSkip && onTimePassed && watermark <= 
ss.strat.EarliestCompletion(elm.window) {
+                                       shouldSkip = true
+                               }
+                               if !shouldSkip && ss.strat.IsNeverTrigger() && 
watermark <= ss.strat.EarliestCompletion(elm.window) {
+                                       shouldSkip = true
+                               }
+
+                               if shouldSkip {
+                                       toSkipForKey = append(toSkipForKey, elm)
+                                       continue
+                               }
+                               if newByWin == nil {
+                                       newByWin = map[typex.Window][]element{}
+                               }
+                               newByWin[elm.window] = 
append(newByWin[elm.window], elm)
+                               if !firingSet.present(elm.window) {
+                                       firingSet.insert(elm.window)
+                                       firingWindows = append(firingWindows, 
elm.window)
+                               }
                        }
 
-                       if shouldSkip {
-                               toSkipForKey = append(toSkipForKey, elm)
-                       } else {
-                               toProcessForKey = append(toProcessForKey, elm)
+                       for _, elm := range toSkipForKey {
+                               heap.Push(&dnt.elements, elm)
+                       }
+                       if dnt.elements.Len() == 0 {
+                               delete(ss.pendingByKeys, k)
                        }
                }
 
-               // Re-add skipped elements to the heap.
-               // This ensures that `dnt.elements` reflects only the truly 
skipped elements for future processing.
-               for _, elm := range toSkipForKey {
-                       heap.Push(&dnt.elements, elm)
+               for win := range ss.pendingOnTime[k] {
+                       if watermark <= win.MaxTimestamp() {
+                               continue
+                       }
+                       if ss.strat.IsNeverTrigger() && watermark <= 
ss.strat.EarliestCompletion(win) {
+                               continue
+                       }
+                       if !firingSet.present(win) {
+                               firingSet.insert(win)
+                               firingWindows = append(firingWindows, win)
+                       }
                }
 
-               // Get the pane for the aggregation correct, only mutate it 
once per key and window.
-               handledWindows := set[typex.Window]{}
-               for _, elm := range toProcessForKey {
-                       state := 
ss.state[LinkID{}][elm.window][string(elm.keyBytes)]
-                       if handledWindows.present(elm.window) {
-                               // The pane is already correct for this key + 
window + firing.
-                               if ss.strat.Accumulating && !state.Pane.IsLast {
-                                       // If this isn't the last pane, then we 
must add the element back to the pending store for subsequent firings.
-                                       heap.Push(&dnt.elements, elm)
-                                       accumulatingPendingAdjustment++
-                               }
+               var toProcessForKey []element
+               for _, win := range firingWindows {
+                       ss.initAggStateLocked(win, k)
+                       state := ss.state[LinkID{}][win][k]
+                       removed := ss.takePendingOnTimeLocked(win, k, 
holdsInBundle)
+                       newElems := newByWin[win]
+                       priorCount := len(state.Accumulated)
+                       if priorCount+len(newElems) == 0 {
                                continue
                        }
-                       handledWindows.insert(elm.window)
 
-                       state.Pane = computeNextWatermarkPane(state.Pane)
-                       // Determine if this is the last pane.
-                       // Check if this is the post closing firing, which will 
be the last one.
-                       // Unless it's the ontime pane, at which point it can 
never be last.
-                       if watermark > ss.strat.EarliestCompletion(elm.window) 
&& state.Pane.Timing != typex.PaneOnTime {
+                       winElems := make([]element, 0, priorCount+len(newElems))
+                       winElems = append(winElems, state.Accumulated...)
+                       winElems = append(winElems, newElems...)
+
+                       state.Pane = computeNextWatermarkPane(state.Pane, 
win.MaxTimestamp() < ss.output)

Review Comment:
   **🟠 High**: When `buildEventTimeBundle` consumes unemitted elements 
(`newElems`) from `ss.pendingByKeys[k]` for an `ON_TIME` or closing watermark 
firing, `state.Trigger` is never updated or reset for those consumed elements.
   
   For example, in `early_count2_nonempty_ontime_then_late_count1` (where 
`Early: ElementCount(2), Late: ElementCount(1)`):
   1. Element `3` arrives before the watermark advances. 
`injectTriggeredBundlesIfReady` runs `Early.onElement`, incrementing `Early`'s 
count to `1` (not ready) and setting `TriggerAfterEndOfWindow`'s `extra` to 
`false` (`endOfWindowReached == false`).
   2. Watermark advances past the window end (`20`). `buildEventTimeBundle` 
pops element `3` and emits the `ON_TIME` pane `[1, 2, 3]`, but never resets 
`Early` or transitions `TriggerAfterEndOfWindow`.
   3. What if the trigger was `Repeatedly(ElementCount(2))` or `AfterEach` or 
`OrFinally` instead of `AfterEndOfWindow`? Because `Early` / the subtrigger 
still has `count = 1` from element `3` (which was already emitted in the 
`ON_TIME` pane!), the very next late element `4` will see `count = 1 + 1 = 2` 
and fire immediately on a single late element even though `ElementCount(2)` 
required 2 new elements after the `ON_TIME` firing.



##########
sdks/go/pkg/beam/runners/prism/internal/handlerunner_test.go:
##########
@@ -16,13 +16,161 @@
 package internal
 
 import (
+       "context"
+       "fmt"
+       "os"
+       "slices"
+       "strings"
        "testing"
+       "time"
 
+       "github.com/apache/beam/sdks/v2/go/pkg/beam"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/window"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/window/trigger"
        pipepb "github.com/apache/beam/sdks/v2/go/pkg/beam/model/pipeline_v1"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/options/jobopts"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/register"
+       
"github.com/apache/beam/sdks/v2/go/pkg/beam/runners/prism/internal/jobservices"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/runners/universal"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/passert"
+       "github.com/apache/beam/sdks/v2/go/pkg/beam/testing/teststream"
        "github.com/google/go-cmp/cmp"
        "google.golang.org/protobuf/testing/protocmp"
 )
 
+func init() {
+       register.Function4x0(formatAccumPaneFn)
+       register.Iter1[int64]()
+       register.Emitter1[string]()
+}
+
+func formatAccumPaneFn(pn beam.PaneInfo, _ int, iter func(*int64) bool, emit 
func(string)) {
+       var vals []int64
+       var v int64
+       for iter(&v) {
+               vals = append(vals, v)
+       }
+       slices.Sort(vals)
+       emit(fmt.Sprintf("timing=%s index=%d nonSpec=%d first=%t last=%t 
vals=%v",
+               pn.Timing, pn.Index, pn.NonSpeculativeIndex, pn.IsFirst, 
pn.IsLast, vals))
+}
+
+func runLocalTestPipeline(t *testing.T, p *beam.Pipeline, runFn 
func(*jobservices.Job)) {
+       t.Helper()
+       prevEndpoint := *jobopts.Endpoint
+       prevEnv := *jobopts.EnvironmentType
+       prevBin := *jobopts.WorkerBinary
+       prevJob := *jobopts.JobName
+
+       s := jobservices.NewServer(0, runFn)
+       go s.Serve()
+
+       f, err := os.CreateTemp("", "prism-worker-dummy")
+       if err != nil {
+               s.Stop()
+               t.Fatal(err)
+       }

Review Comment:
   **🟢 Low**: The temporary file `f` created by `os.CreateTemp` is never closed 
before being removed in `t.Cleanup`, leaking a file descriptor for the duration 
of the test (and preventing removal on Windows if ever run there). Close `f` 
right after creation.
   
   ```suggestion
        _ = f.Close()
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to