fix(render): report immediate resize completion
- commit
- ef6cad648fccfba019422a6659692c6d1166e610
- author
- brice · · 2026-07-13 07:45 UTC
- parent
- b901b13
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +77 -24
M internal/usecase/daemon/render_coordinator.go +15 -6
| @@ -265,16 +265,24 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||
| 265 | 265 | // attachment. This check shares c.mu with replacement, making a stale | |
| 266 | 266 | // attachment's timer callback unable to enqueue work for its replacement. | |
| 267 | 267 | // A nil source represents an internal pane/session invalidation. | |
| 268 | - | func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) { | |
| 268 | + | func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) bool { | |
| 269 | + | return c.invalidateForAttachmentAtResizeEpoch(source, 0, inv) | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | // invalidateForAttachmentAtResizeEpoch additionally requires epoch to remain | |
| 273 | + | // current when nonzero, so a completed resize cannot publish after a newer | |
| 274 | + | // request supersedes it. | |
| 275 | + | func (c *renderCoordinator) invalidateForAttachmentAtResizeEpoch(source *attachedClient, epoch uint64, inv renderInvalidation) bool { | |
| 269 | 276 | c.mu.Lock() | |
| 270 | 277 | // An unbound coordinator is the manual/headless harness state; there is no | |
| 271 | 278 | // published production attachment to protect there. A parked target may | |
| 272 | 279 | // still publish its own PTY/session mutations to picker observers, but no | |
| 273 | 280 | // attachment-owned callback may revive that target's render path. | |
| 274 | 281 | detachedPreviewOnly := c.detached && source == nil && (c.previewWake != nil || len(c.previewWakes) != 0) | |
| 275 | - | if c.torndown || (c.detached && !detachedPreviewOnly) || (source != nil && c.attachment != nil && c.attachment != source) { | |
| 282 | + | if c.torndown || (c.detached && !detachedPreviewOnly) || (source != nil && c.attachment != nil && c.attachment != source) || | |
| 283 | + | (epoch != 0 && (c.resize.epoch != epoch || c.resize.source != source)) { | |
| 276 | 284 | c.mu.Unlock() | |
| 277 | - | return | |
| 285 | + | return false | |
| 278 | 286 | } | |
| 279 | 287 | onInvalidate := c.opts.onInvalidate | |
| 280 | 288 | c.metrics.invalidations.Add(1) | |
| @@ -310,7 +318,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||
| 310 | 318 | if onInvalidate != nil { | |
| 311 | 319 | onInvalidate(inv) | |
| 312 | 320 | } | |
| 313 | - | return | |
| 321 | + | return true | |
| 314 | 322 | } | |
| 315 | 323 | ||
| 316 | 324 | // NewTimer and C may re-enter the coordinator. Publish only if this | |
| @@ -330,7 +338,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||
| 330 | 338 | if onInvalidate != nil { | |
| 331 | 339 | onInvalidate(inv) | |
| 332 | 340 | } | |
| 333 | - | return | |
| 341 | + | return true | |
| 334 | 342 | } | |
| 335 | 343 | if timerC == nil { | |
| 336 | 344 | stopTimer(timer) | |
| @@ -338,7 +346,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||
| 338 | 346 | if onInvalidate != nil { | |
| 339 | 347 | onInvalidate(inv) | |
| 340 | 348 | } | |
| 341 | - | return | |
| 349 | + | return true | |
| 342 | 350 | } | |
| 343 | 351 | go func() { | |
| 344 | 352 | defer close(done) | |
| @@ -356,6 +364,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||
| 356 | 364 | onInvalidate(inv) | |
| 357 | 365 | } | |
| 358 | 366 | runtime.Gosched() | |
| 367 | + | return true | |
| 359 | 368 | } | |
| 360 | 369 | ||
| 361 | 370 | // notifyAck reports that the client acknowledged an output state, releasing | |
| @@ -265,16 +265,24 @@ func (c *renderCoordinator) invalidate(inv renderInvalidation) { | |||||
| 265 | // attachment. This check shares c.mu with replacement, making a stale | 265 | // attachment. This check shares c.mu with replacement, making a stale | ||
| 266 | // attachment's timer callback unable to enqueue work for its replacement. | 266 | // attachment's timer callback unable to enqueue work for its replacement. | ||
| 267 | // A nil source represents an internal pane/session invalidation. | 267 | // A nil source represents an internal pane/session invalidation. | ||
| 268 | - | func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) { | 268 | + | func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv renderInvalidation) bool { |
| 269 | + | return c.invalidateForAttachmentAtResizeEpoch(source, 0, inv) | |||
| 270 | + | } | |||
| 271 | + | ||||
| 272 | + | // invalidateForAttachmentAtResizeEpoch additionally requires epoch to remain | |||
| 273 | + | // current when nonzero, so a completed resize cannot publish after a newer | |||
| 274 | + | // request supersedes it. | |||
| 275 | + | func (c *renderCoordinator) invalidateForAttachmentAtResizeEpoch(source *attachedClient, epoch uint64, inv renderInvalidation) bool { | |||
| 269 | c.mu.Lock() | 276 | c.mu.Lock() | ||
| 270 | // An unbound coordinator is the manual/headless harness state; there is no | 277 | // An unbound coordinator is the manual/headless harness state; there is no | ||
| 271 | // published production attachment to protect there. A parked target may | 278 | // published production attachment to protect there. A parked target may | ||
| 272 | // still publish its own PTY/session mutations to picker observers, but no | 279 | // still publish its own PTY/session mutations to picker observers, but no | ||
| 273 | // attachment-owned callback may revive that target's render path. | 280 | // attachment-owned callback may revive that target's render path. | ||
| 274 | detachedPreviewOnly := c.detached && source == nil && (c.previewWake != nil || len(c.previewWakes) != 0) | 281 | detachedPreviewOnly := c.detached && source == nil && (c.previewWake != nil || len(c.previewWakes) != 0) | ||
| 275 | - | if c.torndown || (c.detached && !detachedPreviewOnly) || (source != nil && c.attachment != nil && c.attachment != source) { | 282 | + | if c.torndown || (c.detached && !detachedPreviewOnly) || (source != nil && c.attachment != nil && c.attachment != source) || |
| 283 | + | (epoch != 0 && (c.resize.epoch != epoch || c.resize.source != source)) { | |||
| 276 | c.mu.Unlock() | 284 | c.mu.Unlock() | ||
| 277 | - | return | 285 | + | return false |
| 278 | } | 286 | } | ||
| 279 | onInvalidate := c.opts.onInvalidate | 287 | onInvalidate := c.opts.onInvalidate | ||
| 280 | c.metrics.invalidations.Add(1) | 288 | c.metrics.invalidations.Add(1) | ||
| @@ -310,7 +318,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||||
| 310 | if onInvalidate != nil { | 318 | if onInvalidate != nil { | ||
| 311 | onInvalidate(inv) | 319 | onInvalidate(inv) | ||
| 312 | } | 320 | } | ||
| 313 | - | return | 321 | + | return true |
| 314 | } | 322 | } | ||
| 315 | 323 | ||||
| 316 | // NewTimer and C may re-enter the coordinator. Publish only if this | 324 | // NewTimer and C may re-enter the coordinator. Publish only if this | ||
| @@ -330,7 +338,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||||
| 330 | if onInvalidate != nil { | 338 | if onInvalidate != nil { | ||
| 331 | onInvalidate(inv) | 339 | onInvalidate(inv) | ||
| 332 | } | 340 | } | ||
| 333 | - | return | 341 | + | return true |
| 334 | } | 342 | } | ||
| 335 | if timerC == nil { | 343 | if timerC == nil { | ||
| 336 | stopTimer(timer) | 344 | stopTimer(timer) | ||
| @@ -338,7 +346,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||||
| 338 | if onInvalidate != nil { | 346 | if onInvalidate != nil { | ||
| 339 | onInvalidate(inv) | 347 | onInvalidate(inv) | ||
| 340 | } | 348 | } | ||
| 341 | - | return | 349 | + | return true |
| 342 | } | 350 | } | ||
| 343 | go func() { | 351 | go func() { | ||
| 344 | defer close(done) | 352 | defer close(done) | ||
| @@ -356,6 +364,7 @@ func (c *renderCoordinator) invalidateForAttachment(source *attachedClient, inv | |||||
| 356 | onInvalidate(inv) | 364 | onInvalidate(inv) | ||
| 357 | } | 365 | } | ||
| 358 | runtime.Gosched() | 366 | runtime.Gosched() | ||
| 367 | + | return true | |||
| 359 | } | 368 | } | ||
| 360 | 369 | ||||
| 361 | // notifyAck reports that the client acknowledged an output state, releasing | 370 | // notifyAck reports that the client acknowledged an output state, releasing | ||
M internal/usecase/daemon/render_coordinator_test.go +48 -7
| @@ -1559,14 +1559,55 @@ func TestRenderCoordinatorResizeEpochDispatch(t *testing.T) { | |||
| 1559 | 1559 | requireNoCoordinatorOutputFrame(t, sends) | |
| 1560 | 1560 | }) | |
| 1561 | 1561 | ||
| 1562 | - | t.Run("async request reports coordinator schedule acceptance", func(t *testing.T) { | |
| 1563 | - | d, sess, ac, _, clk, _ := newResizeFixture(t) | |
| 1562 | + | t.Run("request reports immediate completion and async schedule acceptance", func(t *testing.T) { | |
| 1563 | + | t.Run("immediate reset invalidation", func(t *testing.T) { | |
| 1564 | + | d, sess, ac, _, _, invs := newResizeFixture(t) | |
| 1564 | 1565 | ||
| 1565 | - | require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, false)) | |
| 1566 | - | awaitCoordinatorScheduledTimer(t, clk) | |
| 1566 | + | require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, true)) | |
| 1567 | + | inv := awaitInvalidation(t, invs) | |
| 1568 | + | require.True(t, inv.reset) | |
| 1569 | + | }) | |
| 1570 | + | ||
| 1571 | + | t.Run("superseded immediate transaction", func(t *testing.T) { | |
| 1572 | + | resizeStarted := make(chan struct{}) | |
| 1573 | + | releaseResize := make(chan struct{}) | |
| 1574 | + | pty := &transactionalResizePTY{onResize: func() { | |
| 1575 | + | close(resizeStarted) | |
| 1576 | + | <-releaseResize | |
| 1577 | + | }} | |
| 1578 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | |
| 1579 | + | clk := newCoordinatorMockClock(t, 2) | |
| 1580 | + | d.clock = clk.clock | |
| 1581 | + | invs := make(chan renderInvalidation, 1) | |
| 1582 | + | rc := newRenderCoordinator(renderCoordinatorOptions{ | |
| 1583 | + | clock: clk.clock, | |
| 1584 | + | wake: func(renderWake) {}, | |
| 1585 | + | onInvalidate: func(inv renderInvalidation) { invs <- inv }, | |
| 1586 | + | }) | |
| 1587 | + | rc.attach(ac) | |
| 1588 | + | sess.installRenderCoordinator(rc) | |
| 1589 | + | ||
| 1590 | + | result := make(chan bool, 1) | |
| 1591 | + | go func() { | |
| 1592 | + | result <- d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, true) | |
| 1593 | + | }() | |
| 1594 | + | <-resizeStarted | |
| 1595 | + | require.NotZero(t, rc.recordResizeRequest(domain.Size{Cols: 140, Rows: 30}, ac)) | |
| 1596 | + | close(releaseResize) | |
| 1597 | + | ||
| 1598 | + | require.False(t, <-result) | |
| 1599 | + | requireNoInvalidation(t, invs) | |
| 1600 | + | }) | |
| 1601 | + | ||
| 1602 | + | t.Run("async schedule and teardown rejection", func(t *testing.T) { | |
| 1603 | + | d, sess, ac, _, clk, _ := newResizeFixture(t) | |
| 1567 | 1604 | ||
| 1568 | - | // A torn-down coordinator cannot accept a stale attachment's schedule. | |
| 1569 | - | sess.renderCoordinator().noteSessionTeardown() | |
| 1570 | - | require.False(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 140, Rows: 30}, false)) | |
| 1605 | + | require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, false)) | |
| 1606 | + | awaitCoordinatorScheduledTimer(t, clk) | |
| 1607 | + | ||
| 1608 | + | // A torn-down coordinator cannot accept a stale attachment's schedule. | |
| 1609 | + | sess.renderCoordinator().noteSessionTeardown() | |
| 1610 | + | require.False(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 140, Rows: 30}, false)) | |
| 1611 | + | }) | |
| 1571 | 1612 | }) | |
| 1572 | 1613 | } | |
| @@ -1559,14 +1559,55 @@ func TestRenderCoordinatorResizeEpochDispatch(t *testing.T) { | |||||
| 1559 | requireNoCoordinatorOutputFrame(t, sends) | 1559 | requireNoCoordinatorOutputFrame(t, sends) | ||
| 1560 | }) | 1560 | }) | ||
| 1561 | 1561 | ||||
| 1562 | - | t.Run("async request reports coordinator schedule acceptance", func(t *testing.T) { | 1562 | + | t.Run("request reports immediate completion and async schedule acceptance", func(t *testing.T) { |
| 1563 | - | d, sess, ac, _, clk, _ := newResizeFixture(t) | 1563 | + | t.Run("immediate reset invalidation", func(t *testing.T) { |
| 1564 | + | d, sess, ac, _, _, invs := newResizeFixture(t) | |||
| 1564 | 1565 | ||||
| 1565 | - | require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, false)) | 1566 | + | require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, true)) |
| 1566 | - | awaitCoordinatorScheduledTimer(t, clk) | 1567 | + | inv := awaitInvalidation(t, invs) |
| 1568 | + | require.True(t, inv.reset) | |||
| 1569 | + | }) | |||
| 1570 | + | ||||
| 1571 | + | t.Run("superseded immediate transaction", func(t *testing.T) { | |||
| 1572 | + | resizeStarted := make(chan struct{}) | |||
| 1573 | + | releaseResize := make(chan struct{}) | |||
| 1574 | + | pty := &transactionalResizePTY{onResize: func() { | |||
| 1575 | + | close(resizeStarted) | |||
| 1576 | + | <-releaseResize | |||
| 1577 | + | }} | |||
| 1578 | + | d, sess, ac, _ := newManualSessionWithPTYs(t, pty) | |||
| 1579 | + | clk := newCoordinatorMockClock(t, 2) | |||
| 1580 | + | d.clock = clk.clock | |||
| 1581 | + | invs := make(chan renderInvalidation, 1) | |||
| 1582 | + | rc := newRenderCoordinator(renderCoordinatorOptions{ | |||
| 1583 | + | clock: clk.clock, | |||
| 1584 | + | wake: func(renderWake) {}, | |||
| 1585 | + | onInvalidate: func(inv renderInvalidation) { invs <- inv }, | |||
| 1586 | + | }) | |||
| 1587 | + | rc.attach(ac) | |||
| 1588 | + | sess.installRenderCoordinator(rc) | |||
| 1589 | + | ||||
| 1590 | + | result := make(chan bool, 1) | |||
| 1591 | + | go func() { | |||
| 1592 | + | result <- d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, true) | |||
| 1593 | + | }() | |||
| 1594 | + | <-resizeStarted | |||
| 1595 | + | require.NotZero(t, rc.recordResizeRequest(domain.Size{Cols: 140, Rows: 30}, ac)) | |||
| 1596 | + | close(releaseResize) | |||
| 1597 | + | ||||
| 1598 | + | require.False(t, <-result) | |||
| 1599 | + | requireNoInvalidation(t, invs) | |||
| 1600 | + | }) | |||
| 1601 | + | ||||
| 1602 | + | t.Run("async schedule and teardown rejection", func(t *testing.T) { | |||
| 1603 | + | d, sess, ac, _, clk, _ := newResizeFixture(t) | |||
| 1567 | 1604 | ||||
| 1568 | - | // A torn-down coordinator cannot accept a stale attachment's schedule. | 1605 | + | require.True(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 120, Rows: 24}, false)) |
| 1569 | - | sess.renderCoordinator().noteSessionTeardown() | 1606 | + | awaitCoordinatorScheduledTimer(t, clk) |
| 1570 | - | require.False(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 140, Rows: 30}, false)) | 1607 | + | |
| 1608 | + | // A torn-down coordinator cannot accept a stale attachment's schedule. | |||
| 1609 | + | sess.renderCoordinator().noteSessionTeardown() | |||
| 1610 | + | require.False(t, d.requestTransactionalResize(sess, ac, domain.Size{Cols: 140, Rows: 30}, false)) | |||
| 1611 | + | }) | |||
| 1571 | }) | 1612 | }) | ||
| 1572 | } | 1613 | } | ||
M internal/usecase/daemon/transactional_resize.go +14 -11
| @@ -163,25 +163,25 @@ func (d *Daemon) commitResize(sess *session, ac *attachedClient, plan resizePlan | |||
| 163 | 163 | markSnapshotDirty(sess) | |
| 164 | 164 | } | |
| 165 | 165 | ||
| 166 | - | func (d *Daemon) runResizeTransaction(sess *session, ac *attachedClient, epoch uint64) { | |
| 166 | + | func (d *Daemon) runResizeTransaction(sess *session, ac *attachedClient, epoch uint64) bool { | |
| 167 | 167 | rc := sess.renderCoordinator() | |
| 168 | 168 | if rc == nil { | |
| 169 | - | return | |
| 169 | + | return false | |
| 170 | 170 | } | |
| 171 | 171 | snap := rc.resizeSnapshot() | |
| 172 | 172 | if !rc.resizeCurrent(epoch, ac, false) { | |
| 173 | - | return | |
| 173 | + | return false | |
| 174 | 174 | } | |
| 175 | 175 | d.exitCopyMode(ac) | |
| 176 | 176 | plan := d.prepareResize(sess, snap.size) | |
| 177 | 177 | if !rc.resizeCurrent(epoch, ac, false) { | |
| 178 | - | return | |
| 178 | + | return false | |
| 179 | 179 | } | |
| 180 | 180 | if !d.applyResize(&plan, func() bool { return rc.resizeCurrent(epoch, ac, false) }) { | |
| 181 | - | return | |
| 181 | + | return false | |
| 182 | 182 | } | |
| 183 | 183 | if !rc.resizeCurrent(epoch, ac, true) { | |
| 184 | - | return | |
| 184 | + | return false | |
| 185 | 185 | } | |
| 186 | 186 | d.commitResize(sess, ac, plan) | |
| 187 | 187 | failed := make([]resizeMember, 0, len(plan.members)) | |
| @@ -196,11 +196,14 @@ func (d *Daemon) runResizeTransaction(sess *session, ac *attachedClient, epoch u | |||
| 196 | 196 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | |
| 197 | 197 | // A successful transaction publishes exactly one full S2 frame. The | |
| 198 | 198 | // coordinator is the only emission route and stale epochs never reach it. | |
| 199 | - | rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, reset: true, producer: "transactional_resize.go"}) | |
| 199 | + | if !rc.invalidateForAttachmentAtResizeEpoch(ac, epoch, renderInvalidation{class: invalidateUrgent, reset: true, producer: "transactional_resize.go"}) { | |
| 200 | + | return false | |
| 201 | + | } | |
| 200 | 202 | // The resize debounce has already elapsed. Consume this sticky reset now; | |
| 201 | 203 | // fire preserves ACK and synchronized-output gates rather than scheduling a | |
| 202 | 204 | // second urgent deadline. | |
| 203 | 205 | rc.fireCurrent(false) | |
| 206 | + | return true | |
| 204 | 207 | } | |
| 205 | 208 | ||
| 206 | 209 | // retryResizeMembers retries only members which failed the committed epoch. | |
| @@ -242,8 +245,9 @@ func (d *Daemon) retryResizeMembers(sess *session, ac *attachedClient, epoch uin | |||
| 242 | 245 | } | |
| 243 | 246 | } | |
| 244 | 247 | ||
| 245 | - | // requestTransactionalResize reports whether the coordinator accepted the | |
| 246 | - | // request. Immediate requests complete synchronously before it returns. | |
| 248 | + | // requestTransactionalResize reports reset invalidation completion for immediate | |
| 249 | + | // attached requests, and coordinator schedule acceptance for async requests. | |
| 250 | + | // Headless requests have no reset invalidation and report geometry completion. | |
| 247 | 251 | func (d *Daemon) requestTransactionalResize(sess *session, ac *attachedClient, size domain.Size, immediate bool) bool { | |
| 248 | 252 | if sess == nil || !size.Valid() { | |
| 249 | 253 | return false | |
| @@ -262,8 +266,7 @@ func (d *Daemon) requestTransactionalResize(sess *session, ac *attachedClient, s | |||
| 262 | 266 | if epoch == 0 { | |
| 263 | 267 | return false | |
| 264 | 268 | } | |
| 265 | - | d.runResizeTransaction(sess, ac, epoch) | |
| 266 | - | return true | |
| 269 | + | return d.runResizeTransaction(sess, ac, epoch) | |
| 267 | 270 | } | |
| 268 | 271 | return rc.scheduleResize(size, ac, func(epoch uint64) { d.runResizeTransaction(sess, ac, epoch) }) != 0 | |
| 269 | 272 | } | |
| @@ -163,25 +163,25 @@ func (d *Daemon) commitResize(sess *session, ac *attachedClient, plan resizePlan | |||||
| 163 | markSnapshotDirty(sess) | 163 | markSnapshotDirty(sess) | ||
| 164 | } | 164 | } | ||
| 165 | 165 | ||||
| 166 | - | func (d *Daemon) runResizeTransaction(sess *session, ac *attachedClient, epoch uint64) { | 166 | + | func (d *Daemon) runResizeTransaction(sess *session, ac *attachedClient, epoch uint64) bool { |
| 167 | rc := sess.renderCoordinator() | 167 | rc := sess.renderCoordinator() | ||
| 168 | if rc == nil { | 168 | if rc == nil { | ||
| 169 | - | return | 169 | + | return false |
| 170 | } | 170 | } | ||
| 171 | snap := rc.resizeSnapshot() | 171 | snap := rc.resizeSnapshot() | ||
| 172 | if !rc.resizeCurrent(epoch, ac, false) { | 172 | if !rc.resizeCurrent(epoch, ac, false) { | ||
| 173 | - | return | 173 | + | return false |
| 174 | } | 174 | } | ||
| 175 | d.exitCopyMode(ac) | 175 | d.exitCopyMode(ac) | ||
| 176 | plan := d.prepareResize(sess, snap.size) | 176 | plan := d.prepareResize(sess, snap.size) | ||
| 177 | if !rc.resizeCurrent(epoch, ac, false) { | 177 | if !rc.resizeCurrent(epoch, ac, false) { | ||
| 178 | - | return | 178 | + | return false |
| 179 | } | 179 | } | ||
| 180 | if !d.applyResize(&plan, func() bool { return rc.resizeCurrent(epoch, ac, false) }) { | 180 | if !d.applyResize(&plan, func() bool { return rc.resizeCurrent(epoch, ac, false) }) { | ||
| 181 | - | return | 181 | + | return false |
| 182 | } | 182 | } | ||
| 183 | if !rc.resizeCurrent(epoch, ac, true) { | 183 | if !rc.resizeCurrent(epoch, ac, true) { | ||
| 184 | - | return | 184 | + | return false |
| 185 | } | 185 | } | ||
| 186 | d.commitResize(sess, ac, plan) | 186 | d.commitResize(sess, ac, plan) | ||
| 187 | failed := make([]resizeMember, 0, len(plan.members)) | 187 | failed := make([]resizeMember, 0, len(plan.members)) | ||
| @@ -196,11 +196,14 @@ func (d *Daemon) runResizeTransaction(sess *session, ac *attachedClient, epoch u | |||||
| 196 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | 196 | d.refreshBarScriptsIfDue(sess, d.clock.Now(), true) | ||
| 197 | // A successful transaction publishes exactly one full S2 frame. The | 197 | // A successful transaction publishes exactly one full S2 frame. The | ||
| 198 | // coordinator is the only emission route and stale epochs never reach it. | 198 | // coordinator is the only emission route and stale epochs never reach it. | ||
| 199 | - | rc.invalidateForAttachment(ac, renderInvalidation{class: invalidateUrgent, reset: true, producer: "transactional_resize.go"}) | 199 | + | if !rc.invalidateForAttachmentAtResizeEpoch(ac, epoch, renderInvalidation{class: invalidateUrgent, reset: true, producer: "transactional_resize.go"}) { |
| 200 | + | return false | |||
| 201 | + | } | |||
| 200 | // The resize debounce has already elapsed. Consume this sticky reset now; | 202 | // The resize debounce has already elapsed. Consume this sticky reset now; | ||
| 201 | // fire preserves ACK and synchronized-output gates rather than scheduling a | 203 | // fire preserves ACK and synchronized-output gates rather than scheduling a | ||
| 202 | // second urgent deadline. | 204 | // second urgent deadline. | ||
| 203 | rc.fireCurrent(false) | 205 | rc.fireCurrent(false) | ||
| 206 | + | return true | |||
| 204 | } | 207 | } | ||
| 205 | 208 | ||||
| 206 | // retryResizeMembers retries only members which failed the committed epoch. | 209 | // retryResizeMembers retries only members which failed the committed epoch. | ||
| @@ -242,8 +245,9 @@ func (d *Daemon) retryResizeMembers(sess *session, ac *attachedClient, epoch uin | |||||
| 242 | } | 245 | } | ||
| 243 | } | 246 | } | ||
| 244 | 247 | ||||
| 245 | - | // requestTransactionalResize reports whether the coordinator accepted the | 248 | + | // requestTransactionalResize reports reset invalidation completion for immediate |
| 246 | - | // request. Immediate requests complete synchronously before it returns. | 249 | + | // attached requests, and coordinator schedule acceptance for async requests. |
| 250 | + | // Headless requests have no reset invalidation and report geometry completion. | |||
| 247 | func (d *Daemon) requestTransactionalResize(sess *session, ac *attachedClient, size domain.Size, immediate bool) bool { | 251 | func (d *Daemon) requestTransactionalResize(sess *session, ac *attachedClient, size domain.Size, immediate bool) bool { | ||
| 248 | if sess == nil || !size.Valid() { | 252 | if sess == nil || !size.Valid() { | ||
| 249 | return false | 253 | return false | ||
| @@ -262,8 +266,7 @@ func (d *Daemon) requestTransactionalResize(sess *session, ac *attachedClient, s | |||||
| 262 | if epoch == 0 { | 266 | if epoch == 0 { | ||
| 263 | return false | 267 | return false | ||
| 264 | } | 268 | } | ||
| 265 | - | d.runResizeTransaction(sess, ac, epoch) | 269 | + | return d.runResizeTransaction(sess, ac, epoch) |
| 266 | - | return true | |||
| 267 | } | 270 | } | ||
| 268 | return rc.scheduleResize(size, ac, func(epoch uint64) { d.runResizeTransaction(sess, ac, epoch) }) != 0 | 271 | return rc.scheduleResize(size, ac, func(epoch uint64) { d.runResizeTransaction(sess, ac, epoch) }) != 0 | ||
| 269 | } | 272 | } | ||