refactor(daemon): extract graphics namespace pool
- commit
- 4c53368af4d9a34040302254fd6501d18cc50a9a
- author
- brice · · 2026-10-08 06:03 UTC
- committer
- brice · · 2026-10-08 08:12 UTC
- parent
- f04184f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +158 -117
M internal/usecase/daemon/daemon.go +26 -38
| @@ -148,18 +148,9 @@ type Daemon struct { | |||
| 148 | 148 | // re-suspension, detach, or session teardown can never be retired by a | |
| 149 | 149 | // stale timer. Guarded by mu. | |
| 150 | 150 | suspended map[*attachedClient]*suspendedAttachmentRetention | |
| 151 | - | // graphicsNamespaces reserves deterministic, attachment/session-scoped Kitty | |
| 152 | - | // ID blocks. Once a block may have reached an outer terminal it remains in | |
| 153 | - | // this bounded table for the daemon lifetime: side-effect Output frames have | |
| 154 | - | // no terminal ACK. Pool exhaustion disables graphics for new attachments and | |
| 155 | - | // leaves their ordinary text output intact. | |
| 156 | - | graphicsNamespaces map[uint64]struct{} | |
| 157 | - | graphicsNamespaceFences map[uint64]uint64 | |
| 158 | - | graphicsNamespaceQuarantines map[uint64]*graphicsNamespaceQuarantine | |
| 159 | - | // graphicsNamespaceSalt separates daemon lifetimes before attachment keys | |
| 160 | - | // are hashed. Kitty IDs are terminal-global, so a restarted or neighboring | |
| 161 | - | // daemon must not deterministically reopen the previous daemon's block. | |
| 162 | - | graphicsNamespaceSalt uint64 | |
| 151 | + | // graphics reserves attachment/session-scoped Kitty ID blocks. Guarded by | |
| 152 | + | // mu; see graphicsNamespacePool. | |
| 153 | + | graphics graphicsNamespacePool | |
| 163 | 154 | ||
| 164 | 155 | attnMu sync.Mutex | |
| 165 | 156 | animFrame int | |
| @@ -708,32 +699,29 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt | |||
| 708 | 699 | } | |
| 709 | 700 | paneProcessCtx, paneProcessCancel := context.WithCancel(context.Background()) | |
| 710 | 701 | d := &Daemon{ | |
| 711 | - | sessions: make(map[domain.SessionID]*session), | |
| 712 | - | inactive: make(map[string]inactiveSession), | |
| 713 | - | creating: make(map[string]struct{}), | |
| 714 | - | resume: newResumeCredentials(), | |
| 715 | - | suspended: make(map[*attachedClient]*suspendedAttachmentRetention), | |
| 716 | - | graphicsNamespaces: make(map[uint64]struct{}), | |
| 717 | - | graphicsNamespaceFences: make(map[uint64]uint64), | |
| 718 | - | graphicsNamespaceQuarantines: make(map[uint64]*graphicsNamespaceQuarantine), | |
| 719 | - | graphicsNamespaceSalt: newGraphicsNamespaceSalt(), | |
| 720 | - | paneProcessCtx: paneProcessCtx, | |
| 721 | - | paneProcessCancel: paneProcessCancel, | |
| 722 | - | ptys: ptys, | |
| 723 | - | clock: clock, | |
| 724 | - | log: log, | |
| 725 | - | baseEnv: os.Environ(), | |
| 726 | - | shell: defaultShellCommand, | |
| 727 | - | dirOrHome: dirOrHome, | |
| 728 | - | done: make(chan struct{}), | |
| 729 | - | restoreDone: make(chan struct{}), | |
| 730 | - | animWake: make(chan struct{}, 1), | |
| 731 | - | snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity), | |
| 732 | - | snapshotAdmitted: make(map[*snapshotCapture]struct{}), | |
| 733 | - | snapshotWake: make(chan struct{}, 1), | |
| 734 | - | notices: newNoticeCenter(), | |
| 735 | - | resumeParkGrace: defaultResumeParkGrace, | |
| 736 | - | suspendedSafetyExpiry: defaultSuspendedSafetyExpiry, | |
| 702 | + | sessions: make(map[domain.SessionID]*session), | |
| 703 | + | inactive: make(map[string]inactiveSession), | |
| 704 | + | creating: make(map[string]struct{}), | |
| 705 | + | resume: newResumeCredentials(), | |
| 706 | + | suspended: make(map[*attachedClient]*suspendedAttachmentRetention), | |
| 707 | + | graphics: newGraphicsNamespacePool(), | |
| 708 | + | paneProcessCtx: paneProcessCtx, | |
| 709 | + | paneProcessCancel: paneProcessCancel, | |
| 710 | + | ptys: ptys, | |
| 711 | + | clock: clock, | |
| 712 | + | log: log, | |
| 713 | + | baseEnv: os.Environ(), | |
| 714 | + | shell: defaultShellCommand, | |
| 715 | + | dirOrHome: dirOrHome, | |
| 716 | + | done: make(chan struct{}), | |
| 717 | + | restoreDone: make(chan struct{}), | |
| 718 | + | animWake: make(chan struct{}, 1), | |
| 719 | + | snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity), | |
| 720 | + | snapshotAdmitted: make(map[*snapshotCapture]struct{}), | |
| 721 | + | snapshotWake: make(chan struct{}, 1), | |
| 722 | + | notices: newNoticeCenter(), | |
| 723 | + | resumeParkGrace: defaultResumeParkGrace, | |
| 724 | + | suspendedSafetyExpiry: defaultSuspendedSafetyExpiry, | |
| 737 | 725 | barScripts: &barScriptState{ | |
| 738 | 726 | cfg: barConfigFromDomain(domain.Defaults().Bar), | |
| 739 | 727 | outputs: make(map[domain.SessionID]barScriptOutputs), | |
| @@ -148,18 +148,9 @@ type Daemon struct { | |||||
| 148 | // re-suspension, detach, or session teardown can never be retired by a | 148 | // re-suspension, detach, or session teardown can never be retired by a | ||
| 149 | // stale timer. Guarded by mu. | 149 | // stale timer. Guarded by mu. | ||
| 150 | suspended map[*attachedClient]*suspendedAttachmentRetention | 150 | suspended map[*attachedClient]*suspendedAttachmentRetention | ||
| 151 | - | // graphicsNamespaces reserves deterministic, attachment/session-scoped Kitty | 151 | + | // graphics reserves attachment/session-scoped Kitty ID blocks. Guarded by |
| 152 | - | // ID blocks. Once a block may have reached an outer terminal it remains in | 152 | + | // mu; see graphicsNamespacePool. |
| 153 | - | // this bounded table for the daemon lifetime: side-effect Output frames have | 153 | + | graphics graphicsNamespacePool |
| 154 | - | // no terminal ACK. Pool exhaustion disables graphics for new attachments and | |||
| 155 | - | // leaves their ordinary text output intact. | |||
| 156 | - | graphicsNamespaces map[uint64]struct{} | |||
| 157 | - | graphicsNamespaceFences map[uint64]uint64 | |||
| 158 | - | graphicsNamespaceQuarantines map[uint64]*graphicsNamespaceQuarantine | |||
| 159 | - | // graphicsNamespaceSalt separates daemon lifetimes before attachment keys | |||
| 160 | - | // are hashed. Kitty IDs are terminal-global, so a restarted or neighboring | |||
| 161 | - | // daemon must not deterministically reopen the previous daemon's block. | |||
| 162 | - | graphicsNamespaceSalt uint64 | |||
| 163 | 154 | ||||
| 164 | attnMu sync.Mutex | 155 | attnMu sync.Mutex | ||
| 165 | animFrame int | 156 | animFrame int | ||
| @@ -708,32 +699,29 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt | |||||
| 708 | } | 699 | } | ||
| 709 | paneProcessCtx, paneProcessCancel := context.WithCancel(context.Background()) | 700 | paneProcessCtx, paneProcessCancel := context.WithCancel(context.Background()) | ||
| 710 | d := &Daemon{ | 701 | d := &Daemon{ | ||
| 711 | - | sessions: make(map[domain.SessionID]*session), | 702 | + | sessions: make(map[domain.SessionID]*session), |
| 712 | - | inactive: make(map[string]inactiveSession), | 703 | + | inactive: make(map[string]inactiveSession), |
| 713 | - | creating: make(map[string]struct{}), | 704 | + | creating: make(map[string]struct{}), |
| 714 | - | resume: newResumeCredentials(), | 705 | + | resume: newResumeCredentials(), |
| 715 | - | suspended: make(map[*attachedClient]*suspendedAttachmentRetention), | 706 | + | suspended: make(map[*attachedClient]*suspendedAttachmentRetention), |
| 716 | - | graphicsNamespaces: make(map[uint64]struct{}), | 707 | + | graphics: newGraphicsNamespacePool(), |
| 717 | - | graphicsNamespaceFences: make(map[uint64]uint64), | 708 | + | paneProcessCtx: paneProcessCtx, |
| 718 | - | graphicsNamespaceQuarantines: make(map[uint64]*graphicsNamespaceQuarantine), | 709 | + | paneProcessCancel: paneProcessCancel, |
| 719 | - | graphicsNamespaceSalt: newGraphicsNamespaceSalt(), | 710 | + | ptys: ptys, |
| 720 | - | paneProcessCtx: paneProcessCtx, | 711 | + | clock: clock, |
| 721 | - | paneProcessCancel: paneProcessCancel, | 712 | + | log: log, |
| 722 | - | ptys: ptys, | 713 | + | baseEnv: os.Environ(), |
| 723 | - | clock: clock, | 714 | + | shell: defaultShellCommand, |
| 724 | - | log: log, | 715 | + | dirOrHome: dirOrHome, |
| 725 | - | baseEnv: os.Environ(), | 716 | + | done: make(chan struct{}), |
| 726 | - | shell: defaultShellCommand, | 717 | + | restoreDone: make(chan struct{}), |
| 727 | - | dirOrHome: dirOrHome, | 718 | + | animWake: make(chan struct{}, 1), |
| 728 | - | done: make(chan struct{}), | 719 | + | snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity), |
| 729 | - | restoreDone: make(chan struct{}), | 720 | + | snapshotAdmitted: make(map[*snapshotCapture]struct{}), |
| 730 | - | animWake: make(chan struct{}, 1), | 721 | + | snapshotWake: make(chan struct{}, 1), |
| 731 | - | snapshotJobs: make(chan *snapshotCapture, snapshotQueueCapacity), | 722 | + | notices: newNoticeCenter(), |
| 732 | - | snapshotAdmitted: make(map[*snapshotCapture]struct{}), | 723 | + | resumeParkGrace: defaultResumeParkGrace, |
| 733 | - | snapshotWake: make(chan struct{}, 1), | 724 | + | suspendedSafetyExpiry: defaultSuspendedSafetyExpiry, |
| 734 | - | notices: newNoticeCenter(), | |||
| 735 | - | resumeParkGrace: defaultResumeParkGrace, | |||
| 736 | - | suspendedSafetyExpiry: defaultSuspendedSafetyExpiry, | |||
| 737 | barScripts: &barScriptState{ | 725 | barScripts: &barScriptState{ | ||
| 738 | cfg: barConfigFromDomain(domain.Defaults().Bar), | 726 | cfg: barConfigFromDomain(domain.Defaults().Bar), | ||
| 739 | outputs: make(map[domain.SessionID]barScriptOutputs), | 727 | outputs: make(map[domain.SessionID]barScriptOutputs), | ||
A internal/usecase/daemon/graphics_namespace_pool.go +116 -0
| @@ -0,0 +1,116 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "crypto/sha256" | |
| 5 | + | "encoding/binary" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | type graphicsNamespaceQuarantine struct { | |
| 9 | + | base uint64 | |
| 10 | + | fence uint64 | |
| 11 | + | } | |
| 12 | + | ||
| 13 | + | // graphicsNamespacePool reserves deterministic, attachment/session-scoped Kitty | |
| 14 | + | // ID blocks. Once a block may have reached an outer terminal it remains in | |
| 15 | + | // this bounded table for the daemon lifetime: side-effect Output frames have | |
| 16 | + | // no terminal ACK. Pool exhaustion disables graphics for new attachments and | |
| 17 | + | // leaves their ordinary text output intact. | |
| 18 | + | // | |
| 19 | + | // The pool is guarded by Daemon.mu and has no lock of its own: every method | |
| 20 | + | // requires the caller to hold it. The zero value is usable; the maps are | |
| 21 | + | // created lazily and a zero salt simply skips salting. | |
| 22 | + | type graphicsNamespacePool struct { | |
| 23 | + | reserved map[uint64]struct{} | |
| 24 | + | fences map[uint64]uint64 | |
| 25 | + | quarantines map[uint64]*graphicsNamespaceQuarantine | |
| 26 | + | // salt separates daemon lifetimes before attachment keys are hashed. Kitty | |
| 27 | + | // IDs are terminal-global, so a restarted or neighboring daemon must not | |
| 28 | + | // deterministically reopen the previous daemon's block. | |
| 29 | + | salt uint64 | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func newGraphicsNamespacePool() graphicsNamespacePool { | |
| 33 | + | p := graphicsNamespacePool{salt: newGraphicsNamespaceSalt()} | |
| 34 | + | p.init() | |
| 35 | + | return p | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | func (p *graphicsNamespacePool) init() { | |
| 39 | + | if p.reserved == nil { | |
| 40 | + | p.reserved = make(map[uint64]struct{}) | |
| 41 | + | } | |
| 42 | + | if p.fences == nil { | |
| 43 | + | p.fences = make(map[uint64]uint64) | |
| 44 | + | } | |
| 45 | + | if p.quarantines == nil { | |
| 46 | + | p.quarantines = make(map[uint64]*graphicsNamespaceQuarantine) | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) { | |
| 51 | + | p.init() | |
| 52 | + | hashInput := []byte(key) | |
| 53 | + | if p.salt != 0 { | |
| 54 | + | salt := make([]byte, 8, 8+len(key)) | |
| 55 | + | binary.BigEndian.PutUint64(salt, p.salt) | |
| 56 | + | hashInput = append(salt, hashInput...) | |
| 57 | + | } | |
| 58 | + | digest := sha256.Sum256(hashInput) | |
| 59 | + | preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount | |
| 60 | + | for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ { | |
| 61 | + | block := (preferred + offset) % graphicsIDNamespaceCount | |
| 62 | + | if _, exists := p.reserved[block]; exists { | |
| 63 | + | continue | |
| 64 | + | } | |
| 65 | + | fence := nextGraphicsNamespaceFence() | |
| 66 | + | p.reserved[block] = struct{}{} | |
| 67 | + | p.fences[block] = fence | |
| 68 | + | return block*graphicsIDNamespaceSize + 1, fence | |
| 69 | + | } | |
| 70 | + | return 0, 0 | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // releaseBase is the legacy base-only release. A quarantined block is never | |
| 74 | + | // released by it. | |
| 75 | + | func (p *graphicsNamespacePool) releaseBase(base uint64) { | |
| 76 | + | if base == 0 || base%graphicsIDNamespaceSize != 1 { | |
| 77 | + | return | |
| 78 | + | } | |
| 79 | + | block := (base - 1) / graphicsIDNamespaceSize | |
| 80 | + | if _, quarantined := p.quarantines[block]; quarantined { | |
| 81 | + | return | |
| 82 | + | } | |
| 83 | + | delete(p.reserved, block) | |
| 84 | + | delete(p.fences, block) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) { | |
| 88 | + | if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | |
| 89 | + | return | |
| 90 | + | } | |
| 91 | + | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |
| 92 | + | if _, quarantined := p.quarantines[block]; quarantined { | |
| 93 | + | return | |
| 94 | + | } | |
| 95 | + | if current := p.fences[block]; current != 0 && current != state.namespaceFence { | |
| 96 | + | return | |
| 97 | + | } | |
| 98 | + | delete(p.reserved, block) | |
| 99 | + | delete(p.fences, block) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func (p *graphicsNamespacePool) quarantine(state *graphicsOutputState) *graphicsNamespaceQuarantine { | |
| 103 | + | if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | |
| 104 | + | return nil | |
| 105 | + | } | |
| 106 | + | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |
| 107 | + | p.init() | |
| 108 | + | if _, exists := p.quarantines[block]; exists { | |
| 109 | + | return nil | |
| 110 | + | } | |
| 111 | + | p.reserved[block] = struct{}{} | |
| 112 | + | p.fences[block] = state.namespaceFence | |
| 113 | + | q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence} | |
| 114 | + | p.quarantines[block] = q | |
| 115 | + | return q | |
| 116 | + | } | |
| @@ -0,0 +1,116 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "crypto/sha256" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | type graphicsNamespaceQuarantine struct { | |||
| 9 | + | base uint64 | |||
| 10 | + | fence uint64 | |||
| 11 | + | } | |||
| 12 | + | ||||
| 13 | + | // graphicsNamespacePool reserves deterministic, attachment/session-scoped Kitty | |||
| 14 | + | // ID blocks. Once a block may have reached an outer terminal it remains in | |||
| 15 | + | // this bounded table for the daemon lifetime: side-effect Output frames have | |||
| 16 | + | // no terminal ACK. Pool exhaustion disables graphics for new attachments and | |||
| 17 | + | // leaves their ordinary text output intact. | |||
| 18 | + | // | |||
| 19 | + | // The pool is guarded by Daemon.mu and has no lock of its own: every method | |||
| 20 | + | // requires the caller to hold it. The zero value is usable; the maps are | |||
| 21 | + | // created lazily and a zero salt simply skips salting. | |||
| 22 | + | type graphicsNamespacePool struct { | |||
| 23 | + | reserved map[uint64]struct{} | |||
| 24 | + | fences map[uint64]uint64 | |||
| 25 | + | quarantines map[uint64]*graphicsNamespaceQuarantine | |||
| 26 | + | // salt separates daemon lifetimes before attachment keys are hashed. Kitty | |||
| 27 | + | // IDs are terminal-global, so a restarted or neighboring daemon must not | |||
| 28 | + | // deterministically reopen the previous daemon's block. | |||
| 29 | + | salt uint64 | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func newGraphicsNamespacePool() graphicsNamespacePool { | |||
| 33 | + | p := graphicsNamespacePool{salt: newGraphicsNamespaceSalt()} | |||
| 34 | + | p.init() | |||
| 35 | + | return p | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | func (p *graphicsNamespacePool) init() { | |||
| 39 | + | if p.reserved == nil { | |||
| 40 | + | p.reserved = make(map[uint64]struct{}) | |||
| 41 | + | } | |||
| 42 | + | if p.fences == nil { | |||
| 43 | + | p.fences = make(map[uint64]uint64) | |||
| 44 | + | } | |||
| 45 | + | if p.quarantines == nil { | |||
| 46 | + | p.quarantines = make(map[uint64]*graphicsNamespaceQuarantine) | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func (p *graphicsNamespacePool) reserveLease(key string) (uint64, uint64) { | |||
| 51 | + | p.init() | |||
| 52 | + | hashInput := []byte(key) | |||
| 53 | + | if p.salt != 0 { | |||
| 54 | + | salt := make([]byte, 8, 8+len(key)) | |||
| 55 | + | binary.BigEndian.PutUint64(salt, p.salt) | |||
| 56 | + | hashInput = append(salt, hashInput...) | |||
| 57 | + | } | |||
| 58 | + | digest := sha256.Sum256(hashInput) | |||
| 59 | + | preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount | |||
| 60 | + | for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ { | |||
| 61 | + | block := (preferred + offset) % graphicsIDNamespaceCount | |||
| 62 | + | if _, exists := p.reserved[block]; exists { | |||
| 63 | + | continue | |||
| 64 | + | } | |||
| 65 | + | fence := nextGraphicsNamespaceFence() | |||
| 66 | + | p.reserved[block] = struct{}{} | |||
| 67 | + | p.fences[block] = fence | |||
| 68 | + | return block*graphicsIDNamespaceSize + 1, fence | |||
| 69 | + | } | |||
| 70 | + | return 0, 0 | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // releaseBase is the legacy base-only release. A quarantined block is never | |||
| 74 | + | // released by it. | |||
| 75 | + | func (p *graphicsNamespacePool) releaseBase(base uint64) { | |||
| 76 | + | if base == 0 || base%graphicsIDNamespaceSize != 1 { | |||
| 77 | + | return | |||
| 78 | + | } | |||
| 79 | + | block := (base - 1) / graphicsIDNamespaceSize | |||
| 80 | + | if _, quarantined := p.quarantines[block]; quarantined { | |||
| 81 | + | return | |||
| 82 | + | } | |||
| 83 | + | delete(p.reserved, block) | |||
| 84 | + | delete(p.fences, block) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func (p *graphicsNamespacePool) releaseLease(state *graphicsOutputState) { | |||
| 88 | + | if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | |||
| 89 | + | return | |||
| 90 | + | } | |||
| 91 | + | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |||
| 92 | + | if _, quarantined := p.quarantines[block]; quarantined { | |||
| 93 | + | return | |||
| 94 | + | } | |||
| 95 | + | if current := p.fences[block]; current != 0 && current != state.namespaceFence { | |||
| 96 | + | return | |||
| 97 | + | } | |||
| 98 | + | delete(p.reserved, block) | |||
| 99 | + | delete(p.fences, block) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func (p *graphicsNamespacePool) quarantine(state *graphicsOutputState) *graphicsNamespaceQuarantine { | |||
| 103 | + | if state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | |||
| 104 | + | return nil | |||
| 105 | + | } | |||
| 106 | + | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |||
| 107 | + | p.init() | |||
| 108 | + | if _, exists := p.quarantines[block]; exists { | |||
| 109 | + | return nil | |||
| 110 | + | } | |||
| 111 | + | p.reserved[block] = struct{}{} | |||
| 112 | + | p.fences[block] = state.namespaceFence | |||
| 113 | + | q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence} | |||
| 114 | + | p.quarantines[block] = q | |||
| 115 | + | return q | |||
| 116 | + | } | |||
M internal/usecase/daemon/graphics_output.go +7 -70
| @@ -5,7 +5,6 @@ import ( | |||
| 5 | 5 | "compress/zlib" | |
| 6 | 6 | "crypto/sha256" | |
| 7 | 7 | "encoding/base64" | |
| 8 | - | "encoding/binary" | |
| 9 | 8 | "errors" | |
| 10 | 9 | "fmt" | |
| 11 | 10 | "sort" | |
| @@ -106,11 +105,6 @@ var ( | |||
| 106 | 105 | graphicsNamespaceFence atomic.Uint64 | |
| 107 | 106 | ) | |
| 108 | 107 | ||
| 109 | - | type graphicsNamespaceQuarantine struct { | |
| 110 | - | base uint64 | |
| 111 | - | fence uint64 | |
| 112 | - | } | |
| 113 | - | ||
| 114 | 108 | func nextGraphicsNamespaceFence() uint64 { | |
| 115 | 109 | fence := graphicsNamespaceFence.Add(1) | |
| 116 | 110 | if fence == 0 { | |
| @@ -167,34 +161,7 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64 | |||
| 167 | 161 | if d == nil { | |
| 168 | 162 | return 0, 0 | |
| 169 | 163 | } | |
| 170 | - | if d.graphicsNamespaces == nil { | |
| 171 | - | d.graphicsNamespaces = make(map[uint64]struct{}) | |
| 172 | - | } | |
| 173 | - | if d.graphicsNamespaceFences == nil { | |
| 174 | - | d.graphicsNamespaceFences = make(map[uint64]uint64) | |
| 175 | - | } | |
| 176 | - | if d.graphicsNamespaceQuarantines == nil { | |
| 177 | - | d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine) | |
| 178 | - | } | |
| 179 | - | hashInput := []byte(key) | |
| 180 | - | if d.graphicsNamespaceSalt != 0 { | |
| 181 | - | salt := make([]byte, 8, 8+len(key)) | |
| 182 | - | binary.BigEndian.PutUint64(salt, d.graphicsNamespaceSalt) | |
| 183 | - | hashInput = append(salt, hashInput...) | |
| 184 | - | } | |
| 185 | - | digest := sha256.Sum256(hashInput) | |
| 186 | - | preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount | |
| 187 | - | for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ { | |
| 188 | - | block := (preferred + offset) % graphicsIDNamespaceCount | |
| 189 | - | if _, exists := d.graphicsNamespaces[block]; exists { | |
| 190 | - | continue | |
| 191 | - | } | |
| 192 | - | fence := nextGraphicsNamespaceFence() | |
| 193 | - | d.graphicsNamespaces[block] = struct{}{} | |
| 194 | - | d.graphicsNamespaceFences[block] = fence | |
| 195 | - | return block*graphicsIDNamespaceSize + 1, fence | |
| 196 | - | } | |
| 197 | - | return 0, 0 | |
| 164 | + | return d.graphics.reserveLease(key) | |
| 198 | 165 | } | |
| 199 | 166 | ||
| 200 | 167 | func (d *Daemon) releaseGraphicsNamespace(base uint64) { | |
| @@ -210,30 +177,17 @@ func (d *Daemon) releaseGraphicsNamespace(base uint64) { | |||
| 210 | 177 | // already held. A quarantined block is intentionally not released by this | |
| 211 | 178 | // legacy base-only helper; only its fenced cleanup lifecycle may retire it. | |
| 212 | 179 | func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) { | |
| 213 | - | if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 { | |
| 214 | - | return | |
| 215 | - | } | |
| 216 | - | block := (base - 1) / graphicsIDNamespaceSize | |
| 217 | - | if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined { | |
| 180 | + | if d == nil { | |
| 218 | 181 | return | |
| 219 | 182 | } | |
| 220 | - | delete(d.graphicsNamespaces, block) | |
| 221 | - | delete(d.graphicsNamespaceFences, block) | |
| 183 | + | d.graphics.releaseBase(base) | |
| 222 | 184 | } | |
| 223 | 185 | ||
| 224 | 186 | func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) { | |
| 225 | - | if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | |
| 226 | - | return | |
| 227 | - | } | |
| 228 | - | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |
| 229 | - | if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined { | |
| 230 | - | return | |
| 231 | - | } | |
| 232 | - | if current := d.graphicsNamespaceFences[block]; current != 0 && current != state.namespaceFence { | |
| 187 | + | if d == nil { | |
| 233 | 188 | return | |
| 234 | 189 | } | |
| 235 | - | delete(d.graphicsNamespaces, block) | |
| 236 | - | delete(d.graphicsNamespaceFences, block) | |
| 190 | + | d.graphics.releaseLease(state) | |
| 237 | 191 | } | |
| 238 | 192 | ||
| 239 | 193 | // quarantineGraphicsNamespaceLocked fences one exact namespace instance for | |
| @@ -241,27 +195,10 @@ func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) | |||
| 241 | 195 | // completion must never release a block that may have reached an outer terminal. | |
| 242 | 196 | // The caller holds d.mu. | |
| 243 | 197 | func (d *Daemon) quarantineGraphicsNamespaceLocked(state *graphicsOutputState) *graphicsNamespaceQuarantine { | |
| 244 | - | if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | |
| 245 | - | return nil | |
| 246 | - | } | |
| 247 | - | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |
| 248 | - | if d.graphicsNamespaces == nil { | |
| 249 | - | d.graphicsNamespaces = make(map[uint64]struct{}) | |
| 250 | - | } | |
| 251 | - | if d.graphicsNamespaceFences == nil { | |
| 252 | - | d.graphicsNamespaceFences = make(map[uint64]uint64) | |
| 253 | - | } | |
| 254 | - | if d.graphicsNamespaceQuarantines == nil { | |
| 255 | - | d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine) | |
| 256 | - | } | |
| 257 | - | if _, exists := d.graphicsNamespaceQuarantines[block]; exists { | |
| 198 | + | if d == nil { | |
| 258 | 199 | return nil | |
| 259 | 200 | } | |
| 260 | - | d.graphicsNamespaces[block] = struct{}{} | |
| 261 | - | d.graphicsNamespaceFences[block] = state.namespaceFence | |
| 262 | - | q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence} | |
| 263 | - | d.graphicsNamespaceQuarantines[block] = q | |
| 264 | - | return q | |
| 201 | + | return d.graphics.quarantine(state) | |
| 265 | 202 | } | |
| 266 | 203 | ||
| 267 | 204 | func cloneGraphicsOutputState(in *graphicsOutputState) graphicsOutputState { | |
| @@ -5,7 +5,6 @@ import ( | |||||
| 5 | "compress/zlib" | 5 | "compress/zlib" | ||
| 6 | "crypto/sha256" | 6 | "crypto/sha256" | ||
| 7 | "encoding/base64" | 7 | "encoding/base64" | ||
| 8 | - | "encoding/binary" | |||
| 9 | "errors" | 8 | "errors" | ||
| 10 | "fmt" | 9 | "fmt" | ||
| 11 | "sort" | 10 | "sort" | ||
| @@ -106,11 +105,6 @@ var ( | |||||
| 106 | graphicsNamespaceFence atomic.Uint64 | 105 | graphicsNamespaceFence atomic.Uint64 | ||
| 107 | ) | 106 | ) | ||
| 108 | 107 | ||||
| 109 | - | type graphicsNamespaceQuarantine struct { | |||
| 110 | - | base uint64 | |||
| 111 | - | fence uint64 | |||
| 112 | - | } | |||
| 113 | - | ||||
| 114 | func nextGraphicsNamespaceFence() uint64 { | 108 | func nextGraphicsNamespaceFence() uint64 { | ||
| 115 | fence := graphicsNamespaceFence.Add(1) | 109 | fence := graphicsNamespaceFence.Add(1) | ||
| 116 | if fence == 0 { | 110 | if fence == 0 { | ||
| @@ -167,34 +161,7 @@ func (d *Daemon) reserveGraphicsNamespaceLeaseLocked(key string) (uint64, uint64 | |||||
| 167 | if d == nil { | 161 | if d == nil { | ||
| 168 | return 0, 0 | 162 | return 0, 0 | ||
| 169 | } | 163 | } | ||
| 170 | - | if d.graphicsNamespaces == nil { | 164 | + | return d.graphics.reserveLease(key) |
| 171 | - | d.graphicsNamespaces = make(map[uint64]struct{}) | |||
| 172 | - | } | |||
| 173 | - | if d.graphicsNamespaceFences == nil { | |||
| 174 | - | d.graphicsNamespaceFences = make(map[uint64]uint64) | |||
| 175 | - | } | |||
| 176 | - | if d.graphicsNamespaceQuarantines == nil { | |||
| 177 | - | d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine) | |||
| 178 | - | } | |||
| 179 | - | hashInput := []byte(key) | |||
| 180 | - | if d.graphicsNamespaceSalt != 0 { | |||
| 181 | - | salt := make([]byte, 8, 8+len(key)) | |||
| 182 | - | binary.BigEndian.PutUint64(salt, d.graphicsNamespaceSalt) | |||
| 183 | - | hashInput = append(salt, hashInput...) | |||
| 184 | - | } | |||
| 185 | - | digest := sha256.Sum256(hashInput) | |||
| 186 | - | preferred := binary.BigEndian.Uint64(digest[:8]) % graphicsIDNamespaceCount | |||
| 187 | - | for offset := uint64(0); offset < graphicsIDNamespaceCount; offset++ { | |||
| 188 | - | block := (preferred + offset) % graphicsIDNamespaceCount | |||
| 189 | - | if _, exists := d.graphicsNamespaces[block]; exists { | |||
| 190 | - | continue | |||
| 191 | - | } | |||
| 192 | - | fence := nextGraphicsNamespaceFence() | |||
| 193 | - | d.graphicsNamespaces[block] = struct{}{} | |||
| 194 | - | d.graphicsNamespaceFences[block] = fence | |||
| 195 | - | return block*graphicsIDNamespaceSize + 1, fence | |||
| 196 | - | } | |||
| 197 | - | return 0, 0 | |||
| 198 | } | 165 | } | ||
| 199 | 166 | ||||
| 200 | func (d *Daemon) releaseGraphicsNamespace(base uint64) { | 167 | func (d *Daemon) releaseGraphicsNamespace(base uint64) { | ||
| @@ -210,30 +177,17 @@ func (d *Daemon) releaseGraphicsNamespace(base uint64) { | |||||
| 210 | // already held. A quarantined block is intentionally not released by this | 177 | // already held. A quarantined block is intentionally not released by this | ||
| 211 | // legacy base-only helper; only its fenced cleanup lifecycle may retire it. | 178 | // legacy base-only helper; only its fenced cleanup lifecycle may retire it. | ||
| 212 | func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) { | 179 | func (d *Daemon) releaseGraphicsNamespaceLocked(base uint64) { | ||
| 213 | - | if d == nil || base == 0 || base%graphicsIDNamespaceSize != 1 { | 180 | + | if d == nil { |
| 214 | - | return | |||
| 215 | - | } | |||
| 216 | - | block := (base - 1) / graphicsIDNamespaceSize | |||
| 217 | - | if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined { | |||
| 218 | return | 181 | return | ||
| 219 | } | 182 | } | ||
| 220 | - | delete(d.graphicsNamespaces, block) | 183 | + | d.graphics.releaseBase(base) |
| 221 | - | delete(d.graphicsNamespaceFences, block) | |||
| 222 | } | 184 | } | ||
| 223 | 185 | ||||
| 224 | func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) { | 186 | func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) { | ||
| 225 | - | if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | 187 | + | if d == nil { |
| 226 | - | return | |||
| 227 | - | } | |||
| 228 | - | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |||
| 229 | - | if _, quarantined := d.graphicsNamespaceQuarantines[block]; quarantined { | |||
| 230 | - | return | |||
| 231 | - | } | |||
| 232 | - | if current := d.graphicsNamespaceFences[block]; current != 0 && current != state.namespaceFence { | |||
| 233 | return | 188 | return | ||
| 234 | } | 189 | } | ||
| 235 | - | delete(d.graphicsNamespaces, block) | 190 | + | d.graphics.releaseLease(state) |
| 236 | - | delete(d.graphicsNamespaceFences, block) | |||
| 237 | } | 191 | } | ||
| 238 | 192 | ||||
| 239 | // quarantineGraphicsNamespaceLocked fences one exact namespace instance for | 193 | // quarantineGraphicsNamespaceLocked fences one exact namespace instance for | ||
| @@ -241,27 +195,10 @@ func (d *Daemon) releaseGraphicsNamespaceLeaseLocked(state *graphicsOutputState) | |||||
| 241 | // completion must never release a block that may have reached an outer terminal. | 195 | // completion must never release a block that may have reached an outer terminal. | ||
| 242 | // The caller holds d.mu. | 196 | // The caller holds d.mu. | ||
| 243 | func (d *Daemon) quarantineGraphicsNamespaceLocked(state *graphicsOutputState) *graphicsNamespaceQuarantine { | 197 | func (d *Daemon) quarantineGraphicsNamespaceLocked(state *graphicsOutputState) *graphicsNamespaceQuarantine { | ||
| 244 | - | if d == nil || state == nil || state.namespaceBase == 0 || state.namespaceBase%graphicsIDNamespaceSize != 1 { | 198 | + | if d == nil { |
| 245 | - | return nil | |||
| 246 | - | } | |||
| 247 | - | block := (state.namespaceBase - 1) / graphicsIDNamespaceSize | |||
| 248 | - | if d.graphicsNamespaces == nil { | |||
| 249 | - | d.graphicsNamespaces = make(map[uint64]struct{}) | |||
| 250 | - | } | |||
| 251 | - | if d.graphicsNamespaceFences == nil { | |||
| 252 | - | d.graphicsNamespaceFences = make(map[uint64]uint64) | |||
| 253 | - | } | |||
| 254 | - | if d.graphicsNamespaceQuarantines == nil { | |||
| 255 | - | d.graphicsNamespaceQuarantines = make(map[uint64]*graphicsNamespaceQuarantine) | |||
| 256 | - | } | |||
| 257 | - | if _, exists := d.graphicsNamespaceQuarantines[block]; exists { | |||
| 258 | return nil | 199 | return nil | ||
| 259 | } | 200 | } | ||
| 260 | - | d.graphicsNamespaces[block] = struct{}{} | 201 | + | return d.graphics.quarantine(state) |
| 261 | - | d.graphicsNamespaceFences[block] = state.namespaceFence | |||
| 262 | - | q := &graphicsNamespaceQuarantine{base: state.namespaceBase, fence: state.namespaceFence} | |||
| 263 | - | d.graphicsNamespaceQuarantines[block] = q | |||
| 264 | - | return q | |||
| 265 | } | 202 | } | ||
| 266 | 203 | ||||
| 267 | func cloneGraphicsOutputState(in *graphicsOutputState) graphicsOutputState { | 204 | func cloneGraphicsOutputState(in *graphicsOutputState) graphicsOutputState { | ||
M internal/usecase/daemon/graphics_output_test.go +9 -9
| @@ -398,13 +398,13 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T) | |||
| 398 | 398 | done: make(chan struct{}), | |
| 399 | 399 | } | |
| 400 | 400 | d.mu.Lock() | |
| 401 | - | d.graphicsNamespaces[block] = struct{}{} | |
| 401 | + | d.graphics.reserved[block] = struct{}{} | |
| 402 | 402 | d.resume.parked[1] = parked | |
| 403 | 403 | d.mu.Unlock() | |
| 404 | 404 | d.expireParked(1, parked) | |
| 405 | 405 | require.Nil(t, parked.ac.output.graphicsOutput) | |
| 406 | 406 | d.mu.Lock() | |
| 407 | - | _, reserved := d.graphicsNamespaces[block] | |
| 407 | + | _, reserved := d.graphics.reserved[block] | |
| 408 | 408 | d.mu.Unlock() | |
| 409 | 409 | require.False(t, reserved, "park expiry with no outer objects may release the attachment namespace") | |
| 410 | 410 | ||
| @@ -417,12 +417,12 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T) | |||
| 417 | 417 | output: attachmentOutputWithGraphics(failedState), | |
| 418 | 418 | } | |
| 419 | 419 | d.mu.Lock() | |
| 420 | - | d.graphicsNamespaces[block] = struct{}{} | |
| 420 | + | d.graphics.reserved[block] = struct{}{} | |
| 421 | 421 | d.mu.Unlock() | |
| 422 | 422 | require.Error(t, d.cleanupAttachmentOutput(failed), "failing transport must surface the cleanup send error") | |
| 423 | 423 | require.Nil(t, failed.output.graphicsOutput) | |
| 424 | 424 | d.mu.Lock() | |
| 425 | - | _, reserved = d.graphicsNamespaces[block] | |
| 425 | + | _, reserved = d.graphics.reserved[block] | |
| 426 | 426 | d.mu.Unlock() | |
| 427 | 427 | require.True(t, reserved, "failed terminal cleanup must quarantine the namespace") | |
| 428 | 428 | } | |
| @@ -453,7 +453,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterSocketSendSuccessWithoutTerminalF | |||
| 453 | 453 | require.Zero(t, cleanup.New, "graphics cleanup is an unacknowledged side-effect frame") | |
| 454 | 454 | ||
| 455 | 455 | d.mu.Lock() | |
| 456 | - | _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 456 | + | _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 457 | 457 | newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | |
| 458 | 458 | d.mu.Unlock() | |
| 459 | 459 | require.True(t, quarantined, "socket-send success cannot prove terminal cleanup") | |
| @@ -491,7 +491,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterFinalDeleteAndParkExpiry(t *testi | |||
| 491 | 491 | d.expireParked(1, parked) | |
| 492 | 492 | ||
| 493 | 493 | d.mu.Lock() | |
| 494 | - | _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 494 | + | _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 495 | 495 | newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | |
| 496 | 496 | d.mu.Unlock() | |
| 497 | 497 | require.True(t, quarantined, "an apparently empty parked state still has unacknowledged terminal history") | |
| @@ -502,7 +502,7 @@ func TestGraphicsNamespacePoolExhaustionFallsBackToText(t *testing.T) { | |||
| 502 | 502 | d := newTestDaemon(t, nil, stubClock{}) | |
| 503 | 503 | d.mu.Lock() | |
| 504 | 504 | for block := uint64(0); block < graphicsIDNamespaceCount; block++ { | |
| 505 | - | d.graphicsNamespaces[block] = struct{}{} | |
| 505 | + | d.graphics.reserved[block] = struct{}{} | |
| 506 | 506 | } | |
| 507 | 507 | ac := d.prepareAttachedClientLocked( | |
| 508 | 508 | &session{sessionCore: sessionCore{id: "work"}}, | |
| @@ -552,7 +552,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) { | |||
| 552 | 552 | ||
| 553 | 553 | d.mu.Lock() | |
| 554 | 554 | newBase, newFence := d.reserveGraphicsNamespaceLeaseLocked(key) | |
| 555 | - | _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 555 | + | _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 556 | 556 | d.mu.Unlock() | |
| 557 | 557 | require.True(t, quarantined, "timed-out cleanup must keep its namespace quarantined") | |
| 558 | 558 | require.NotEqual(t, oldBase, newBase, "new allocation must skip the quarantined namespace") | |
| @@ -575,7 +575,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) { | |||
| 575 | 575 | require.Contains(t, string(staleOutput.Data), "i="+strconv.FormatUint(oldID, 10)) | |
| 576 | 576 | require.NotContains(t, string(staleOutput.Data), "i="+strconv.FormatUint(newID, 10), "late cleanup must never target the fresh attachment") | |
| 577 | 577 | d.mu.Lock() | |
| 578 | - | _, retained := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 578 | + | _, retained := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] | |
| 579 | 579 | d.mu.Unlock() | |
| 580 | 580 | require.True(t, retained, "late transport success is not a terminal ACK and must not release the namespace") | |
| 581 | 581 | } | |
| @@ -398,13 +398,13 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T) | |||||
| 398 | done: make(chan struct{}), | 398 | done: make(chan struct{}), | ||
| 399 | } | 399 | } | ||
| 400 | d.mu.Lock() | 400 | d.mu.Lock() | ||
| 401 | - | d.graphicsNamespaces[block] = struct{}{} | 401 | + | d.graphics.reserved[block] = struct{}{} |
| 402 | d.resume.parked[1] = parked | 402 | d.resume.parked[1] = parked | ||
| 403 | d.mu.Unlock() | 403 | d.mu.Unlock() | ||
| 404 | d.expireParked(1, parked) | 404 | d.expireParked(1, parked) | ||
| 405 | require.Nil(t, parked.ac.output.graphicsOutput) | 405 | require.Nil(t, parked.ac.output.graphicsOutput) | ||
| 406 | d.mu.Lock() | 406 | d.mu.Lock() | ||
| 407 | - | _, reserved := d.graphicsNamespaces[block] | 407 | + | _, reserved := d.graphics.reserved[block] |
| 408 | d.mu.Unlock() | 408 | d.mu.Unlock() | ||
| 409 | require.False(t, reserved, "park expiry with no outer objects may release the attachment namespace") | 409 | require.False(t, reserved, "park expiry with no outer objects may release the attachment namespace") | ||
| 410 | 410 | ||||
| @@ -417,12 +417,12 @@ func TestGraphicsNamespaceQuarantinesOnParkExpiryAndFailedCleanup(t *testing.T) | |||||
| 417 | output: attachmentOutputWithGraphics(failedState), | 417 | output: attachmentOutputWithGraphics(failedState), | ||
| 418 | } | 418 | } | ||
| 419 | d.mu.Lock() | 419 | d.mu.Lock() | ||
| 420 | - | d.graphicsNamespaces[block] = struct{}{} | 420 | + | d.graphics.reserved[block] = struct{}{} |
| 421 | d.mu.Unlock() | 421 | d.mu.Unlock() | ||
| 422 | require.Error(t, d.cleanupAttachmentOutput(failed), "failing transport must surface the cleanup send error") | 422 | require.Error(t, d.cleanupAttachmentOutput(failed), "failing transport must surface the cleanup send error") | ||
| 423 | require.Nil(t, failed.output.graphicsOutput) | 423 | require.Nil(t, failed.output.graphicsOutput) | ||
| 424 | d.mu.Lock() | 424 | d.mu.Lock() | ||
| 425 | - | _, reserved = d.graphicsNamespaces[block] | 425 | + | _, reserved = d.graphics.reserved[block] |
| 426 | d.mu.Unlock() | 426 | d.mu.Unlock() | ||
| 427 | require.True(t, reserved, "failed terminal cleanup must quarantine the namespace") | 427 | require.True(t, reserved, "failed terminal cleanup must quarantine the namespace") | ||
| 428 | } | 428 | } | ||
| @@ -453,7 +453,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterSocketSendSuccessWithoutTerminalF | |||||
| 453 | require.Zero(t, cleanup.New, "graphics cleanup is an unacknowledged side-effect frame") | 453 | require.Zero(t, cleanup.New, "graphics cleanup is an unacknowledged side-effect frame") | ||
| 454 | 454 | ||||
| 455 | d.mu.Lock() | 455 | d.mu.Lock() | ||
| 456 | - | _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | 456 | + | _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] |
| 457 | newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | 457 | newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | ||
| 458 | d.mu.Unlock() | 458 | d.mu.Unlock() | ||
| 459 | require.True(t, quarantined, "socket-send success cannot prove terminal cleanup") | 459 | require.True(t, quarantined, "socket-send success cannot prove terminal cleanup") | ||
| @@ -491,7 +491,7 @@ func TestGraphicsNamespaceStaysQuarantinedAfterFinalDeleteAndParkExpiry(t *testi | |||||
| 491 | d.expireParked(1, parked) | 491 | d.expireParked(1, parked) | ||
| 492 | 492 | ||||
| 493 | d.mu.Lock() | 493 | d.mu.Lock() | ||
| 494 | - | _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | 494 | + | _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] |
| 495 | newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | 495 | newBase, _ := d.reserveGraphicsNamespaceLeaseLocked(key) | ||
| 496 | d.mu.Unlock() | 496 | d.mu.Unlock() | ||
| 497 | require.True(t, quarantined, "an apparently empty parked state still has unacknowledged terminal history") | 497 | require.True(t, quarantined, "an apparently empty parked state still has unacknowledged terminal history") | ||
| @@ -502,7 +502,7 @@ func TestGraphicsNamespacePoolExhaustionFallsBackToText(t *testing.T) { | |||||
| 502 | d := newTestDaemon(t, nil, stubClock{}) | 502 | d := newTestDaemon(t, nil, stubClock{}) | ||
| 503 | d.mu.Lock() | 503 | d.mu.Lock() | ||
| 504 | for block := uint64(0); block < graphicsIDNamespaceCount; block++ { | 504 | for block := uint64(0); block < graphicsIDNamespaceCount; block++ { | ||
| 505 | - | d.graphicsNamespaces[block] = struct{}{} | 505 | + | d.graphics.reserved[block] = struct{}{} |
| 506 | } | 506 | } | ||
| 507 | ac := d.prepareAttachedClientLocked( | 507 | ac := d.prepareAttachedClientLocked( | ||
| 508 | &session{sessionCore: sessionCore{id: "work"}}, | 508 | &session{sessionCore: sessionCore{id: "work"}}, | ||
| @@ -552,7 +552,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) { | |||||
| 552 | 552 | ||||
| 553 | d.mu.Lock() | 553 | d.mu.Lock() | ||
| 554 | newBase, newFence := d.reserveGraphicsNamespaceLeaseLocked(key) | 554 | newBase, newFence := d.reserveGraphicsNamespaceLeaseLocked(key) | ||
| 555 | - | _, quarantined := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | 555 | + | _, quarantined := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] |
| 556 | d.mu.Unlock() | 556 | d.mu.Unlock() | ||
| 557 | require.True(t, quarantined, "timed-out cleanup must keep its namespace quarantined") | 557 | require.True(t, quarantined, "timed-out cleanup must keep its namespace quarantined") | ||
| 558 | require.NotEqual(t, oldBase, newBase, "new allocation must skip the quarantined namespace") | 558 | require.NotEqual(t, oldBase, newBase, "new allocation must skip the quarantined namespace") | ||
| @@ -575,7 +575,7 @@ func TestGraphicsNamespaceQuarantineFencesTimedOutLateDelete(t *testing.T) { | |||||
| 575 | require.Contains(t, string(staleOutput.Data), "i="+strconv.FormatUint(oldID, 10)) | 575 | require.Contains(t, string(staleOutput.Data), "i="+strconv.FormatUint(oldID, 10)) | ||
| 576 | require.NotContains(t, string(staleOutput.Data), "i="+strconv.FormatUint(newID, 10), "late cleanup must never target the fresh attachment") | 576 | require.NotContains(t, string(staleOutput.Data), "i="+strconv.FormatUint(newID, 10), "late cleanup must never target the fresh attachment") | ||
| 577 | d.mu.Lock() | 577 | d.mu.Lock() | ||
| 578 | - | _, retained := d.graphicsNamespaceQuarantines[(oldBase-1)/graphicsIDNamespaceSize] | 578 | + | _, retained := d.graphics.quarantines[(oldBase-1)/graphicsIDNamespaceSize] |
| 579 | d.mu.Unlock() | 579 | d.mu.Unlock() | ||
| 580 | require.True(t, retained, "late transport success is not a terminal ACK and must not release the namespace") | 580 | require.True(t, retained, "late transport success is not a terminal ACK and must not release the namespace") | ||
| 581 | } | 581 | } | ||