This is an automated email from the ASF dual-hosted git repository.
Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 3b9e647dec1 [Prism] Schedule consumers of a self checkpointing source
(#39572)
3b9e647dec1 is described below
commit 3b9e647dec1e56725b21a90103f9cd7b5711c76c
Author: Elia Liu <[email protected]>
AuthorDate: Sat Aug 22 00:24:16 2026 +1000
[Prism] Schedule consumers of a self checkpointing source (#39572)
* [Prism] Schedule consumers of a self checkpointing source
Fixes #39446.
An unbounded source SDF that returns a process continuation residual has
that residual re-queued as a pending element carrying the input element's
event time, MinTimestamp for an Impulse rooted source, so the stage's
watermark never advances. updateWatermarks returns no refreshes when the
output watermark does not move, and PersistBundle marked only the
producing stage as changed, so a consumer that was just handed pending
elements was never surfaced to the scheduler. Its elements accumulated
forever. checkForQuiescence cannot catch this, because the source stays
schedulable, so the job live locks instead of failing fast.
PersistBundle now records the consumers that accepted data, and
updateWatermarks hands them to the scheduler on the path where the output
watermark does not advance. Recording is gated on any bundle having
returned a residual, so a pipeline that never self checkpoints keeps its
previous bundle scheduling.
bundleReady additionally lets a stateful stage with no side inputs run on
pending data alone, under the same gate, since
statefulStageKind.buildEventTimeBundle takes data at any watermark and
gates only timers. Its stillSchedulable now requires buildable work, and
a key that supplies nothing no longer consumes the OneKeyPerBundle slot,
is not marked in progress, and does not hold the bundle's minimum
timestamp.
A consumer that reads a side input still waits on the watermark, since
side input readiness is derived from it.
* Publish all JmsIO records after the pipeline starts
The test pre-published part of the records before running the pipeline
to work around the starvation this change fixes. All records are now
published after the pipeline starts, and the duplicate-tolerant
assertion tightens back to an exact count. Also bumps the messaging
postcommit trigger file.
* Apply suggestion from @Abacn
---------
Co-authored-by: Yi Hu <[email protected]>
---
...m_PostCommit_Python_Xlang_Messaging_Direct.json | 2 +-
.../prism/internal/engine/elementmanager.go | 91 ++++++-
.../engine/elementmanager_continuation_test.go | 299 +++++++++++++++++++++
.../apache_beam/io/external/xlang_jmsio_it_test.py | 13 +-
4 files changed, 381 insertions(+), 24 deletions(-)
diff --git
a/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json
b/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json
index d6a91b7e2e8..38ae1cf6822 100644
--- a/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json
+++ b/.github/trigger_files/beam_PostCommit_Python_Xlang_Messaging_Direct.json
@@ -1,4 +1,4 @@
{
"comment": "Modify this file in a trivial way to cause this test suite to
run",
- "modification": 7
+ "modification": 8
}
diff --git a/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go
b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go
index 2b502c679db..7ebf174d9ec 100644
--- a/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go
+++ b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager.go
@@ -235,6 +235,10 @@ type ElementManager struct {
livePending atomic.Int64 // An accessible live pending count.
DEBUG USE ONLY
pendingElements sync.WaitGroup // pendingElements counts all
unprocessed elements in a job. Jobs with no pending elements terminate
successfully.
+ // Latched once any bundle returns a residual, after which a stage
watermark
+ // can be pinned indefinitely and can't be relied on to schedule
consumers.
+ sawResidual atomic.Bool
+
processTimeEvents *stageRefreshQueue // Manages sequence of stage
updates when interfacing with processing time. Callers must hold refreshCond.L
lock.
testStreamHandler *testStreamHandler // Optional test stream handler
when a test stream is in the pipeline.
}
@@ -841,6 +845,17 @@ func reElementResiduals(residuals []Residual, inputInfo
PColInfo, rb RunBundle)
// input elements, and the committed output elements.
func (em *ElementManager) PersistBundle(rb RunBundle, col2Coders
map[string]PColInfo, d TentativeData, inputInfo PColInfo, residuals Residuals) {
stage := em.stages[rb.StageID]
+ // Consumers that received data from this bundle, recorded so they can
still
+ // be scheduled when this stage's output watermark is held back. Only
needed
+ // once something self checkpoints, so pipelines that never do keep
their
+ // previous bundle scheduling exactly.
+ if len(residuals.Data) > 0 {
+ em.sawResidual.Store(true)
+ }
+ var changedConsumers set[string]
+ if em.sawResidual.Load() {
+ changedConsumers = set[string]{}
+ }
var seq int
for output, data := range d.Raw {
info := col2Coders[output]
@@ -910,6 +925,9 @@ func (em *ElementManager) PersistBundle(rb RunBundle,
col2Coders map[string]PCol
count = consumer.AddPending(em, newPending)
}
em.addPending(count)
+ if changedConsumers != nil && count > 0 {
+ changedConsumers.insert(sID)
+ }
}
for _, link := range sideConsumers {
consumer := em.stages[link.Global]
@@ -942,6 +960,12 @@ func (em *ElementManager) PersistBundle(rb RunBundle,
col2Coders map[string]PCol
// even if a panic occurs during `em.addPending`. This prevents
potential deadlocks
// if the waitgroup unexpectedly drops below zero due to a
runner bug.
defer stage.mu.Unlock()
+ if len(changedConsumers) > 0 {
+ if stage.consumersWithNewData == nil {
+ stage.consumersWithNewData = set[string]{}
+ }
+ stage.consumersWithNewData.merge(changedConsumers)
+ }
completed := stage.inprogress[rb.BundleID]
em.addPending(-len(completed.es))
delete(stage.inprogress, rb.BundleID)
@@ -1101,6 +1125,9 @@ func (em *ElementManager) ReturnResiduals(rb RunBundle,
firstRsIndex int, inputI
stage := em.stages[rb.StageID]
stage.splitBundle(rb, firstRsIndex, em)
+ if len(residuals.Data) > 0 {
+ em.sawResidual.Store(true)
+ }
unprocessedElements := reElementResiduals(residuals.Data, inputInfo, rb)
if len(unprocessedElements) > 0 {
slog.Debug("ReturnResiduals: unprocessed elements", "bundle",
rb, "count", len(unprocessedElements))
@@ -1211,6 +1238,9 @@ type stageState struct {
estimatedOutput mtime.Time // Estimated watermark output from DoFns
previousInput mtime.Time // input watermark before the latest
watermark refresh
+ // Consumers handed data by a bundle, pending delivery to the scheduler.
+ consumersWithNewData set[string]
+
pending elementHeap // pending input
elements for this stage that are to be processesd
inprogress map[string]elements // inprogress elements
by active bundles, keyed by bundle
sideInputs map[LinkID]map[typex.Window][][]byte // side input data for
this stage, from {tid, inputID} -> window
@@ -1822,12 +1852,6 @@ keysPerBundle:
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
- }
-
dataInBundle := false
var toProcessForKey []element
@@ -1874,22 +1898,51 @@ keysPerBundle:
break
}
}
+ if len(toProcessForKey) > 0 {
+ newKeys.insert(k)
+ // Track the min-timestamp for later watermark
handling. Elements pop
+ // in timestamp order, so the first selected one is the
earliest.
+ if ts := toProcessForKey[0].timestamp; ts < minTs {
+ minTs = ts
+ }
+ }
toProcess = append(toProcess, toProcessForKey...)
if dnt.elements.Len() == 0 {
delete(ss.pendingByKeys, k)
}
- if OneKeyPerBundle {
+ // A key that yielded nothing, such as one headed by a timer
above the
+ // watermark, must not consume the single key slot, or the
bundle is empty
+ // and the stage keeps rescheduling on it.
+ if OneKeyPerBundle && len(toProcessForKey) > 0 {
break keysPerBundle
}
}
- // If we're out of data, and timers were not cleared then the watermark
is accurate.
- stillSchedulable := !(len(ss.pendingByKeys) == 0 && !timerCleared)
+ // Reschedule only when a later bundle could build something, or a
cleared
+ // timer may have held back the minimum pending timestamp.
+ stillSchedulable := timerCleared || ss.hasBuildableDataLocked(watermark)
return toProcess, minTs, newKeys, holdsInBundle, nil, stillSchedulable, 0
}
+// hasBuildableDataLocked reports whether a key that isn't in progress heads
its
+// heap with data, or with a timer the watermark has reached. Callers hold
ss.mu.
+func (ss *stageState) hasBuildableDataLocked(watermark mtime.Time) bool {
+ for k, dnt := range ss.pendingByKeys {
+ if ss.inprogressKeys.present(k) {
+ continue
+ }
+ if dnt.elements.Len() == 0 {
+ continue
+ }
+ if e := dnt.elements[0]; e.IsData() || e.timestamp <= watermark
{
+ return true
+ }
+ }
+ return false
+}
+
// 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
@@ -2286,9 +2339,13 @@ func (ss *stageState) updateWatermarks(em
*ElementManager) set[string] {
if minWatermarkHold < newOut {
newOut = minWatermarkHold
}
- // If the newOut is smaller, then don't change downstream watermarks.
+ // If the newOut is smaller, then don't change downstream watermarks.
Any
+ // consumer that received data still needs scheduling, since an
unadvancing
+ // watermark is otherwise the only thing that would surface it.
if newOut <= ss.output {
- return nil
+ refreshes := ss.consumersWithNewData
+ ss.consumersWithNewData = nil
+ return refreshes
}
// If bigger, advance the output watermark
@@ -2328,6 +2385,7 @@ func (ss *stageState) updateWatermarks(em
*ElementManager) set[string] {
// Update this stage's output watermark, and then propagate that to
downstream stages
refreshes := set[string]{}
+ ss.consumersWithNewData = nil
ss.output = newOut
for _, outputCol := range ss.outputIDs {
consumers := em.consumers[outputCol]
@@ -2435,13 +2493,20 @@ func (ss *stageState) bundleReady(em *ElementManager,
emNow mtime.Time) (mtime.T
previousInputW := ss.previousInput
_, isOrdinaryStage := ss.kind.(*ordinaryStageKind)
- if isOrdinaryStage && len(ss.sides) == 0 {
+ _, isStatefulStage := ss.kind.(*statefulStageKind)
+ switch {
+ case isOrdinaryStage && len(ss.sides) == 0:
// For ordinary stage with no side inputs, we use whether there
are pending elements to determine
// whether a bundle is ready or not.
if len(ss.pending) == 0 {
return mtime.MinTimestamp, false, ptimeEventsReady,
injectedReady
}
- } else if inputW == upstreamW && previousInputW == inputW {
+ case isStatefulStage && len(ss.sides) == 0 && em.sawResidual.Load() &&
ss.hasBuildableDataLocked(upstreamW):
+ // A stateful stage processes pending data at whatever the
current
+ // watermark is, so data alone makes it ready once something is
self
+ // checkpointing. Side input readiness comes from the
watermark, so
+ // stages that read one keep waiting.
+ case inputW == upstreamW && previousInputW == inputW:
// Otherwise, use the progression of watermark to determine the
bundle readiness.
slog.Debug("bundleReady: unchanged upstream watermark",
slog.String("stage", ss.ID),
diff --git
a/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager_continuation_test.go
b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager_continuation_test.go
new file mode 100644
index 00000000000..2c0eac52e5e
--- /dev/null
+++
b/sdks/go/pkg/beam/runners/prism/internal/engine/elementmanager_continuation_test.go
@@ -0,0 +1,299 @@
+// 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.
+
+package engine
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io"
+ "testing"
+
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/coder"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/mtime"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/core/graph/window"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/core/runtime/exec"
+ "github.com/apache/beam/sdks/v2/go/pkg/beam/core/typex"
+)
+
+// bundleBudget bounds how many bundles a continuation test drives. The source
+// never terminates, so these tests stop as soon as the consumer is scheduled
and
+// use the budget only to declare starvation.
+const bundleBudget = 50
+
+// continuationInfo is a global window PColInfo, keyed for stateful consumers.
+func continuationInfo(t *testing.T, keyed bool) PColInfo {
+ t.Helper()
+ readAll := func(r io.Reader) []byte {
+ b, err := io.ReadAll(r)
+ if err != nil {
+ t.Fatalf("error decoding element: %v", err)
+ }
+ return b
+ }
+ info := PColInfo{
+ GlobalID: "continuation_info",
+ WDec: exec.MakeWindowDecoder(coder.NewGlobalWindow()),
+ WEnc: exec.MakeWindowEncoder(coder.NewGlobalWindow()),
+ EDec: readAll,
+ }
+ if keyed {
+ info.KeyDec = readAll
+ }
+ return info
+}
+
+// encodeElement produces a global window element at the given event time.
+func encodeElement(t *testing.T, info PColInfo, et mtime.Time) []byte {
+ t.Helper()
+ var buf bytes.Buffer
+ if err := exec.EncodeWindowedValueHeader(info.WEnc,
[]typex.Window{window.GlobalWindow{}}, et, typex.NoFiringPane(), &buf); err !=
nil {
+ t.Fatalf("EncodeWindowedValueHeader: %v", err)
+ }
+ buf.Write([]byte{3, 65, 66, 67}) // "ABC"
+ return buf.Bytes()
+}
+
+// TestPersistBundle_ContinuationResidualConsumers covers issue #39446: a
source
+// whose residual pins its watermark must still get its consumers scheduled.
+func TestPersistBundle_ContinuationResidualConsumers(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ keyed bool
+ stateful bool
+ }{
+ {name: "ordinary consumer"},
+ {name: "stateful consumer", keyed: true, stateful: true},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ srcInfo := continuationInfo(t, false)
+ outInfo := continuationInfo(t, test.keyed)
+
+ ctx, cancelFn :=
context.WithCancelCause(context.Background())
+ defer cancelFn(nil)
+
+ em := NewElementManager(Config{})
+ em.AddStage("impulse", nil, []string{"src_in"}, nil)
+ em.AddStage("src", []string{"src_in"},
[]string{"sink_in"}, nil)
+ em.AddStage("sink", []string{"sink_in"}, nil, nil)
+ if test.stateful {
+ em.StageStateful("sink", nil)
+ }
+ em.Impulse("impulse")
+
+ var i int
+ ch := em.Bundles(ctx, cancelFn, func() string {
+ defer func() { i++ }()
+ return fmt.Sprintf("%v", i)
+ })
+
+ src := em.stages["src"]
+
+ // The source emits a record and self checkpoints every
round, so it
+ // always has a residual outstanding and never
terminates.
+ var srcBundles, sinkBundles int
+ for b := 0; b < bundleBudget && sinkBundles == 0; b++ {
+ rb, ok := <-ch
+ if !ok {
+ t.Fatalf("bundle %d: bundles channel
closed early", b)
+ }
+ switch rb.StageID {
+ case "sink":
+ sinkBundles++
+ em.PersistBundle(rb, nil,
TentativeData{}, outInfo, Residuals{})
+ case "src":
+ srcBundles++
+ td := TentativeData{}
+ td.WriteData("sink_in",
encodeElement(t, outInfo, mtime.Time(100*srcBundles)))
+ // No reported estimate means
MIN_TIMESTAMP, so only the
+ // arriving data can drive the consumer.
+ em.PersistBundle(rb,
map[string]PColInfo{"sink_in": outInfo}, td, srcInfo, Residuals{
+ TransformID: "src",
+ InputID: "i0",
+ Data:
[]Residual{{Element: encodeElement(t, srcInfo, mtime.MinTimestamp)}},
+ })
+ default:
+ t.Fatalf("bundle %d: unexpected stage
%v", b, rb.StageID)
+ }
+ }
+
+ if sinkBundles == 0 {
+ t.Errorf("consumer stage was never scheduled
across %d source bundles, src output watermark = %v; its pending elements are
starved",
+ srcBundles, src.OutputWatermark())
+ }
+ })
+ }
+}
+
+// TestPersistBundle_ContinuationResidualTransitive covers a consumer two
stages
+// below the source. The middle stage returns no residual of its own, yet its
+// watermark is still pinned by the source's.
+func TestPersistBundle_ContinuationResidualTransitive(t *testing.T) {
+ info := continuationInfo(t, false)
+ ctx, cancelFn := context.WithCancelCause(context.Background())
+ defer cancelFn(nil)
+
+ em := NewElementManager(Config{})
+ em.AddStage("impulse", nil, []string{"src_in"}, nil)
+ em.AddStage("src", []string{"src_in"}, []string{"mid_in"}, nil)
+ em.AddStage("mid", []string{"mid_in"}, []string{"sink_in"}, nil)
+ em.AddStage("sink", []string{"sink_in"}, nil, nil)
+ em.Impulse("impulse")
+
+ var i int
+ ch := em.Bundles(ctx, cancelFn, func() string {
+ defer func() { i++ }()
+ return fmt.Sprintf("%v", i)
+ })
+
+ var srcBundles, midBundles, sinkBundles int
+ for b := 0; b < bundleBudget && sinkBundles == 0; b++ {
+ rb, ok := <-ch
+ if !ok {
+ t.Fatalf("bundle %d: bundles channel closed early", b)
+ }
+ switch rb.StageID {
+ case "src":
+ srcBundles++
+ td := TentativeData{}
+ td.WriteData("mid_in", encodeElement(t, info,
mtime.Time(100*srcBundles)))
+ em.PersistBundle(rb, map[string]PColInfo{"mid_in":
info}, td, info, Residuals{
+ TransformID: "src",
+ InputID: "i0",
+ Data: []Residual{{Element:
encodeElement(t, info, mtime.MinTimestamp)}},
+ })
+ case "mid":
+ midBundles++
+ td := TentativeData{}
+ td.WriteData("sink_in", encodeElement(t, info,
mtime.Time(100*midBundles)))
+ em.PersistBundle(rb, map[string]PColInfo{"sink_in":
info}, td, info, Residuals{})
+ case "sink":
+ sinkBundles++
+ em.PersistBundle(rb, nil, TentativeData{}, info,
Residuals{})
+ default:
+ t.Fatalf("bundle %d: unexpected stage %v", b,
rb.StageID)
+ }
+ }
+
+ if midBundles == 0 {
+ t.Error("middle stage was never scheduled")
+ }
+ if sinkBundles == 0 {
+ t.Errorf("stage two below the source was never scheduled across
%d source and %d middle bundles; its pending elements are starved",
+ srcBundles, midBundles)
+ }
+}
+
+// TestPersistBundle_ContinuationResidualWatermark pins the BundleApplication
+// output_watermarks contract: an unreported estimate means MIN_TIMESTAMP.
+func TestPersistBundle_ContinuationResidualWatermark(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ report bool
+ wantHeld bool
+ }{
+ {name: "reported estimate advances the watermark", report:
true},
+ {name: "no estimate holds the watermark", wantHeld: true},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ info := continuationInfo(t, false)
+ ctx, cancelFn :=
context.WithCancelCause(context.Background())
+ defer cancelFn(nil)
+
+ em := NewElementManager(Config{})
+ em.AddStage("impulse", nil, []string{"src_in"}, nil)
+ em.AddStage("src", []string{"src_in"},
[]string{"sink_in"}, nil)
+ em.AddStage("sink", []string{"sink_in"}, nil, nil)
+ em.Impulse("impulse")
+
+ var i int
+ ch := em.Bundles(ctx, cancelFn, func() string {
+ defer func() { i++ }()
+ return fmt.Sprintf("%v", i)
+ })
+
+ src := em.stages["src"]
+
+ for round := 0; round < 3; round++ {
+ rb, ok := <-ch
+ if !ok {
+ t.Fatalf("round %d: bundles channel
closed early", round)
+ }
+ residuals := Residuals{
+ TransformID: "src",
+ InputID: "i0",
+ Data: []Residual{{Element:
encodeElement(t, info, mtime.MinTimestamp)}},
+ }
+ if test.report {
+ residuals.MinOutputWatermarks =
map[string]mtime.Time{"sink_in": mtime.Time(1000 * (round + 1))}
+ }
+ em.PersistBundle(rb, nil, TentativeData{},
info, residuals)
+ }
+
+ got := src.OutputWatermark()
+ if test.wantHeld && got != mtime.MinTimestamp {
+ t.Errorf("src.OutputWatermark() = %v, want %v:
an unreported estimate defaults to MIN_TIMESTAMP", got, mtime.MinTimestamp)
+ }
+ if !test.wantHeld && got == mtime.MinTimestamp {
+ t.Errorf("src.OutputWatermark() = %v, want it
to follow the reported estimate", got)
+ }
+ })
+ }
+}
+
+// TestStatefulBuildEventTimeBundle_OneKeyPerBundle checks that a key holding
+// only a timer above the watermark does not consume the single key slot, which
+// would build an empty bundle and reschedule the stage on the same key
forever.
+func TestStatefulBuildEventTimeBundle_OneKeyPerBundle(t *testing.T) {
+ OneKeyPerBundle = true
+ t.Cleanup(func() { OneKeyPerBundle = false })
+
+ // Key iteration order is randomized, so repeat until the timer key
leads.
+ for i := 0; i < 20; i++ {
+ em := NewElementManager(Config{})
+ ss := makeStageState("stateful", []string{"input"}, nil, nil)
+ ss.kind = &statefulStageKind{}
+ ss.AddPending(em, []element{{
+ window: window.GlobalWindow{},
+ timestamp: mtime.MaxTimestamp - 1,
+ holdTimestamp: mtime.MaxTimestamp - 1,
+ pane: typex.NoFiringPane(),
+ transform: "stateful",
+ family: "timer",
+ keyBytes: []byte("timerkey"),
+ sequence: 0,
+ }, {
+ window: window.GlobalWindow{},
+ timestamp: 10,
+ pane: typex.NoFiringPane(),
+ elmBytes: []byte{3, 65, 66, 67},
+ keyBytes: []byte("datakey"),
+ }})
+
+ toProcess, minTs, newKeys, _, _, _, _ :=
ss.kind.buildEventTimeBundle(ss, mtime.Time(100))
+ if len(toProcess) == 0 {
+ t.Fatalf("iteration %d: built an empty bundle while a
data key was pending", i)
+ }
+ // The skipped timer key must not be marked in progress, nor
hold the
+ // bundle's minimum timestamp.
+ if len(newKeys) != 1 || !newKeys.present("datakey") {
+ t.Fatalf("iteration %d: newKeys = %v, want only
datakey", i, newKeys)
+ }
+ if want := mtime.Time(10); minTs != want {
+ t.Fatalf("iteration %d: minTs = %v, want %v", i, minTs,
want)
+ }
+ }
+}
diff --git a/sdks/python/apache_beam/io/external/xlang_jmsio_it_test.py
b/sdks/python/apache_beam/io/external/xlang_jmsio_it_test.py
index c1922eb26ba..c3f26097376 100644
--- a/sdks/python/apache_beam/io/external/xlang_jmsio_it_test.py
+++ b/sdks/python/apache_beam/io/external/xlang_jmsio_it_test.py
@@ -92,7 +92,7 @@ class _BaseJmsIOTest(unittest.TestCase):
subscriber_result = {}
def publish():
- self.produce(source_queue, remaining_records)
+ self.produce(source_queue, NUM_RECORDS)
stop_event = threading.Event()
@@ -110,12 +110,6 @@ class _BaseJmsIOTest(unittest.TestCase):
break
_LOGGER.info('received %s messages', len(received_messages))
- # TODO(https://github.com/apache/beam/issues/39446): Clean up
- # pre-publishing Prism runner issue resolved
- initial_records = 10
- remaining_records = NUM_RECORDS - initial_records
- self.produce(source_queue, initial_records)
-
publisher = threading.Thread(target=publish, daemon=True)
subscriber = threading.Thread(target=subscribe, daemon=True)
@@ -147,7 +141,7 @@ class _BaseJmsIOTest(unittest.TestCase):
result = p.run()
subscriber.start()
try:
- subscriber.join(timeout=20) # 1.5 min
+ subscriber.join(timeout=20)
finally:
stop_event.set()
publisher.join()
@@ -160,8 +154,7 @@ class _BaseJmsIOTest(unittest.TestCase):
received = subscriber_result.get('received', [])
self.assertEqual(len(received), NUM_RECORDS)
- # there are identical records
- self.assertEqual(len(set(received)), NUM_RECORDS - initial_records)
+ self.assertEqual(len(set(received)), NUM_RECORDS)
class ActiveMQJmsIOTest(_BaseJmsIOTest):