@@ -4,13 +4,19 @@ import (
44 "bytes"
55 "context"
66 "encoding/json"
7+ "fmt"
8+ "net"
9+ "sync"
710 "testing"
811 "time"
912
13+ "github.com/cirruslabs/orchard/internal/controller"
14+ "github.com/cirruslabs/orchard/internal/dialer"
1015 "github.com/cirruslabs/orchard/internal/execstream"
1116 "github.com/cirruslabs/orchard/internal/tests/devcontroller"
1217 "github.com/cirruslabs/orchard/internal/tests/platformdependent"
1318 "github.com/cirruslabs/orchard/internal/tests/wait"
19+ "github.com/cirruslabs/orchard/internal/worker"
1420 "github.com/cirruslabs/orchard/pkg/client"
1521 v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
1622 "github.com/coder/websocket"
@@ -221,6 +227,88 @@ func TestVMExecScript(t *testing.T) {
221227 require .Equal (t , websocket .StatusNormalClosure , closeError .Code )
222228}
223229
230+ func TestVMExecManyConcurrentSessions (t * testing.T ) {
231+ sshServer := startExecSSHServer (t , 24 )
232+
233+ devClient , vmName := prepareForSyntheticExec (t , dialer .DialFunc (
234+ func (ctx context.Context , network string , addr string ) (net.Conn , error ) {
235+ var netDialer net.Dialer
236+
237+ return netDialer .DialContext (ctx , network , sshServer .Addr ())
238+ },
239+ ))
240+
241+ const concurrentExecs = 32
242+
243+ start := make (chan struct {})
244+ errCh := make (chan error , concurrentExecs )
245+
246+ var wg sync.WaitGroup
247+ wg .Add (concurrentExecs )
248+
249+ for i := range concurrentExecs {
250+ go func () {
251+ defer wg .Done ()
252+
253+ <- start
254+
255+ wsConn , err := devClient .VMs ().Exec (
256+ t .Context (),
257+ vmName ,
258+ fmt .Sprintf ("sh -c 'sleep 2; printf exec-%02d'" , i ),
259+ false ,
260+ 30 ,
261+ )
262+ if err != nil {
263+ errCh <- fmt .Errorf ("exec %d failed to start: %w" , i , err )
264+
265+ return
266+ }
267+ defer wsConn .CloseNow ()
268+
269+ frame , err := readFrameErr (t .Context (), wsConn )
270+ if err != nil {
271+ errCh <- fmt .Errorf ("exec %d failed to read stdout frame: %w" , i , err )
272+
273+ return
274+ }
275+ if frame .Type != execstream .FrameTypeStdout {
276+ errCh <- fmt .Errorf ("exec %d produced first frame %q" , i , frame .Type )
277+
278+ return
279+ }
280+ if got , want := string (frame .Data ), "ok" ; got != want {
281+ errCh <- fmt .Errorf ("exec %d produced stdout %q, want %q" , i , got , want )
282+
283+ return
284+ }
285+
286+ frame , err = readFrameErr (t .Context (), wsConn )
287+ if err != nil {
288+ errCh <- fmt .Errorf ("exec %d failed to read exit frame: %w" , i , err )
289+
290+ return
291+ }
292+ if frame .Type != execstream .FrameTypeExit {
293+ errCh <- fmt .Errorf ("exec %d produced second frame %q" , i , frame .Type )
294+
295+ return
296+ }
297+ if frame .Exit .Code != 0 {
298+ errCh <- fmt .Errorf ("exec %d exited with code %d" , i , frame .Exit .Code )
299+ }
300+ }()
301+ }
302+
303+ close (start )
304+ wg .Wait ()
305+ close (errCh )
306+
307+ for err := range errCh {
308+ require .NoError (t , err )
309+ }
310+ }
311+
224312func TestVMExecSessionReconnectHistory (t * testing.T ) {
225313 devClient , vmName := prepareForExec (t )
226314 sessionID := uuid .NewString ()
@@ -411,25 +499,63 @@ func prepareForExec(t *testing.T) (*client.Client, string) {
411499 return devClient , vmName
412500}
413501
502+ func prepareForSyntheticExec (t * testing.T , vmDialer dialer.Dialer ) (* client.Client , string ) {
503+ devClient , _ , _ := devcontroller .StartIntegrationTestEnvironmentWithAdditionalOpts (t ,
504+ false , []controller.Option {controller .WithSynthetic ()},
505+ false , []worker.Option {
506+ worker .WithSynthetic (),
507+ worker .WithDialer (vmDialer ),
508+ },
509+ )
510+
511+ vmName := "test-vm-exec-" + uuid .NewString ()
512+
513+ err := devClient .VMs ().Create (t .Context (), platformdependent .VM (vmName ))
514+ require .NoError (t , err )
515+
516+ require .True (t , wait .Wait (30 * time .Second , func () bool {
517+ vm , err := devClient .VMs ().Get (t .Context (), vmName )
518+ require .NoError (t , err )
519+
520+ t .Logf ("Waiting for the synthetic VM to start. Current status: %s" , vm .Status )
521+
522+ return vm .Status == v1 .VMStatusRunning
523+ }), "failed to start a synthetic VM" )
524+
525+ return devClient , vmName
526+ }
527+
414528func readFrame (t * testing.T , wsConn * websocket.Conn ) * execstream.Frame {
415529 t .Helper ()
416530
531+ frame , err := readFrameErr (t .Context (), wsConn )
532+ require .NoError (t , err )
533+
534+ return frame
535+ }
536+
537+ func readFrameErr (ctx context.Context , wsConn * websocket.Conn ) (* execstream.Frame , error ) {
417538 var frame execstream.Frame
418539
419- readCtx , readCtxCancel := context .WithTimeout (t . Context () , 30 * time .Second )
540+ readCtx , readCtxCancel := context .WithTimeout (ctx , 30 * time .Second )
420541 defer readCtxCancel ()
421542
422543 messageType , payloadBytes , err := wsConn .Read (readCtx )
423- require .NoError (t , err )
424- require .Equal (t , websocket .MessageText , messageType )
544+ if err != nil {
545+ return nil , err
546+ }
547+ if messageType != websocket .MessageText {
548+ return nil , fmt .Errorf ("unexpected websocket message type %q" , messageType )
549+ }
425550
426- err = json .Unmarshal (payloadBytes , & frame )
427- require .NoError (t , err )
551+ if err := json .Unmarshal (payloadBytes , & frame ); err != nil {
552+ return nil , err
553+ }
428554 if frame .Type == execstream .FrameTypeError {
429- require . FailNowf ( t , "exec stream error" , " %s" , frame .Error )
555+ return nil , fmt . Errorf ( "exec stream error: %s" , frame .Error )
430556 }
431557
432- return & frame
558+ return & frame , nil
433559}
434560
435561func readFramesUntilExit (t * testing.T , wsConn * websocket.Conn ) []* execstream.Frame {
0 commit comments