Use buffered channels and ring-buffer logic when processing console data
This change fixes pterodactyl/panel#3921 by implementing logic to drop the oldest message in a channel and push the newest message onto the channel when the channel buffer is full. This is distinctly different than the previous implementation which just dropped the newest messages, leading to confusing behavior on the client side when a large amount of data was sent over the connection. Up to 10ms per channel is allowed for blocking before falling back to the drop logic.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/franela/goblin"
|
||||
)
|
||||
@@ -123,22 +125,67 @@ func TestSink(t *testing.T) {
|
||||
g.Assert(len(pool.sinks)).Equal(2)
|
||||
})
|
||||
|
||||
g.It("does not block if a channel is nil or otherwise full", func() {
|
||||
ch := make([]chan []byte, 2)
|
||||
ch[1] = make(chan []byte, 1)
|
||||
ch[1] <- []byte("test")
|
||||
g.It("uses a ring-buffer to avoid blocking when the channel is full", func() {
|
||||
ch1 := make(chan []byte, 1)
|
||||
ch2 := make(chan []byte, 2)
|
||||
ch3 := make(chan []byte)
|
||||
|
||||
pool.On(ch[0])
|
||||
pool.On(ch[1])
|
||||
// ch1 and ch2 are now full, and would block if the code doesn't account
|
||||
// for a full buffer.
|
||||
ch1 <- []byte("pre-test")
|
||||
ch2 <- []byte("pre-test")
|
||||
ch2 <- []byte("pre-test 2")
|
||||
|
||||
pool.On(ch1)
|
||||
pool.On(ch2)
|
||||
pool.On(ch3)
|
||||
|
||||
pool.Push([]byte("testing"))
|
||||
time.Sleep(time.Millisecond * 20)
|
||||
|
||||
g.Assert(MutexLocked(&pool.mu)).IsFalse()
|
||||
g.Assert(<-ch[1]).Equal([]byte("test"))
|
||||
// We expect that value previously in the channel to have been dumped
|
||||
// and therefore only the value we pushed will be present. For ch2 we
|
||||
// expect only the first message was dropped, and the second one is now
|
||||
// the first in the out queue.
|
||||
g.Assert(<-ch1).Equal([]byte("testing"))
|
||||
g.Assert(<-ch2).Equal([]byte("pre-test 2"))
|
||||
g.Assert(<-ch2).Equal([]byte("testing"))
|
||||
|
||||
// Because nothing in this test was listening for ch3, it would have
|
||||
// blocked for the 10ms duration, and then been skipped over entirely
|
||||
// because it had no length to try and push onto.
|
||||
g.Assert(len(ch3)).Equal(0)
|
||||
|
||||
// Now, push again and expect similar results.
|
||||
pool.Push([]byte("testing 2"))
|
||||
time.Sleep(time.Millisecond * 20)
|
||||
|
||||
pool.Push([]byte("test2"))
|
||||
g.Assert(<-ch[1]).Equal([]byte("test2"))
|
||||
g.Assert(MutexLocked(&pool.mu)).IsFalse()
|
||||
g.Assert(<-ch1).Equal([]byte("testing 2"))
|
||||
g.Assert(<-ch2).Equal([]byte("testing 2"))
|
||||
})
|
||||
|
||||
g.It("can handle concurrent pushes FIFO", func() {
|
||||
ch := make(chan []byte, 4)
|
||||
|
||||
pool.On(ch)
|
||||
pool.On(make(chan []byte))
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
pool.Push([]byte(fmt.Sprintf("iteration %d", i)))
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 20)
|
||||
g.Assert(MutexLocked(&pool.mu)).IsFalse()
|
||||
g.Assert(len(ch)).Equal(4)
|
||||
|
||||
g.Timeout(time.Millisecond * 500)
|
||||
g.Assert(<-ch).Equal([]byte("iteration 96"))
|
||||
g.Assert(<-ch).Equal([]byte("iteration 97"))
|
||||
g.Assert(<-ch).Equal([]byte("iteration 98"))
|
||||
g.Assert(<-ch).Equal([]byte("iteration 99"))
|
||||
g.Assert(len(ch)).Equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user