From ff0b38810fea6f5f2fdef14a58566929f137bf7a Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 10 Aug 2026 14:55:14 +0300 Subject: [PATCH 1/3] fix concurrent compression changes during publish --- client.go | 17 +++++++-- client_codec_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 client_codec_test.go diff --git a/client.go b/client.go index 3879ee0..3780b49 100644 --- a/client.go +++ b/client.go @@ -125,20 +125,29 @@ func (c *Client) PublishFrame(f Frame) error { pixels := f.Pixels uncompressedSize := saturateUint32(len(pixels)) + // Snapshot the codec while holding the client lock, then release it before + // doing the potentially expensive encode. Keeping the snapshot local makes + // the compression flags and payload use the same codec even if + // SetCompression runs concurrently. + c.mu.RLock() + frameCodec := c.codec + c.mu.RUnlock() + // Compress if codec is not raw. var flags protocol.Flag compressionID := protocol.CompressionNone - if c.codec.ID() != codec.IDRaw { - maxSize := c.codec.MaxEncodedSize(len(pixels)) + codecID := frameCodec.ID() + if codecID != codec.IDRaw { + maxSize := frameCodec.MaxEncodedSize(len(pixels)) dst := make([]byte, maxSize) - compressed, err := c.codec.Encode(dst, pixels) + compressed, err := frameCodec.Encode(dst, pixels) if err != nil { return fmt.Errorf("compose: compress frame: %w", err) } pixels = compressed flags = flags.Set(protocol.FlagCompressed) - compressionID = protocol.Compression(c.codec.ID()) + compressionID = protocol.Compression(codecID) } // Set dirty rect flags. diff --git a/client_codec_test.go b/client_codec_test.go new file mode 100644 index 0000000..f346976 --- /dev/null +++ b/client_codec_test.go @@ -0,0 +1,89 @@ +package compose + +import ( + "bytes" + "runtime" + "sync" + "sync/atomic" + "testing" +) + +func TestClientSetCompressionConcurrentPublishFrame(t *testing.T) { + addr := tempSocket(t) + + srv, err := Listen(addr) + if err != nil { + t.Fatalf("Listen: %v", err) + } + t.Cleanup(func() { _ = srv.Close() }) + + const ( + width = 32 + height = 32 + frames = 128 + ) + expectedPixels := makePixels(width, height, 0xA5) + var received atomic.Int64 + var invalid atomic.Bool + srv.OnFrame(func(f Frame) { + if f.Width != width || f.Height != height || !bytes.Equal(f.Pixels, expectedPixels) { + invalid.Store(true) + } + received.Add(1) + }) + + client, err := Dial(addr, WithName("codec-race"), WithFrameSize(width, height)) + if err != nil { + t.Fatalf("Dial: %v", err) + } + t.Cleanup(func() { _ = client.Close() }) + + start := make(chan struct{}) + errs := make(chan error, frames) + var wg sync.WaitGroup + wg.Add(2) + + // Publish and switch codecs from separate goroutines. The scheduler hint + // keeps both operations interleaved while retaining deterministic inputs. + go func() { + defer wg.Done() + <-start + for i := 0; i < frames; i++ { + if err := client.PublishFrame(Frame{ + Pixels: expectedPixels, + Width: width, + Height: height, + }); err != nil { + errs <- err + } + runtime.Gosched() + } + }() + + go func() { + defer wg.Done() + <-start + for i := 0; i < frames*2; i++ { + if i%2 == 0 { + client.SetCompression("lz4") + } else { + client.SetCompression("raw") + } + runtime.Gosched() + } + }() + + close(start) + wg.Wait() + close(errs) + for publishErr := range errs { + t.Errorf("PublishFrame: %v", publishErr) + } + + if !waitFor(t, func() bool { return received.Load() == frames }) { + t.Fatalf("received %d/%d frames", received.Load(), frames) + } + if invalid.Load() { + t.Fatal("received frame did not match the published payload") + } +} From 0cad3b75dba0abe80beaf77e56b377591f17386f Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 11 Aug 2026 00:24:59 +0300 Subject: [PATCH 2/3] ci: keep lint output configuration valid --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index 11b9746..1c1edf5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -161,5 +161,6 @@ issues: new: false output: + sort-results: true sort-order: - file From 80d7a62c4a6cc3bf917f5f6d06401bdfa1753d1f Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 12 Aug 2026 09:52:18 +0300 Subject: [PATCH 3/3] chore: update golangci-lint v2 output config --- .golangci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 1c1edf5..11b9746 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -161,6 +161,5 @@ issues: new: false output: - sort-results: true sort-order: - file