Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add external healthcheck to cartesi-rollups-node #212

Merged
merged 3 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/cartesi-rollups-node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func main() {
s = append(s, newDispatcher()) // Depends on the state server
s = append(s, newInspectServer()) // Depends on the server-manager/host-runner

s = append(s, newHttpService())

ready := make(chan struct{}, 1)
// logs startup time
go func() {
Expand Down
15 changes: 15 additions & 0 deletions cmd/cartesi-rollups-node/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package main

import (
"fmt"
"net/http"
"os"

"github.com/cartesi/rollups-node/internal/config"
Expand Down Expand Up @@ -326,3 +327,17 @@ func newSupervisorService(s []services.Service) services.SupervisorService {
Services: s,
}
}

func newHttpService() services.HttpService {
handler := http.NewServeMux()
handler.Handle("/healthz", http.HandlerFunc(healthcheckHandler))
return services.HttpService{
Name: "http",
Address: fmt.Sprintf("%v:%v", config.GetCartesiHttpAddress(), getPort(portOffsetProxy)),
Handler: handler,
}
}

func healthcheckHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
6 changes: 6 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ In reader mode, the node does not make claims.
* **Type:** `bool`
* **Default:** `"false"`

### `CARTESI_HTTP_ADDRESS`
HTTP address for the node.

* **Type:** `string`
* **Default:** `"127.0.0.1"`

### `CARTESI_HTTP_PORT`
HTTP port for the node.
The node will also use the 20 ports after this one for internal services.
Expand Down
6 changes: 6 additions & 0 deletions internal/config/generate/Config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ for more information."""
# HTTP
#

[http.CARTESI_HTTP_ADDRESS]
default = "127.0.0.1"
go-type = "string"
description = """
HTTP address for the node."""

[http.CARTESI_HTTP_PORT]
default = "10000"
go-type = "int"
Expand Down
5 changes: 5 additions & 0 deletions internal/config/get.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions internal/services/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package services

import (
"context"
"errors"
"net"
"net/http"

"github.com/cartesi/rollups-node/internal/config"
)

type HttpService struct {
Name string
Address string
Handler http.Handler
}

func (s HttpService) String() string {
return s.Name
}

func (s HttpService) Start(ctx context.Context, ready chan<- struct{}) error {
server := http.Server{
Addr: s.Address,
Handler: s.Handler,
}

listener, err := net.Listen("tcp", s.Address)
if err != nil {
return err
}

config.InfoLogger.Printf("%v: listening at %v\n", s, listener.Addr())
ready <- struct{}{}

done := make(chan error, 1)
go func() {
err := server.Serve(listener)
if !errors.Is(err, http.ErrServerClosed) {
config.WarningLogger.Printf("%v: %v", s, err)
}
done <- err
}()

select {
case err = <-done:
return err
case <-ctx.Done():
ctx, cancel := context.WithTimeout(context.Background(), DefaultServiceTimeout)
defer cancel()
return server.Shutdown(ctx)
}
}
156 changes: 156 additions & 0 deletions internal/services/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package services

import (
"context"
"fmt"
"io"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/suite"
)

type HttpServiceSuite struct {
suite.Suite
ServicePort int
ServiceAddr string
}

func TestHttpService(t *testing.T) {
suite.Run(t, new(HttpServiceSuite))
}

func (s *HttpServiceSuite) SetupSuite() {
s.ServicePort = 5555
}

func (s *HttpServiceSuite) SetupTest() {
s.ServicePort++
s.ServiceAddr = fmt.Sprintf("127.0.0.1:%v", s.ServicePort)
}

func (s *HttpServiceSuite) TestItStopsWhenContextIsClosed() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

service := HttpService{Name: "http", Address: s.ServiceAddr, Handler: http.NewServeMux()}

result := make(chan error, 1)
ready := make(chan struct{}, 1)
go func() {
result <- service.Start(ctx, ready)
}()

select {
case <-ready:
cancel()
case <-time.After(DefaultServiceTimeout):
s.FailNow("timed out waiting for HttpService to be ready")
}

select {
case err := <-result:
s.Nil(err)
case <-time.After(DefaultServiceTimeout):
s.FailNow("timed out waiting for HttpService to stop")
}
}

func (s *HttpServiceSuite) TestItRespondsToRequests() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

router := http.NewServeMux()
router.HandleFunc("/test", defaultHandler)
service := HttpService{Name: "http", Address: s.ServiceAddr, Handler: router}

result := make(chan error, 1)
ready := make(chan struct{}, 1)
go func() {
result <- service.Start(ctx, ready)
}()

select {
case <-ready:
case <-time.After(DefaultServiceTimeout):
s.FailNow("timed out waiting for HttpService to be ready")
}

resp, err := http.Get(fmt.Sprintf("http://%v/test", s.ServiceAddr))
if err != nil {
s.FailNow(err.Error())
}
s.assertResponse(resp)
}

func (s *HttpServiceSuite) TestItRespondsOngoingRequestsAfterContextIsClosed() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

router := http.NewServeMux()
router.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
// simulate a long-running request
<-time.After(100 * time.Millisecond)
fmt.Fprintf(w, "test")
})
service := HttpService{Name: "http", Address: s.ServiceAddr, Handler: router}

result := make(chan error, 1)
ready := make(chan struct{}, 1)
go func() {
result <- service.Start(ctx, ready)
}()

select {
case <-ready:
case <-time.After(DefaultServiceTimeout):
s.FailNow("timed out wating for HttpService to be ready")
}

clientResult := make(chan ClientResult, 1)
go func() {
resp, err := http.Get(fmt.Sprintf("http://%v/test", s.ServiceAddr))
clientResult <- ClientResult{Response: resp, Error: err}
}()

// wait a bit so server has enough time to start responding the request
<-time.After(200 * time.Millisecond)
cancel()

select {
case res := <-clientResult:
s.Nil(res.Error)
s.assertResponse(res.Response)
err := <-result
s.Nil(err)
case <-result:
s.FailNow("HttpService closed before responding")
}
}

type ClientResult struct {
Response *http.Response
Error error
}

func defaultHandler(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "test")
}

func (s *HttpServiceSuite) assertResponse(resp *http.Response) {
s.Equal(http.StatusOK, resp.StatusCode)

defer resp.Body.Close()

bytes, err := io.ReadAll(resp.Body)
if err != nil {
s.FailNow("failed to read response body. ", err)
}
s.Equal([]byte("test"), bytes)
}
12 changes: 6 additions & 6 deletions internal/services/supervisor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (s *SupervisorServiceSuite) TestItStopsAllServicesWhenContextIsCanceled() {

select {
case err := <-result:
s.Assert().ErrorIs(err, context.Canceled)
s.ErrorIs(err, context.Canceled)
for _, service := range services {
mockService := service.(*MockService)
mockService.AssertExpectations(s.T())
Expand Down Expand Up @@ -158,7 +158,7 @@ func (s *SupervisorServiceSuite) TestItStopsAllServicesIfAServiceStops() {

select {
case err := <-result:
s.Assert().ErrorIs(err, mockErr)
s.ErrorIs(err, mockErr)
for _, service := range services {
mockService := service.(*MockService)
mockService.AssertExpectations(s.T())
Expand Down Expand Up @@ -204,7 +204,7 @@ func (s *SupervisorServiceSuite) TestItStopsCreatingServicesIfAServiceFailsToSta

select {
case err := <-result:
s.Assert().ErrorIs(err, mockErr)
s.ErrorIs(err, mockErr)
last := services[len(services)-1].(*MockService)
last.AssertNotCalled(s.T(), "Start", mock.Anything, mock.Anything)
case <-time.After(DefaultServiceTimeout):
Expand Down Expand Up @@ -252,7 +252,7 @@ func (s *SupervisorServiceSuite) TestItStopsCreatingServicesIfContextIsCanceled(

select {
case err := <-result:
s.Assert().ErrorIs(err, context.Canceled)
s.ErrorIs(err, context.Canceled)
for idx, service := range services {
mockService := service.(*MockService)
if idx > 1 {
Expand Down Expand Up @@ -292,7 +292,7 @@ func (s *SupervisorServiceSuite) TestItTimesOutIfServiceTakesTooLongToBeReady()

select {
case err := <-result:
s.Assert().ErrorIs(err, ServiceTimeoutError)
s.ErrorIs(err, ServiceTimeoutError)
mock1.AssertCalled(s.T(), "Start", mock.Anything, mock.Anything)
case <-ready:
s.FailNow("supervisor shouldn't be ready")
Expand Down Expand Up @@ -328,7 +328,7 @@ func (s *SupervisorServiceSuite) TestItTimesOutIfServicesTakeTooLongToStop() {
cancel()

err := <-result
s.Assert().ErrorIs(err, SupervisorTimeoutError)
s.ErrorIs(err, SupervisorTimeoutError)
}

type MockService struct {
Expand Down
Loading