// Unregister returns an error if the service isn't registered in etcd. func (s *ServiceTest) TestUnregisterUnregisteredService(c *C) { address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) err := service.Unregister() c.Assert(err.(*etcd.EtcdError).ErrorCode, Equals, 100) }
// Register is effectively a no-op if the service record already exists in // etcd. func (s *ServiceTest) TestRegisterDuplicate(c *C) { address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) _, err := service.Register(0) c.Assert(err, IsNil) _, err = service.Register(0) c.Assert(err, IsNil) }
// Watch observes unregistered services in etcd and removes the relevant // patterns and addresses from the ExchangeServeMux. func (s *ExchangeTest) TestWatchUnegisteredService(c *C) { // Create the namespace in etcd. _, err := s.client.CreateDir("/test", 0) c.Assert(err, IsNil) // Register a new service. address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users"}} service := switchboard.NewService("test", s.client, address, routes) _, err = service.Register(0) c.Assert(err, IsNil) // Initialize the exchange. err = s.exchange.Init() c.Assert(err, IsNil) // Start the watcher goroutine. stop := make(chan bool) stopped := make(chan bool) go func() { s.exchange.Watch(stop) stopped <- true }() // Cleanly shutdown the watcher no matter how the test finishes. defer func() { stop <- true c.Assert(<-stopped, Equals, true) }() // Unregister the service. err = service.Unregister() c.Assert(err, IsNil) // Janky logic to wait for updates from etcd will fail when updates don't // propagate within 500ms. receivedUpdate := false for i := 0; i < 500; i++ { addresses, err := s.mux.Match("GET", "/users") if err == nil { time.Sleep(time.Duration(1) * time.Millisecond) continue } else { c.Assert(addresses, IsNil) receivedUpdate = true break } } c.Assert(receivedUpdate, Equals, true) }
// Broadcast stops pushing changes to etcd when when a bool value is sent to // the stop channel. func (s *ServiceTest) TestBroadcastStops(c *C) { address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) stop := make(chan bool) stopped := make(chan bool) go func() { service.Broadcast(2, 1, stop) stopped <- true }() stop <- true c.Assert(<-stopped, Equals, true) }
// Unregister deletes the service record in etcd. func (s *ServiceTest) TestUnregister(c *C) { address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) _, err := service.Register(0) c.Assert(err, IsNil) err = service.Unregister() c.Assert(err, IsNil) key := "test/" + service.ID() sort := false recursive := true _, err = s.client.Get(key, sort, recursive) c.Assert(err.(*etcd.EtcdError).ErrorCode, Equals, 100) }
// Register creates a service record to represent the service and registers it // in etcd. func (s *ServiceTest) TestRegister(c *C) { address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) record, err := service.Register(0) c.Assert(err, IsNil) key := "test/" + service.ID() sort := false recursive := true response, err := s.client.Get(key, sort, recursive) c.Assert(err, IsNil) recordJSON, _ := json.Marshal(record) c.Assert(response.Node.Value, Equals, bytes.NewBuffer(recordJSON).String()) }
// Init returns an error if the specified namespace doesn't exist in etcd. func (s *ExchangeTest) TestInit(c *C) { address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) _, err := service.Register(0) c.Assert(err, IsNil) err = s.exchange.Init() c.Assert(err, IsNil) addresses, err := s.mux.Match("GET", "/users") c.Assert(err, IsNil) c.Assert(addresses, DeepEquals, &[]string{"http://localhost:8080"}) addresses, err = s.mux.Match("GET", "/user/123") c.Assert(err, IsNil) c.Assert(addresses, DeepEquals, &[]string{"http://localhost:8080"}) }
// Broadcast registers a service with etcd. func (s *ServiceTest) TestBroadcast(c *C) { // Create a new service. address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) // Start broadcasting service changes to etcd. stop := make(chan bool) stopped := make(chan bool) go func() { service.Broadcast(2, 1, stop) stopped <- true }() // Cleanly shutdown the broadcaster no matter how the test finishes. defer func() { stop <- true c.Assert(<-stopped, Equals, true) }() // Janky logic to wait for the broadcaster to write a value to etcd that // can be read back will fail when service registration doesn't complete // within 500ms. receivedUpdate := false for i := 0; i < 500; i++ { key := "test/" + service.ID() sort := false recursive := true response, err := s.client.Get(key, sort, recursive) if err != nil { time.Sleep(time.Duration(1) * time.Millisecond) continue } else { record := switchboard.ServiceRecord{ ID: service.ID(), Address: service.Address(), Routes: service.Routes()} recordJSON, _ := json.Marshal(record) c.Assert(response.Node.Value, Equals, bytes.NewBuffer(recordJSON).String()) receivedUpdate = true break } } c.Assert(receivedUpdate, Equals, true) }
// Watch stops observing changes in etcd when when a bool value is sent to the // stop channel. func (s *ExchangeTest) TestWatchStops(c *C) { address := "http://localhost:8080" routes := switchboard.Routes{"GET": []string{"/users", "/user/:id"}} service := switchboard.NewService("test", s.client, address, routes) _, err := service.Register(0) c.Assert(err, IsNil) stop := make(chan bool) stopped := make(chan bool) go func() { err = s.exchange.Init() c.Assert(err, IsNil) s.exchange.Watch(stop) stopped <- true }() stop <- true c.Assert(<-stopped, Equals, true) }
func main() { // Setup HTTP routes and initialize the service. port := os.Getenv("PORT") address := "http://localhost:" + port client := etcd.NewClient([]string{"http://127.0.0.1:4001"}) routes := switchboard.Routes{"GET": []string{"/hello/:name"}} service := switchboard.NewService("example", client, address, routes) // Broadcast service presence to etcd every 5 seconds (with a TTL of 10 // seconds). If this service crashes the exchange will only attempt to // route requests to it during the TTL window. go func() { log.Print("Broadcasting service configuration to etcd") stop := make(chan bool) service.Broadcast(5, 10, stop) }() // Unregister the service in etcd when we receive a SIGTERM. interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) go func() { for sig := range interrupt { log.Print("Unregistering service") service.Unregister() log.Fatal(sig) } }() // Setup a handler to process requests for our registered routes and // listen for HTTP requests from the exchange. handler := pat.New() handler.Get("/hello/:name", Log(http.HandlerFunc(Hello))) log.Printf("Listening for HTTP requests on port %v", port) err := http.ListenAndServe("localhost:"+port, handler) if err != nil { log.Print(err) log.Print("Unregistering service") service.Unregister() log.Print("Shutting down") } }