The controller owns a worker's shape, type included: one of the wrong type is re-made on a work queue (hq issue 206)
A holder built for a pull worker cannot bind a push one — `cannot pull subscribe to push based consumer` — and on 2026-10-03 the build machine rolled before the controller that would have redefined its worker, restarted on that for an hour, and nothing could build the controller that would have ended it. The server cannot change a consumer's type in place, so the assertion re-makes one of the wrong type: on a work queue nothing is lost, because what was acknowledged is gone from the stream and what was not is delivered again from the start. On a stream that keeps its history it is said and left, since a re-made consumer replays what this one acknowledged (issue 156), and that is a person's call. Proven against a real bus: a push worker with one ask acknowledged and two pending is re-made as pull, a pull subscription binds, and takes exactly the two.
This commit is contained in:
@@ -214,6 +214,43 @@ func (j *JetStream) EnsureConsumer(c Consumer) error {
|
||||
|
||||
switch have, err := j.js.ConsumerInfo(c.Stream, c.Name); {
|
||||
case err == nil:
|
||||
// **The controller owns the worker's shape, type included** (novox/hq issue 206). A holder
|
||||
// built for a pull worker cannot bind a push one — `cannot pull subscribe to push based
|
||||
// consumer` — and on 2026-10-03 the build machine rolled before the controller that would
|
||||
// have redefined its worker, restarted on that for an hour, and nothing could build the
|
||||
// controller that would have ended it. The server cannot change a consumer's type in place,
|
||||
// so one of the wrong type is re-made: on a work queue nothing is lost, because what was
|
||||
// acknowledged is gone from the stream and what was not is delivered again from the start.
|
||||
// On any other stream a re-made consumer would replay what this one acknowledged (issue
|
||||
// 156), so there it is said and left, and the person re-makes it knowing the cost.
|
||||
if havePush, wantPush := have.Config.DeliverSubject != "", want.DeliverSubject != ""; havePush != wantPush {
|
||||
shape := func(push bool) string {
|
||||
if push {
|
||||
return "push"
|
||||
}
|
||||
return "pull"
|
||||
}
|
||||
info, err := j.js.StreamInfo(c.Stream)
|
||||
if err != nil {
|
||||
return fmt.Errorf("asking about stream %s to re-make consumer %s: %w", c.Stream, c.Name, err)
|
||||
}
|
||||
if info.Config.Retention != nats.WorkQueuePolicy {
|
||||
j.note("consumer %s on %s is %s and should be %s; not re-made, because %s keeps its history "+
|
||||
"and a re-made consumer replays what this one acknowledged (novox/hq issue 156). Re-make it by hand",
|
||||
c.Name, c.Stream, shape(havePush), shape(wantPush), c.Stream)
|
||||
return nil
|
||||
}
|
||||
j.note("consumer %s on %s changes from %s to %s delivery: re-made where it left off, nothing "+
|
||||
"acknowledged comes back and nothing pending is lost (novox/hq issue 206); a holder bound to "+
|
||||
"the old shape binds again", c.Name, c.Stream, shape(havePush), shape(wantPush))
|
||||
if err := j.js.DeleteConsumer(c.Stream, c.Name); err != nil {
|
||||
return fmt.Errorf("re-making consumer %s on %s as %s: %w", c.Name, c.Stream, shape(wantPush), err)
|
||||
}
|
||||
if _, err := j.js.AddConsumer(c.Stream, want); err != nil {
|
||||
return fmt.Errorf("re-making consumer %s on %s as %s: %w", c.Name, c.Stream, shape(wantPush), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Where an existing consumer starts is its history, not something an assertion may move:
|
||||
// the server refuses a changed deliver policy outright. Carried across, so asserting twice
|
||||
// is the no-op a restart depends on.
|
||||
|
||||
Reference in New Issue
Block a user