Add server group implementation for running multiple servers at once
This commit is contained in:
		
							parent
							
								
									d8aca8ac30
								
							
						
					
					
						commit
						2c54ee703f
					
				|  | @ -0,0 +1,35 @@ | ||||||
|  | package http | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 
 | ||||||
|  | 	"golang.org/x/sync/errgroup" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // NewServerGroup creates a new Server to start and gracefully stop a collection
 | ||||||
|  | // of Servers.
 | ||||||
|  | func NewServerGroup(servers ...Server) Server { | ||||||
|  | 	return &serverGroup{ | ||||||
|  | 		servers: servers, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // serverGroup manages the starting and graceful shutdown of a collection of
 | ||||||
|  | // servers.
 | ||||||
|  | type serverGroup struct { | ||||||
|  | 	servers []Server | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Start runs the servers in the server group.
 | ||||||
|  | func (s *serverGroup) Start(ctx context.Context) error { | ||||||
|  | 	g, groupCtx := errgroup.WithContext(ctx) | ||||||
|  | 
 | ||||||
|  | 	for _, server := range s.servers { | ||||||
|  | 		srv := server | ||||||
|  | 		g.Go(func() error { | ||||||
|  | 			return srv.Start(groupCtx) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return g.Wait() | ||||||
|  | } | ||||||
|  | @ -0,0 +1,102 @@ | ||||||
|  | package http | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 
 | ||||||
|  | 	. "github.com/onsi/ginkgo" | ||||||
|  | 	. "github.com/onsi/gomega" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var _ = Describe("Server Group", func() { | ||||||
|  | 	var m1, m2, m3 *mockServer | ||||||
|  | 	var ctx context.Context | ||||||
|  | 	var cancel context.CancelFunc | ||||||
|  | 	var group Server | ||||||
|  | 
 | ||||||
|  | 	BeforeEach(func() { | ||||||
|  | 		ctx, cancel = context.WithCancel(context.Background()) | ||||||
|  | 
 | ||||||
|  | 		m1 = newMockServer() | ||||||
|  | 		m2 = newMockServer() | ||||||
|  | 		m3 = newMockServer() | ||||||
|  | 		group = NewServerGroup(m1, m2, m3) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	AfterEach(func() { | ||||||
|  | 		cancel() | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	It("starts each server in the group", func() { | ||||||
|  | 		go func() { | ||||||
|  | 			defer GinkgoRecover() | ||||||
|  | 			Expect(group.Start(ctx)).To(Succeed()) | ||||||
|  | 		}() | ||||||
|  | 
 | ||||||
|  | 		Eventually(m1.started).Should(BeClosed(), "mock server 1 not started") | ||||||
|  | 		Eventually(m2.started).Should(BeClosed(), "mock server 2 not started") | ||||||
|  | 		Eventually(m3.started).Should(BeClosed(), "mock server 3 not started") | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	It("stop each server in the group when the context is cancelled", func() { | ||||||
|  | 		go func() { | ||||||
|  | 			defer GinkgoRecover() | ||||||
|  | 			Expect(group.Start(ctx)).To(Succeed()) | ||||||
|  | 		}() | ||||||
|  | 
 | ||||||
|  | 		cancel() | ||||||
|  | 		Eventually(m1.stopped).Should(BeClosed(), "mock server 1 not stopped") | ||||||
|  | 		Eventually(m2.stopped).Should(BeClosed(), "mock server 2 not stopped") | ||||||
|  | 		Eventually(m3.stopped).Should(BeClosed(), "mock server 3 not stopped") | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	It("stop each server in the group when the an error occurs", func() { | ||||||
|  | 		err := errors.New("server error") | ||||||
|  | 		go func() { | ||||||
|  | 			defer GinkgoRecover() | ||||||
|  | 			Expect(group.Start(ctx)).To(MatchError(err)) | ||||||
|  | 		}() | ||||||
|  | 
 | ||||||
|  | 		m2.errors <- err | ||||||
|  | 		Eventually(m1.stopped).Should(BeClosed(), "mock server 1 not stopped") | ||||||
|  | 		Eventually(m2.stopped).Should(BeClosed(), "mock server 2 not stopped") | ||||||
|  | 		Eventually(m3.stopped).Should(BeClosed(), "mock server 3 not stopped") | ||||||
|  | 	}) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // mockServer is used to test the server group can start
 | ||||||
|  | // and stop multiple servers simultaneously.
 | ||||||
|  | type mockServer struct { | ||||||
|  | 	started     chan struct{} | ||||||
|  | 	startClosed bool | ||||||
|  | 	stopped     chan struct{} | ||||||
|  | 	stopClosed  bool | ||||||
|  | 	errors      chan error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newMockServer() *mockServer { | ||||||
|  | 	return &mockServer{ | ||||||
|  | 		started: make(chan struct{}), | ||||||
|  | 		stopped: make(chan struct{}), | ||||||
|  | 		errors:  make(chan error), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (m *mockServer) Start(ctx context.Context) error { | ||||||
|  | 	if !m.startClosed { | ||||||
|  | 		close(m.started) | ||||||
|  | 		m.startClosed = true | ||||||
|  | 	} | ||||||
|  | 	defer func() { | ||||||
|  | 		if !m.stopClosed { | ||||||
|  | 			close(m.stopped) | ||||||
|  | 			m.stopClosed = true | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return nil | ||||||
|  | 	case err := <-m.errors: | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue