├── cpu_profile ├── .markdownlint.json ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── security.yml │ ├── hygeine.yml │ └── test.yml ├── internal ├── core │ └── constants.go ├── dyport │ ├── dyport_test.go │ └── dyport.go ├── shrek │ ├── constants.go │ ├── raft_rpc.go │ ├── peers.go │ ├── fsm.go │ ├── shrek_test.go │ └── shrek.go ├── network │ ├── network_test.go │ └── network.go ├── utils │ └── utils.go ├── logging │ └── logging.go ├── app │ └── app.go ├── config │ └── config.go ├── server │ ├── handlers.go │ ├── server.go │ └── server_test.go └── db │ ├── db_test.go │ └── db.go ├── gosql.iml ├── README.md ├── Makefile ├── config.example.env ├── e2e ├── cluster.go ├── node.go └── cluster_test.go ├── go.mod ├── cmd └── main.go ├── .golangci.yml └── go.sum /cpu_profile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | config/config.env 3 | /.vscode/ 4 | /.idea 5 | genesis.json 6 | /.devnet/ 7 | vendor -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /internal/core/constants.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Env string 4 | 5 | const ( 6 | Development Env = "development" 7 | Production Env = "production" 8 | Local Env = "local" 9 | ) 10 | -------------------------------------------------------------------------------- /gosql.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: security 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | # Static security scan using gosec 11 | # https://github.com/securego/gosec 12 | gosec: 13 | runs-on: ubuntu-latest 14 | env: 15 | GO111MODULE: on 16 | steps: 17 | - name: Checkout Source 18 | uses: actions/checkout@v3 19 | - name: Run Gosec Security Scanner 20 | uses: securego/gosec@master 21 | with: 22 | args: ./... 23 | -------------------------------------------------------------------------------- /internal/dyport/dyport_test.go: -------------------------------------------------------------------------------- 1 | package dyport 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "net" 6 | "testing" 7 | ) 8 | 9 | func TestDyPort(t *testing.T) { 10 | count := 5 11 | ports, err := AllocatePorts(count) 12 | require.NoError(t, err) 13 | require.Equal(t, count, len(ports)) 14 | 15 | for _, port := range ports { 16 | ln, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}) 17 | require.NoError(t, err) 18 | err = ln.Close() 19 | if err != nil { 20 | return 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/hygeine.yml: -------------------------------------------------------------------------------- 1 | name: hygeiene 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | golangci: 11 | # Linting job 12 | # https://github.com/golangci/golangci-lint-action 13 | name: lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v3 19 | with: 20 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 21 | version: v1.59.1 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shrek 2 | 3 | A database that's as easy to use as SQLite but also offers the strength and reliability of a distributed system. 4 | 5 | ## Setup 6 | 7 | To use the template, run the following command(s): 8 | 9 | 1. [Download](https://go.dev/doc/install) or upgrade to `golang 1.19`. 10 | 11 | 2. Install all project golang dependencies by running `go mod download`. 12 | 13 | ## To Run 14 | 15 | 1. Compile shrek to machine binary by running the following project level command(s): 16 | * Using Make: `make build-app` 17 | 18 | 2. To run the compiled binary, you can use the following project level command(s): 19 | * Using Make: `make run-app` 20 | * Direct Call: `./bin/shrek` 21 | -------------------------------------------------------------------------------- /internal/shrek/constants.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | type ConsistencyLevel int 4 | 5 | const ( 6 | None ConsistencyLevel = iota 7 | Weak 8 | Strong 9 | ) 10 | 11 | type ClusterState int 12 | 13 | const ( 14 | Leader ClusterState = iota 15 | Follower 16 | Candidate 17 | Shutdown 18 | Unknown 19 | ) 20 | 21 | // BackupFormat defines the structure of a database backup 22 | type BackupFormat int 23 | 24 | const ( 25 | BackupSQL BackupFormat = iota // plaintext SQL command format 26 | 27 | BackupBinary // SQLite file backup format 28 | ) 29 | 30 | type payloadType int 31 | 32 | const ( 33 | Execute payloadType = iota // Modifies the database 34 | Query // Queries the database 35 | Peer // Modifies the peers map 36 | ) 37 | -------------------------------------------------------------------------------- /internal/network/network_test.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/stretchr/testify/require" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func Test_Network(t *testing.T) { 10 | t.Run("test network open close", func(t *testing.T) { 11 | nt := NewNetwork() 12 | err := nt.Open("127.0.0.1:7777") 13 | require.NoError(t, err) 14 | require.Equal(t, "127.0.0.1:7777", nt.Addr().String()) 15 | err = nt.Close() 16 | require.NoError(t, err) 17 | }) 18 | 19 | t.Run("test network dial", func(t *testing.T) { 20 | nt1 := NewNetwork() 21 | err := nt1.Open("127.0.0.1:7777") 22 | require.NoError(t, err) 23 | go func() { 24 | _, err := nt1.Accept() 25 | if err != nil { 26 | return 27 | } 28 | }() 29 | 30 | nt2 := NewNetwork() 31 | _, err = nt2.Dial(nt1.Addr().String(), time.Second) 32 | require.NoError(t, err) 33 | nt1.Close() 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /internal/shrek/raft_rpc.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "github.com/hashicorp/raft" 5 | "net" 6 | "time" 7 | ) 8 | 9 | type Listener interface { 10 | net.Listener 11 | // Dial - makes outgoing connections to other servers in the Raft cluster 12 | // When we connect to a server, we write the 13 | // RaftRPC byte to identify the connection type so we can multiplex Raft on the 14 | // same port as our Log gRPC requests 15 | Dial(addr string, timeout time.Duration) (net.Conn, error) 16 | } 17 | 18 | type RaftLayer struct { 19 | root Listener 20 | } 21 | 22 | func NewRaftLayer(ln Listener) *RaftLayer { 23 | return &RaftLayer{ 24 | root: ln, 25 | } 26 | } 27 | 28 | func (l *RaftLayer) Dial(addr raft.ServerAddress, timeout time.Duration) (net.Conn, error) { 29 | return l.root.Dial(string(addr), timeout) 30 | } 31 | 32 | func (l *RaftLayer) Accept() (net.Conn, error) { 33 | return l.root.Accept() 34 | } 35 | 36 | func (l *RaftLayer) Close() error { 37 | return l.root.Close() 38 | } 39 | 40 | func (l *RaftLayer) Addr() net.Addr { 41 | return l.root.Addr() 42 | } 43 | -------------------------------------------------------------------------------- /internal/network/network.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | type Network struct { 9 | root net.Listener 10 | } 11 | 12 | func NewNetwork() *Network { 13 | return &Network{} 14 | } 15 | 16 | func (nt *Network) Open(addr string) error { 17 | ln, err := net.Listen("tcp", addr) 18 | if err != nil { 19 | return err 20 | } 21 | nt.root = ln 22 | return nil 23 | } 24 | 25 | // Dial ... Establishes a network connection 26 | func (nt *Network) Dial(addr string, timeout time.Duration) (net.Conn, error) { 27 | dialer := &net.Dialer{Timeout: timeout} 28 | conn, err := dialer.Dial("tcp", addr) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return conn, nil 33 | } 34 | 35 | // Accept ... Awaiting the next incoming connection 36 | func (nt *Network) Accept() (net.Conn, error) { 37 | conn, err := nt.root.Accept() 38 | if err != nil { 39 | return nil, err 40 | } 41 | return conn, nil 42 | } 43 | 44 | func (nt *Network) Close() error { 45 | return nt.root.Close() 46 | } 47 | 48 | func (nt *Network) Addr() net.Addr { 49 | return nt.root.Addr() 50 | } 51 | -------------------------------------------------------------------------------- /internal/shrek/peers.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | const ( 11 | jsonPeerPath = "peers.json" 12 | ) 13 | 14 | // NumPeers ... Returns the number of peers indicated by the config files within raftDir 15 | // This code makes assumptions about how the Raft module works 16 | func NumPeers(raftDir string) (int, error) { 17 | buf, err := os.ReadFile(filepath.Join(raftDir, jsonPeerPath)) // #nosec G304 18 | if err != nil && !os.IsNotExist(err) { 19 | return 0, err 20 | } 21 | 22 | if len(buf) == 0 { 23 | return 0, nil 24 | } 25 | 26 | var peerSet []string 27 | dec := json.NewDecoder(bytes.NewReader(buf)) 28 | if err := dec.Decode(&peerSet); err != nil { 29 | return 0, err 30 | } 31 | return len(peerSet), nil 32 | } 33 | 34 | // JoinAllowed ... Returns whether the config files within raftDir indicate 35 | // that the node can join a cluster 36 | func (s *Shrek) JoinAllowed(raftDir string) (bool, error) { 37 | n, err := NumPeers(raftDir) 38 | if err != nil { 39 | return false, err 40 | } 41 | return n <= 1, nil 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Go test workflow 2 | name: test 3 | 4 | on: 5 | push: 6 | branches: [ "master" ] 7 | pull_request: 8 | branches: [ "master" ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.21 20 | 21 | - name: Install project dependencies 22 | run: | 23 | go mod download 24 | 25 | - name: Build App 26 | run: make build-app 27 | 28 | go-test: 29 | outputs: 30 | COVERAGE: ${{ steps.unit.outputs.coverage }} 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v3 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v3 37 | with: 38 | go-version: 1.21 39 | 40 | - name: Install project dependencies 41 | run: | 42 | go mod download 43 | 44 | - name: Run Unit Tests 45 | id: unit 46 | run: | 47 | go test -v -coverprofile=coverage.out ./internal/... 48 | 49 | - name: Run E2E Integration Tests 50 | run: make e2e-test 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME = shrek 2 | 3 | LINTER_VERSION = v1.52.1 4 | LINTER_URL = https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh 5 | 6 | GET_LINT_CMD = "curl -sSfL $(LINTER_URL) | sh -s -- -b $(go env GOPATH)/bin $(LINTER_VERSION)" 7 | 8 | RED = \033[0;34m 9 | GREEN = \033[0;32m 10 | BLUE = \033[0;34m 11 | COLOR_END = \033[0;39m 12 | 13 | TEST_LIMIT = 500s 14 | 15 | build-app: 16 | @echo "$(BLUE)» building application binary... $(COLOR_END)" 17 | @CGO_ENABLED=1 go build -a -o bin/$(APP_NAME) ./cmd/ 18 | @echo "Binary successfully built" 19 | 20 | run-app: 21 | @./bin/${APP_NAME} 22 | 23 | .PHONY: test 24 | test: 25 | go test ./internal/... -timeout $(TEST_LIMIT) 26 | 27 | .PHONY: test-e2e 28 | e2e-test: 29 | go test ./e2e/... -timeout $(TEST_LIMIT) 30 | 31 | .PHONY: lint 32 | lint: 33 | @echo "$(GREEN) Linting repository Go code...$(COLOR_END)" 34 | @if ! command -v golangci-lint &> /dev/null; \ 35 | then \ 36 | echo "golangci-lint command could not be found...."; \ 37 | echo "\nTo install, please run $(GREEN) $(GET_LINT_CMD) $(COLOR_END)"; \ 38 | echo "\nBuild instructions can be found at: https://golangci-lint.run/usage/install/."; \ 39 | exit 1; \ 40 | fi 41 | 42 | @golangci-lint run 43 | 44 | gosec: 45 | @echo "$(GREEN) Running security scan with gosec...$(COLOR_END)" 46 | gosec ./... 47 | -------------------------------------------------------------------------------- /config.example.env: -------------------------------------------------------------------------------- 1 | # Environment 2 | ENV=local 3 | 4 | # Use Debug mode 5 | DEBUG=true 6 | 7 | ALLOWED_ORIGINS=* 8 | ALLOWED_METHODS=* 9 | ALLOWED_HEADERS=* 10 | 11 | # DB Path 12 | DB_PATH=test.db 13 | 14 | # `SQLite DSN parameters. E.g. "cache=shared&mode=memory"` 15 | DSN= 16 | 17 | # Use an on-disk SQLite database 18 | ON_DISK=true 19 | 20 | # Show version information and exit 21 | VERSION=false 22 | 23 | # Server configurations 24 | 25 | HTTP_HOST=localhost 26 | HTTP_PORT=4001 27 | 28 | HTTP_ADDR_HOST= 29 | HTTP_ADDR_PORT= 30 | 31 | PUBLISH_PEER_DELAY = 1s 32 | PUBLISH_PEER_TIMEOUT = 30s 33 | 34 | HTTP_SERVER_KEEP_ALIVE_TIME=10 35 | HTTP_SERVER_READ_TIMEOUT=10 36 | HTTP_SERVER_WRITE_TIMEOUT=10 37 | HTTP_SERVER_SHUTDOWN_TIME=10 38 | 39 | TCP_SERVER_HOST=localhost 40 | TCP_SERVER_PORT=4002 41 | 42 | # Raft 43 | NODE_ID=node1 44 | 45 | RAFT_HOST=localhost 46 | RAFT_PORT=4645 47 | 48 | RAFT_ADDR_HOST= 49 | RAFT_ADDR_PORT= 50 | 51 | RAFT_HEARTBEAT_TIMEOUT=1s 52 | RAFT_ELECTION_TIMEOUT=1s 53 | RAFT_APPLY_TIMEOUT=10s 54 | RAFT_OPEN_TIMEOUT=120s 55 | RAFT_SNAP_THRESHOLD=8192 56 | RAFT_SHUTDOWN_ON_REMOVE=false 57 | 58 | # Comma-delimited list of nodes, through which a cluster can be joined (proto://host:port) 59 | JOIN= 60 | 61 | # Service Discovery 62 | DISCOVERY_URL=http://discovery.shrek.com 63 | DISCOVERY_ID= 64 | 65 | CPU_PROFILE=cpu_profile 66 | -------------------------------------------------------------------------------- /internal/shrek/fsm.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | sql "github.com/draculaas/shrek/internal/db" 7 | "github.com/hashicorp/raft" 8 | ) 9 | 10 | type fsmExecuteResponse struct { 11 | results []*sql.Result 12 | error error 13 | } 14 | 15 | type fsmQueryResponse struct { 16 | rows []*sql.Rows 17 | error error 18 | } 19 | 20 | type fsmGenericResponse struct { 21 | error error 22 | } 23 | 24 | type fsmSnapshot struct { 25 | database []byte 26 | meta []byte 27 | } 28 | 29 | // Persist writes the snapshot to the given sink 30 | func (f *fsmSnapshot) Persist(sink raft.SnapshotSink) error { 31 | err := func() error { 32 | // Start by writing size of database 33 | b := new(bytes.Buffer) 34 | size := uint64(len(f.database)) 35 | err := binary.Write(b, binary.LittleEndian, size) 36 | if err != nil { 37 | return err 38 | } 39 | if _, err := sink.Write(b.Bytes()); err != nil { 40 | return err 41 | } 42 | 43 | if _, err := sink.Write(f.database); err != nil { 44 | return err 45 | } 46 | 47 | if _, err := sink.Write(f.meta); err != nil { 48 | return err 49 | } 50 | 51 | return sink.Close() 52 | }() 53 | 54 | if err != nil { 55 | sinkErr := sink.Cancel() 56 | if sinkErr != nil { 57 | return sinkErr 58 | } 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (f *fsmSnapshot) Release() {} 66 | -------------------------------------------------------------------------------- /internal/dyport/dyport.go: -------------------------------------------------------------------------------- 1 | package dyport 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | "net" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | minPort = 10000 12 | endPort = 65535 13 | countPorts = 1024 14 | maxBlocks = 16 15 | attempts = 3 16 | ) 17 | 18 | var ( 19 | port int 20 | initPort int 21 | once sync.Once 22 | mu sync.Mutex 23 | ) 24 | 25 | func AllocatePorts(count int) ([]int, error) { 26 | if count > countPorts-1 { 27 | count = countPorts - 1 28 | } 29 | 30 | mu.Lock() 31 | defer mu.Unlock() 32 | 33 | ports := make([]int, 0) 34 | 35 | once.Do(func() { 36 | for i := 0; i < attempts; i++ { 37 | rndBlocks, err := rand.Int(rand.Reader, big.NewInt(maxBlocks)) 38 | if err != nil { 39 | continue 40 | } 41 | initPort = minPort + int(rndBlocks.Int64())*countPorts 42 | lockLn, err := listener(initPort) 43 | if err != nil { 44 | continue 45 | } 46 | _ = lockLn.Close() 47 | return 48 | } 49 | panic("failed to allocate port block") 50 | }) 51 | 52 | for len(ports) < count { 53 | port++ 54 | if port < initPort+1 || port >= initPort+countPorts { 55 | port = initPort + 1 56 | } 57 | ln, err := listener(port) 58 | if err != nil { 59 | continue 60 | } 61 | _ = ln.Close() 62 | ports = append(ports, port) 63 | } 64 | 65 | return ports, nil 66 | } 67 | 68 | func listener(port int) (*net.TCPListener, error) { 69 | return net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}) 70 | } 71 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/rand" 5 | "math/big" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func PathExists(path string) bool { 12 | _, err := os.Stat(path) 13 | if err == nil { 14 | return true // path exists 15 | } 16 | if os.IsNotExist(err) { 17 | return false // path does not exist 18 | } 19 | // handle other errors, e.g., permission denied 20 | return false 21 | } 22 | 23 | func FindDirectory(currentDir, target string) string { 24 | for { 25 | if _, err := os.Stat(filepath.Join(currentDir, target)); err == nil { 26 | return currentDir 27 | } 28 | parentDir := filepath.Dir(currentDir) 29 | if parentDir == currentDir { 30 | break 31 | } 32 | currentDir = parentDir 33 | } 34 | 35 | return currentDir 36 | } 37 | 38 | func RandomString(n int) string { 39 | var alphanumerics = []rune("abcdefghijklmnopqrstuvwxyz0123456789") 40 | s := make([]rune, n) 41 | for i := range s { 42 | randomIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphanumerics)))) 43 | if err != nil { 44 | return "" 45 | } 46 | s[i] = alphanumerics[randomIndex.Int64()] 47 | } 48 | return string(s) 49 | } 50 | 51 | func GetTCPAddr(addr string) (*net.TCPAddr, error) { 52 | res, err := net.ResolveTCPAddr("tcp", addr) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return res, nil 57 | } 58 | 59 | func TempDir(name string) string { 60 | path, err := os.MkdirTemp("", name) 61 | if err != nil { 62 | panic("failed to create temp dir") 63 | } 64 | return path 65 | } 66 | -------------------------------------------------------------------------------- /internal/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | "github.com/draculaas/shrek/internal/core" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | ) 9 | 10 | type LogKey = string 11 | 12 | type loggerKeyType int 13 | 14 | const loggerKey loggerKeyType = iota 15 | 16 | const ( 17 | messageKey = "message" 18 | ) 19 | 20 | var logger *zap.Logger = zap.NewNop() 21 | 22 | // NewContext ... A helper for middleware to create requestId or other context fields 23 | // and return a context which logger can understand. 24 | func NewContext(ctx context.Context, fields ...zap.Field) context.Context { 25 | return context.WithValue(ctx, loggerKey, WithContext(ctx).With(fields...)) 26 | } 27 | 28 | // WithContext ... Pass in a context containing values to add to each log message 29 | func WithContext(ctx context.Context) *zap.Logger { 30 | if ctx == nil { 31 | return logger 32 | } 33 | 34 | if ctxLogger, ok := ctx.Value(loggerKey).(*zap.Logger); ok { 35 | return ctxLogger 36 | } 37 | 38 | return logger 39 | } 40 | 41 | // NoContext ... A log helper to log when there's no context. Rare case usage 42 | func NoContext() *zap.Logger { 43 | return logger 44 | } 45 | 46 | // New ... A helper to create a logger based on environment 47 | func New(env core.Env) { 48 | switch env { 49 | case core.Local: 50 | logger = NewLocal() 51 | case core.Production: 52 | // TODO 53 | case core.Development: 54 | // TODO 55 | default: 56 | panic("Invalid environment") 57 | } 58 | } 59 | 60 | func NewLocal() *zap.Logger { 61 | cfg := zap.NewDevelopmentConfig() 62 | cfg.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 63 | cfg.EncoderConfig.MessageKey = messageKey 64 | 65 | logger, err := cfg.Build(zap.AddStacktrace(zap.FatalLevel)) 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | return logger 71 | } 72 | -------------------------------------------------------------------------------- /e2e/cluster.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Cluster impl 9 | type Cluster struct { 10 | nodes []*Node 11 | } 12 | 13 | func (c *Cluster) findNode(addr string) (*Node, error) { 14 | for _, v := range c.nodes { 15 | if v.RaftAddr == addr { 16 | return v, nil 17 | } 18 | } 19 | return nil, fmt.Errorf("node not found") 20 | } 21 | 22 | func (c *Cluster) Leader() (*Node, error) { 23 | l, err := c.nodes[0].WaitForLeader() 24 | if err != nil { 25 | return nil, err 26 | } 27 | return c.findNode(l) 28 | } 29 | 30 | func (c *Cluster) WaitForNewLeader(oldLeader *Node) (*Node, error) { 31 | timer := time.NewTimer(30 * time.Second) 32 | defer timer.Stop() 33 | ticker := time.NewTicker(100 * time.Millisecond) 34 | defer ticker.Stop() 35 | 36 | for { 37 | select { 38 | case <-timer.C: 39 | return nil, fmt.Errorf("timeout expired") 40 | case <-ticker.C: 41 | leader, err := c.Leader() 42 | if err != nil { 43 | continue 44 | } 45 | if !leader.sameAs(oldLeader) { 46 | return leader, nil 47 | } 48 | } 49 | } 50 | } 51 | 52 | func (c *Cluster) Followers() ([]*Node, error) { 53 | leaderAddr, err := c.nodes[0].WaitForLeader() 54 | if err != nil { 55 | return nil, err 56 | } 57 | leader, err := c.findNode(leaderAddr) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | var followers []*Node 63 | for _, v := range c.nodes { 64 | if v != leader { 65 | followers = append(followers, v) 66 | } 67 | } 68 | 69 | return followers, nil 70 | } 71 | 72 | func (c *Cluster) RemoveNode(n *Node) { 73 | newNodes := make([]*Node, 0) 74 | for _, v := range c.nodes { 75 | if v.RaftAddr != n.RaftAddr { 76 | newNodes = append(newNodes, v) 77 | } 78 | } 79 | c.nodes = newNodes 80 | } 81 | 82 | // Shutdown ... Shutdown each Node in Cluster 83 | func (c *Cluster) Shutdown() { 84 | for _, v := range c.nodes { 85 | v.Shutdown() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/draculaas/shrek 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gin-contrib/zap v0.2.0 7 | github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/hashicorp/raft v1.6.0 10 | github.com/hashicorp/raft-boltdb v0.0.0-20231211162105-6c830fa4535e 11 | github.com/mattn/go-sqlite3 v1.14.19 12 | github.com/stretchr/testify v1.8.4 13 | github.com/urfave/cli v1.22.14 14 | go.uber.org/zap v1.26.0 15 | ) 16 | 17 | require ( 18 | github.com/armon/go-metrics v0.4.1 // indirect 19 | github.com/boltdb/bolt v1.3.1 // indirect 20 | github.com/bytedance/sonic v1.10.0 // indirect 21 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 22 | github.com/chenzhuoyu/iasm v0.9.0 // indirect 23 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/fatih/color v1.13.0 // indirect 26 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 27 | github.com/gin-contrib/sse v0.1.0 // indirect 28 | github.com/go-playground/locales v0.14.1 // indirect 29 | github.com/go-playground/universal-translator v0.18.1 // indirect 30 | github.com/go-playground/validator/v10 v10.15.3 // indirect 31 | github.com/goccy/go-json v0.10.2 // indirect 32 | github.com/hashicorp/go-hclog v1.5.0 // indirect 33 | github.com/hashicorp/go-immutable-radix v1.0.0 // indirect 34 | github.com/hashicorp/go-msgpack v0.5.5 // indirect 35 | github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect 36 | github.com/hashicorp/golang-lru v0.5.0 // indirect 37 | github.com/json-iterator/go v1.1.12 // indirect 38 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 39 | github.com/leodido/go-urn v1.2.4 // indirect 40 | github.com/mattn/go-colorable v0.1.12 // indirect 41 | github.com/mattn/go-isatty v0.0.19 // indirect 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 43 | github.com/modern-go/reflect2 v1.0.2 // indirect 44 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect 45 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 46 | github.com/pmezard/go-difflib v1.0.0 // indirect 47 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 48 | github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989 // indirect 49 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 50 | github.com/ugorji/go/codec v1.2.11 // indirect 51 | go.uber.org/multierr v1.11.0 // indirect 52 | golang.org/x/arch v0.5.0 // indirect 53 | golang.org/x/crypto v0.16.0 // indirect 54 | golang.org/x/mod v0.14.0 // indirect 55 | golang.org/x/net v0.19.0 // indirect 56 | golang.org/x/sys v0.16.0 // indirect 57 | golang.org/x/text v0.14.0 // indirect 58 | golang.org/x/tools v0.16.1 // indirect 59 | google.golang.org/protobuf v1.31.0 // indirect 60 | gopkg.in/yaml.v2 v2.4.0 // indirect 61 | gopkg.in/yaml.v3 v3.0.1 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/draculaas/shrek/internal/config" 7 | "github.com/draculaas/shrek/internal/logging" 8 | "github.com/draculaas/shrek/internal/network" 9 | "github.com/draculaas/shrek/internal/server" 10 | "github.com/draculaas/shrek/internal/shrek" 11 | "go.uber.org/zap" 12 | "os" 13 | "os/signal" 14 | "strings" 15 | "syscall" 16 | ) 17 | 18 | type Application struct { 19 | cfg *config.Config 20 | ctx context.Context 21 | g *shrek.Shrek 22 | } 23 | 24 | // New ... Initializer 25 | func New(ctx context.Context, cfg *config.Config, g *shrek.Shrek) *Application { 26 | return &Application{ 27 | ctx: ctx, 28 | cfg: cfg, 29 | g: g, 30 | } 31 | } 32 | 33 | func setupStore(ctx context.Context, cfg *config.Config) (*shrek.Shrek, func(), error) { 34 | logger := logging.WithContext(ctx) 35 | // configure network layer 36 | nt := network.NewNetwork() 37 | raftAddr := cfg.StorageConfig.RaftAddr 38 | 39 | if err := nt.Open(raftAddr.String()); err != nil { 40 | logger.Fatal("failed to open network layer", zap.String("addr", raftAddr.String()), zap.Error(err)) 41 | } 42 | 43 | // create a new store 44 | s := shrek.New(ctx, cfg, nt) 45 | 46 | // Determine join addresses, if necessary 47 | canJoin, err := s.JoinAllowed(cfg.StorageConfig.RaftDir) 48 | if err != nil { 49 | logger.Fatal("Unable to determine if join permitted", zap.Error(err)) 50 | } 51 | 52 | var joins []string 53 | if canJoin { 54 | if cfg.StorageConfig.Join != "" { 55 | joins = strings.Split(cfg.StorageConfig.Join, ",") 56 | } 57 | } 58 | 59 | // Initialize the Raft server 60 | if err := s.Open(len(joins) == 0); err != nil { 61 | logger.Fatal("Unable to open raft store", zap.Error(err)) 62 | } 63 | 64 | apiAdvertise := cfg.ServerConfig.HTTPAddr.String() 65 | if cfg.ServerConfig.HTTPAdvertise.IP != nil { 66 | apiAdvertise = cfg.ServerConfig.HTTPAdvertise.String() 67 | } 68 | 69 | meta := map[string]string{ 70 | "api_addr": apiAdvertise, 71 | } 72 | 73 | if len(joins) > 0 { 74 | logger.Info("list of joining addresses", zap.String("address list", strings.Join(joins, ","))) 75 | raftAdvertise := cfg.StorageConfig.RaftAddr 76 | if cfg.StorageConfig.RaftAdvertise.IP != nil { 77 | raftAdvertise = cfg.StorageConfig.RaftAdvertise 78 | } 79 | // Join to the raft cluster 80 | joinedAddr, err := server.Join(joins, s.ID(), raftAdvertise, meta) 81 | if err != nil { 82 | logger.Fatal("Failed to join raft cluster", zap.Error(err)) 83 | } else { 84 | logger.Info("Successfully joined raft cluster", zap.String("joined addr", joinedAddr)) 85 | } 86 | } else { 87 | logger.Info("No join addresses specified") 88 | } 89 | 90 | // Wait until the store is in full consensus 91 | leader, err := s.WaitForLeader(s.OpenTimeout) 92 | if err != nil { 93 | logger.Fatal("Failed to achieve consensus", zap.Error(err)) 94 | return nil, nil, err 95 | } 96 | 97 | logger.Info("Found the leader", zap.String("leader", leader)) 98 | 99 | err = s.WaitForApplied(s.OpenTimeout) 100 | if err != nil { 101 | logger.Fatal("Can't apply last index", zap.Error(err)) 102 | return nil, nil, err 103 | } 104 | 105 | if err := s.SetMetadata(meta); err != nil && !errors.Is(err, shrek.ErrNotLeader) { 106 | logger.Fatal("Unable to store metadata within the Raft consensus cluster", zap.Error(err)) 107 | } 108 | 109 | stop := func() { 110 | logging.WithContext(ctx).Info("Starting to shutdown store") 111 | 112 | if err := s.Shutdown(); err != nil { 113 | logging.WithContext(ctx).Fatal("Failed to close store", zap.Error(err)) 114 | } 115 | } 116 | 117 | return s, stop, nil 118 | } 119 | 120 | func setupHTTPServer(ctx context.Context, cfg *config.Config, s *shrek.Shrek) error { 121 | srv := server.New(ctx, cfg.ServerConfig, s) 122 | return srv.Run() 123 | } 124 | 125 | func NewApp(ctx context.Context, cfg *config.Config) (*Application, func(), error) { 126 | g, cleanup, err := setupStore(ctx, cfg) 127 | if err != nil { 128 | return nil, nil, err 129 | } 130 | 131 | // create HTTP server 132 | if err := setupHTTPServer(ctx, cfg, g); err != nil { 133 | return nil, nil, err 134 | } 135 | 136 | return New(ctx, cfg, g), cleanup, nil 137 | } 138 | 139 | // ListenForShutdown ... Handles and listens for shutdown 140 | func (a *Application) ListenForShutdown(stop func()) { 141 | done := <-a.End() // Blocks until an OS signal is received 142 | 143 | logging.WithContext(a.ctx). 144 | Info("Received shutdown OS signal", zap.String("signal", done.String())) 145 | stop() 146 | } 147 | 148 | // End ... Returns a channel that will receive an OS signal 149 | func (a *Application) End() <-chan os.Signal { 150 | sigs := make(chan os.Signal, 1) 151 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 152 | return sigs 153 | } 154 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/draculaas/shrek/internal/core" 5 | "github.com/draculaas/shrek/internal/utils" 6 | "github.com/urfave/cli" 7 | "net" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Config ... Application level configuration defined by `FilePath` value 14 | type Config struct { 15 | Environment core.Env 16 | ServerConfig *ServerConfig 17 | CPUProfile string 18 | StorageConfig *StorageConfig 19 | } 20 | 21 | // ServerConfig ... Server configuration options 22 | type ServerConfig struct { 23 | HTTPAddr net.Addr 24 | HTTPAdvertise *net.TCPAddr 25 | 26 | JoinServerHost string 27 | JoinServerPort int 28 | 29 | AllowedOrigins []string 30 | AllowedMethods []string 31 | AllowedHeaders []string 32 | } 33 | 34 | type StorageConfig struct { 35 | // DB config 36 | DBCfg *DBConfig 37 | 38 | // raft working dir 39 | RaftDir string 40 | 41 | // Node ID 42 | RaftID string 43 | 44 | // RaftAddr is the RPC address used by Nomad. This should be reachable 45 | // by the other servers and clients 46 | RaftAddr *net.TCPAddr 47 | 48 | // RaftAdvertise is the address that is advertised to client nodes for 49 | // the RPC endpoint. This can differ from the RPC address, if for example 50 | // the RaftAddr is unspecified "0.0.0.0:4646", but this address must be 51 | // reachable 52 | RaftAdvertise *net.TCPAddr 53 | 54 | RaftHeartbeatTimeout time.Duration 55 | 56 | // Join list of address 57 | Join string 58 | 59 | RaftElectionTimeout time.Duration 60 | RaftApplyTimeout time.Duration 61 | RaftOpenTimeout time.Duration 62 | RaftSnapThreshold uint64 63 | RaftShutdownOnRemove bool 64 | } 65 | 66 | type DBConfig struct { 67 | DBFilename string 68 | DSN string // data source naming 69 | InMemory bool // in-memory mode 70 | } 71 | 72 | // NewConfig ... Initializer 73 | func NewConfig(c *cli.Context) *Config { 74 | dbFilename := c.String("db-filename") 75 | dsn := c.String("dsn") 76 | inMemory := c.Bool("in-memory") 77 | 78 | env := c.String("env") 79 | raftNodeID := c.String("node-id") 80 | if raftNodeID == "" { 81 | raftNodeID = utils.RandomString(5) 82 | } 83 | 84 | httpAddr, _ := utils.GetTCPAddr(c.String("server-addr")) 85 | httpAdvertise, _ := utils.GetTCPAddr(c.String("server-advertise")) 86 | 87 | allowedOrigins := strings.Split(c.String("allowed-origins"), ",") 88 | allowedMethods := strings.Split(c.String("allowed-methods"), ",") 89 | allowedHeaders := strings.Split(c.String("allowed-headers"), ",") 90 | 91 | cpuProfile := c.String("cpu_profile") 92 | 93 | raftDir := c.String("raft-dir") 94 | if raftDir == "" { 95 | raftDir = utils.RandomString(5) 96 | } 97 | raftAddr, _ := utils.GetTCPAddr(c.String("raft-addr")) 98 | raftAdvertise, _ := utils.GetTCPAddr(c.String("raft-advertise")) 99 | 100 | raftHeartbeatTimeout, _ := time.ParseDuration(c.String("raft-heartbeat-timeout")) 101 | raftElectionTimeout, _ := time.ParseDuration(c.String("raft-election-timeout")) 102 | raftApplyTimeout, _ := time.ParseDuration(c.String("raft-apply-timeout")) 103 | raftOpenTimeout, _ := time.ParseDuration(c.String("raft-open-timeout")) 104 | raftSnapThreshold := c.Uint64("raft-snap-threshold") 105 | raftShutdownOnRemove := c.Bool("raft-shutdown-on-remove") 106 | 107 | join := c.String("join") 108 | 109 | config := &Config{ 110 | Environment: core.Env(env), 111 | CPUProfile: cpuProfile, 112 | 113 | ServerConfig: &ServerConfig{ 114 | HTTPAddr: httpAddr, 115 | HTTPAdvertise: httpAdvertise, 116 | AllowedOrigins: allowedOrigins, 117 | AllowedMethods: allowedMethods, 118 | AllowedHeaders: allowedHeaders, 119 | }, 120 | 121 | StorageConfig: &StorageConfig{ 122 | DBCfg: &DBConfig{ 123 | DBFilename: filepath.Join(raftDir, dbFilename), 124 | DSN: dsn, 125 | InMemory: inMemory, 126 | }, 127 | RaftID: raftNodeID, 128 | RaftDir: raftDir, 129 | RaftAddr: raftAddr, 130 | RaftAdvertise: raftAdvertise, 131 | RaftHeartbeatTimeout: raftHeartbeatTimeout, 132 | RaftElectionTimeout: raftElectionTimeout, 133 | RaftApplyTimeout: raftApplyTimeout, 134 | RaftOpenTimeout: raftOpenTimeout, 135 | RaftSnapThreshold: raftSnapThreshold, 136 | RaftShutdownOnRemove: raftShutdownOnRemove, 137 | Join: join, 138 | }, 139 | } 140 | 141 | return config 142 | } 143 | 144 | // IsProduction ... Returns true if the env is production 145 | func (cfg *Config) IsProduction() bool { 146 | return cfg.Environment == core.Production 147 | } 148 | 149 | // IsDevelopment ... Returns true if the env is development 150 | func (cfg *Config) IsDevelopment() bool { 151 | return cfg.Environment == core.Development 152 | } 153 | 154 | // IsLocal ... Returns true if the env is local 155 | func (cfg *Config) IsLocal() bool { 156 | return cfg.Environment == core.Local 157 | } 158 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "runtime/pprof" 7 | 8 | "github.com/draculaas/shrek/internal/app" 9 | "github.com/draculaas/shrek/internal/config" 10 | "github.com/draculaas/shrek/internal/logging" 11 | "github.com/urfave/cli" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | // RunApp ... Application entry point 16 | func RunApp(c *cli.Context) error { 17 | cfg := config.NewConfig(c) 18 | ctx := context.Background() 19 | 20 | logging.New(cfg.Environment) 21 | logger := logging.WithContext(ctx) 22 | 23 | if cfg.CPUProfile != "" { 24 | logger.Debug("Profiling enabled") 25 | f, err := os.Create(cfg.CPUProfile) 26 | if err != nil { 27 | logger.Fatal("Failed to create cpu profile file", zap.Error(err), zap.String("file", cfg.CPUProfile)) 28 | return err 29 | } 30 | 31 | defer f.Close() 32 | err = pprof.StartCPUProfile(f) 33 | if err != nil { 34 | logger.Fatal("Failed to start CPU Profile", zap.Error(err)) 35 | return err 36 | } 37 | 38 | defer pprof.StopCPUProfile() 39 | } 40 | 41 | logger.Info("Starting shrek application") 42 | 43 | shrek, shutDown, err := app.NewApp(ctx, cfg) 44 | 45 | if err != nil { 46 | logger.Fatal("Error creating application", zap.Error(err)) 47 | return err 48 | } 49 | 50 | shrek.ListenForShutdown(shutDown) 51 | 52 | logger.Debug("Waiting for all application threads to end") 53 | logger.Info("Successful app shutdown") 54 | 55 | return nil 56 | } 57 | 58 | func main() { 59 | ctx := context.Background() 60 | logger := logging.WithContext(ctx) 61 | 62 | a := cli.NewApp() 63 | a.Name = "shrek" 64 | a.Flags = []cli.Flag{ 65 | &cli.StringFlag{ 66 | Name: "env", 67 | Value: "local", 68 | Usage: "Set the application env", 69 | }, 70 | &cli.StringFlag{ 71 | Name: "node-id", 72 | Value: "", 73 | Usage: "Raft unique node name", 74 | }, 75 | &cli.StringFlag{ 76 | Name: "server-addr", 77 | Value: "localhost:4001", 78 | Usage: "HTTP server bind address", 79 | }, 80 | &cli.StringFlag{ 81 | Name: "server-advertise", 82 | Value: "", 83 | Usage: "Advertised HTTP address", 84 | }, 85 | &cli.StringFlag{ 86 | Name: "raft-dir", 87 | Value: "", 88 | Usage: "Set the Raft dir folder", 89 | }, 90 | &cli.StringFlag{ 91 | Name: "raft-addr", 92 | Value: "localhost:4002", 93 | Usage: "Raft communication address", 94 | }, 95 | &cli.StringFlag{ 96 | Name: "raft-advertise", 97 | Value: "", 98 | Usage: "Advertised Raft communication address", 99 | }, 100 | &cli.StringFlag{ 101 | Name: "raft-heartbeat-timeout", 102 | Value: "1s", 103 | Usage: "Raft heartbeat timeout", 104 | }, 105 | &cli.StringFlag{ 106 | Name: "raft-election-timeout", 107 | Value: "1s", 108 | Usage: "Raft election timeout", 109 | }, 110 | &cli.StringFlag{ 111 | Name: "raft-apply-timeout", 112 | Value: "10s", 113 | Usage: "Raft apply timeout", 114 | }, 115 | &cli.StringFlag{ 116 | Name: "raft-open-timeout", 117 | Value: "120s", 118 | Usage: "Set the time duration for the initial application of Raft logs. To skip the wait, use a duration of 0s", 119 | }, 120 | &cli.Uint64Flag{ 121 | Name: "raft-snap-threshold", 122 | Value: 8192, 123 | Usage: "Comma-delimited list of nodes, through which a cluster can be joined (proto://host:port)", 124 | }, 125 | &cli.BoolFlag{ 126 | Name: "raft-shutdown-on-remove", 127 | Required: false, 128 | Usage: "Shutdown Raft if node removed", 129 | }, 130 | &cli.StringFlag{ 131 | Name: "db-filename", 132 | Value: "db.sqlite", 133 | Usage: "Set the database file name", 134 | }, 135 | &cli.StringFlag{ 136 | Name: "allowed-origins", 137 | Value: "*", 138 | Usage: "Allowed origins for HTTP service", 139 | }, 140 | &cli.StringFlag{ 141 | Name: "allowed-methods", 142 | Value: "*", 143 | Usage: "Allowed methods for HTTP service", 144 | }, 145 | &cli.StringFlag{ 146 | Name: "allowed-headers", 147 | Value: "*", 148 | Usage: "Allowed headers for HTTP service", 149 | }, 150 | &cli.StringFlag{ 151 | Name: "dsn", 152 | Value: "", 153 | Usage: "SQLite DSN parameters. E.g. 'cache=shared&mode=memory'", 154 | }, 155 | &cli.StringFlag{ 156 | Name: "join", 157 | Value: "", 158 | Usage: "Comma-delimited list of nodes, through which a cluster can be joined (proto://host:port)", 159 | }, 160 | &cli.StringFlag{ 161 | Name: "cpu_profile", 162 | Value: "", 163 | Usage: "Set the path to the file for CPU profiling information", 164 | }, 165 | &cli.BoolFlag{ 166 | Name: "in-memory", 167 | Required: false, 168 | Usage: "Use im-memory mode for the SQLite database", 169 | }, 170 | } 171 | a.Description = "Shrek distributed database, which uses SQLite as the storage engine" 172 | a.Usage = "Shrek Application" 173 | a.Action = RunApp 174 | 175 | err := a.Run(os.Args) 176 | if err != nil { 177 | logger.Fatal("Error running application", zap.Error(err)) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /internal/server/handlers.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "github.com/draculaas/shrek/internal/shrek" 8 | "github.com/gin-gonic/gin" 9 | "net/http" 10 | "runtime" 11 | "time" 12 | ) 13 | 14 | func (s *Server) handleExecute() gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | c.Header("Content-Type", "application/json; charset=utf-8") 17 | 18 | response := NewResponse() 19 | 20 | var req ExecuteRequest 21 | if err := c.ShouldBindJSON(&req); err != nil { 22 | c.JSON(http.StatusBadRequest, gin.H{ 23 | "error": fmt.Sprintf("Bad request: %v", err), 24 | }) 25 | } 26 | 27 | res, err := s.storage.Execute(&shrek.ExecuteRequest{ 28 | Queries: req.Queries, 29 | UseTx: req.UseTx, 30 | IncludeTimings: req.IncludeTimings, 31 | }) 32 | if err != nil { 33 | if errors.Is(err, shrek.ErrNotLeader) { 34 | leader := s.leaderID() 35 | if leader == "" { 36 | c.JSON(http.StatusServiceUnavailable, gin.H{ 37 | "error": err.Error(), 38 | }) 39 | return 40 | } 41 | // redirect to the leader 42 | } 43 | response.Error = err.Error() 44 | } else { 45 | response.Results = res 46 | } 47 | // update end request time 48 | response.end = time.Now() 49 | c.JSON(http.StatusOK, response) 50 | } 51 | } 52 | 53 | func (s *Server) handleQuery() gin.HandlerFunc { 54 | return func(c *gin.Context) { 55 | c.Header("Content-Type", "application/json; charset=utf-8") 56 | 57 | var req QueryRequest 58 | if err := c.ShouldBindJSON(&req); err != nil { 59 | c.JSON(http.StatusBadRequest, gin.H{ 60 | "error": fmt.Sprintf("Bad request: %v", err), 61 | }) 62 | } 63 | 64 | resp := NewResponse() 65 | 66 | res, err := s.storage.Query(&shrek.QueryRequest{ 67 | Queries: req.Queries, 68 | UseTx: req.UseTx, 69 | IncludeTimings: req.IncludeTimings, 70 | Lvl: req.Lvl, 71 | }) 72 | if err != nil { 73 | if errors.Is(err, shrek.ErrNotLeader) { 74 | leader := s.leaderID() 75 | if leader == "" { 76 | c.JSON(http.StatusServiceUnavailable, gin.H{ 77 | "error": err.Error(), 78 | }) 79 | return 80 | } 81 | // TODO 82 | } 83 | resp.Error = err.Error() 84 | } else { 85 | resp.Results = res 86 | } 87 | 88 | resp.end = time.Now() 89 | c.JSON(http.StatusOK, resp) 90 | } 91 | } 92 | 93 | func (s *Server) handleStats() gin.HandlerFunc { 94 | return func(c *gin.Context) { 95 | c.Header("Content-Type", "application/json") 96 | 97 | storageStats, err := s.storage.Stats() 98 | if err != nil { 99 | c.JSON(http.StatusInternalServerError, gin.H{ 100 | "error": err, 101 | }) 102 | return 103 | } 104 | 105 | stats := map[string]interface{}{ 106 | "runtime": map[string]interface{}{ 107 | "GOARCH": runtime.GOARCH, 108 | "GOOS": runtime.GOOS, 109 | "GOMAXPROCS": runtime.GOMAXPROCS(0), 110 | "num_cpu": runtime.NumCPU(), 111 | "num_goroutine": runtime.NumGoroutine(), 112 | "version": runtime.Version(), 113 | }, 114 | "storage": storageStats, 115 | "server": map[string]interface{}{ 116 | "addr": s.addr.String(), 117 | }, 118 | "node": map[string]interface{}{ 119 | "start_time": s.start, 120 | "uptime": time.Since(s.start).String(), 121 | }, 122 | } 123 | 124 | c.JSON(http.StatusOK, stats) 125 | } 126 | } 127 | 128 | func (s *Server) handleRemove() gin.HandlerFunc { 129 | return func(c *gin.Context) { 130 | 131 | } 132 | } 133 | 134 | func (s *Server) handleJoin() gin.HandlerFunc { 135 | return func(c *gin.Context) { 136 | var req map[string]interface{} 137 | if err := c.ShouldBindJSON(&req); err != nil { 138 | c.JSON(http.StatusBadRequest, gin.H{ 139 | "error": fmt.Sprintf("Bad request: %v", err), 140 | }) 141 | } 142 | id, ok := req["id"] 143 | if !ok { 144 | c.JSON(http.StatusBadRequest, gin.H{ 145 | "error": errors.New("missing id param"), 146 | }) 147 | return 148 | } 149 | 150 | var meta map[string]string 151 | if _, ok := req["meta"].(map[string]interface{}); ok { 152 | meta = make(map[string]string) 153 | for key, value := range req["meta"].(map[string]interface{}) { 154 | if stringValue, ok := value.(string); ok { 155 | meta[key] = stringValue 156 | } 157 | } 158 | } 159 | 160 | addr, ok := req["addr"] 161 | if !ok { 162 | c.JSON(http.StatusBadRequest, gin.H{ 163 | "error": errors.New("missing addr param"), 164 | }) 165 | return 166 | } 167 | 168 | if err := s.storage.Join(id.(string), addr.(string), meta); err != nil { 169 | if errors.Is(err, shrek.ErrNotLeader) { 170 | leader := s.leaderAPIAddr() 171 | if leader == "" { 172 | c.JSON(http.StatusServiceUnavailable, gin.H{ 173 | "error": err.Error(), 174 | }) 175 | return 176 | } 177 | } 178 | b := bytes.NewBufferString(err.Error()) 179 | c.JSON(http.StatusInternalServerError, b) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /e2e/node.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/draculaas/shrek/internal/config" 9 | "github.com/draculaas/shrek/internal/core" 10 | "github.com/draculaas/shrek/internal/dyport" 11 | "github.com/draculaas/shrek/internal/network" 12 | "github.com/draculaas/shrek/internal/server" 13 | "github.com/draculaas/shrek/internal/shrek" 14 | "github.com/draculaas/shrek/internal/utils" 15 | "io" 16 | "net/http" 17 | "net/url" 18 | "os" 19 | "path/filepath" 20 | "strconv" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | type Node struct { 26 | APIAddr string 27 | RaftAddr string 28 | ID string 29 | Dir string 30 | Store *shrek.Shrek 31 | Service *server.Server 32 | } 33 | 34 | func (n *Node) Shutdown() { 35 | err := n.Store.Shutdown() 36 | if err != nil { 37 | panic(err) 38 | return 39 | } 40 | n.Service.ShutDown() 41 | err = os.RemoveAll(n.Dir) 42 | if err != nil { 43 | panic(err) 44 | return 45 | } 46 | } 47 | 48 | func (n *Node) sameAs(o *Node) bool { 49 | return n.RaftAddr == o.RaftAddr 50 | } 51 | 52 | func (n *Node) WaitForLeader() (string, error) { 53 | return n.Store.WaitForLeader(10 * time.Second) 54 | } 55 | 56 | func (n *Node) Execute(body server.QueryRequest) (string, error) { 57 | j, err := json.Marshal(body) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | u, err := n.getAPIURL("execute") 63 | if err != nil { 64 | return "", err 65 | } 66 | 67 | return n.postRequest(u, string(j)) 68 | } 69 | 70 | func (n *Node) Query(body server.QueryRequest) (string, error) { 71 | u, err := n.getAPIURL("query") 72 | if err != nil { 73 | return "", err 74 | } 75 | j, err := json.Marshal(body) 76 | if err != nil { 77 | return "", err 78 | } 79 | return n.postRequest(u, string(j)) 80 | } 81 | 82 | func (n *Node) Status() (string, error) { 83 | v, _ := url.Parse("http://" + n.APIAddr + "/api/db/stats") 84 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Second) 85 | defer cancel() 86 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, v.String(), nil) 87 | if err != nil { 88 | return "", err 89 | } 90 | resp, err := http.DefaultClient.Do(req) 91 | if err != nil { 92 | return "", err 93 | } 94 | if resp.StatusCode != http.StatusOK { 95 | return "", fmt.Errorf("invalid Status code: %q", resp.StatusCode) 96 | } 97 | defer resp.Body.Close() 98 | body, err := io.ReadAll(resp.Body) 99 | if err != nil { 100 | return "", err 101 | } 102 | return string(body), nil 103 | } 104 | 105 | func (n *Node) getAPIURL(suffix string) (*url.URL, error) { 106 | host := fmt.Sprintf("http://%s", n.APIAddr) 107 | u, err := url.Parse(fmt.Sprintf("%s/api/db/%s", host, suffix)) 108 | if err != nil { 109 | return nil, err 110 | } 111 | return u, nil 112 | } 113 | 114 | func (n *Node) postRequest(u *url.URL, j string) (string, error) { 115 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Second) 116 | defer cancel() 117 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(j)) 118 | if err != nil { 119 | return "", err 120 | } 121 | req.Header.Set("Content-Type", "application-type/json") 122 | resp, err := http.DefaultClient.Do(req) 123 | if err != nil { 124 | return "", err 125 | } 126 | defer resp.Body.Close() 127 | b, err := io.ReadAll(resp.Body) 128 | if err != nil { 129 | return "", err 130 | } 131 | return string(b), err 132 | } 133 | 134 | func (n *Node) Join(leader *Node) error { 135 | resp, err := joinRequest(leader.APIAddr, n.Store.ID(), n.RaftAddr) 136 | if err != nil { 137 | return err 138 | } 139 | if resp.StatusCode != http.StatusOK { 140 | return fmt.Errorf("failed to Join to the raft Cluster, Leader returned: %s", resp.Status) 141 | } 142 | defer resp.Body.Close() 143 | return nil 144 | } 145 | 146 | func joinRequest(nodeAddr, raftID, raftAddr string) (*http.Response, error) { 147 | b, err := json.Marshal(map[string]interface{}{"id": raftID, "addr": raftAddr}) 148 | if err != nil { 149 | return nil, err 150 | } 151 | u, err := url.Parse("http://" + nodeAddr + "/api/db/join") 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | ctx, cancel := context.WithTimeout(context.Background(), 500*time.Second) 157 | defer cancel() 158 | 159 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewReader(b)) 160 | if err != nil { 161 | return nil, err 162 | } 163 | req.Header.Set("Content-Type", "application-type/json") 164 | resp, err := http.DefaultClient.Do(req) 165 | if err != nil { 166 | return nil, err 167 | } 168 | return resp, nil 169 | } 170 | 171 | func CreateNewNode(enableSingle bool) *Node { 172 | ctx := context.Background() 173 | node := &Node{ 174 | Dir: utils.TempDir("shrek-interation-test-"), 175 | } 176 | 177 | nt := network.NewNetwork() 178 | if err := nt.Open(""); err != nil { 179 | panic(err.Error()) 180 | } 181 | 182 | ports, err := dyport.AllocatePorts(2) 183 | if err != nil { 184 | return nil 185 | } 186 | 187 | httpAddr, _ := utils.GetTCPAddr(fmt.Sprintf("localhost:%s", strconv.Itoa(ports[0]))) 188 | raftAddr, _ := utils.GetTCPAddr(fmt.Sprintf("localhost:%s", strconv.Itoa(ports[1]))) 189 | raftHeartbeatTimeout, _ := time.ParseDuration("1s") 190 | raftElectionTimeout, _ := time.ParseDuration("1s") 191 | raftOpenTimeout, _ := time.ParseDuration("120s") 192 | raftApplyTimeout, _ := time.ParseDuration("10s") 193 | 194 | raftID := utils.RandomString(5) 195 | 196 | cfg := &config.Config{ 197 | Environment: core.Local, 198 | ServerConfig: &config.ServerConfig{ 199 | HTTPAddr: httpAddr, 200 | }, 201 | StorageConfig: &config.StorageConfig{ 202 | RaftID: raftID, 203 | RaftDir: filepath.Join(node.Dir, raftID), 204 | RaftAddr: raftAddr, 205 | RaftHeartbeatTimeout: raftHeartbeatTimeout, 206 | RaftElectionTimeout: raftElectionTimeout, 207 | RaftApplyTimeout: raftApplyTimeout, 208 | RaftOpenTimeout: raftOpenTimeout, 209 | RaftSnapThreshold: uint64(8192), 210 | RaftShutdownOnRemove: false, 211 | DBCfg: &config.DBConfig{ 212 | DBFilename: filepath.Join(node.Dir, "db.sqlite"), 213 | InMemory: false, 214 | DSN: "", 215 | }, 216 | }, 217 | } 218 | 219 | node.Store = shrek.New(ctx, cfg, nt) 220 | 221 | if err := node.Store.Open(enableSingle); err != nil { 222 | node.Shutdown() 223 | panic(fmt.Sprintf("failed to open shrek: %s", err.Error())) 224 | } 225 | // store info about RaftAddr and ID 226 | node.RaftAddr = node.Store.Addr() 227 | node.ID = node.Store.ID() 228 | 229 | // launch server service 230 | node.Service = server.New(ctx, cfg.ServerConfig, node.Store) 231 | node.Service.Expvar = true 232 | if err := node.Service.Run(); err != nil { 233 | node.Shutdown() 234 | panic(fmt.Sprintf("failed to start HTTP service: %s", err.Error())) 235 | } 236 | 237 | node.APIAddr = node.Service.Addr().String() 238 | 239 | return node 240 | } 241 | 242 | func CreateLeaderNode() *Node { 243 | node := CreateNewNode(true) 244 | if _, err := node.WaitForLeader(); err != nil { 245 | node.Shutdown() 246 | panic(fmt.Sprintf("failed to achieve consensus: %s", err.Error())) 247 | } 248 | return node 249 | } 250 | -------------------------------------------------------------------------------- /internal/db/db_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/require" 6 | "os" 7 | "path" 8 | "testing" 9 | ) 10 | 11 | func TestDatabase(t *testing.T) { 12 | t.Run("test database file creation", func(t *testing.T) { 13 | dir, err := os.MkdirTemp("", "tmp-db-test") 14 | require.NoError(t, err) 15 | 16 | defer func(path string) { 17 | err := os.RemoveAll(path) 18 | require.NoError(t, err) 19 | }(dir) 20 | 21 | db, err := Open(path.Join(dir, "test_db")) 22 | require.NoError(t, err) 23 | require.NotNil(t, db) 24 | err = db.Close() 25 | require.NoError(t, err) 26 | }) 27 | 28 | t.Run("test database table creation", func(t *testing.T) { 29 | db, path := openDB(t) 30 | defer db.Close() 31 | defer os.Remove(path) 32 | 33 | _, err := db.Execute([]string{"create table test (id integer not null primary key, name TEXT)"}, false, false) 34 | require.NoError(t, err) 35 | 36 | r, err := db.Query([]string{"select * from test"}, false, false) 37 | require.NoError(t, err) 38 | 39 | res, err := json.Marshal(r) 40 | require.NoError(t, err) 41 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"]}]`, string(res)) 42 | }) 43 | 44 | t.Run("test empty statement", func(t *testing.T) { 45 | db, path := openDB(t) 46 | defer db.Close() 47 | defer os.Remove(path) 48 | 49 | _, err := db.Execute([]string{"create table test (id integer not null primary key, name TEXT)"}, false, false) 50 | require.NoError(t, err) 51 | 52 | _, err = db.Execute([]string{""}, false, false) 53 | require.NoError(t, err) 54 | 55 | _, err = db.Execute([]string{";"}, false, false) 56 | require.NoError(t, err) 57 | }) 58 | 59 | t.Run("test master statement", func(t *testing.T) { 60 | db, path := openDB(t) 61 | defer db.Close() 62 | defer os.Remove(path) 63 | 64 | _, err := db.Execute([]string{"create table test (id integer not null primary key, name text)"}, false, false) 65 | require.NoError(t, err) 66 | 67 | r, err := db.Query([]string{"select * from sqlite_master"}, false, false) 68 | require.NoError(t, err) 69 | res, err := json.Marshal(r) 70 | require.NoError(t, err) 71 | require.Equal(t, `[{"columns":["type","name","tbl_name","rootpage","sql"],"types":["text","text","text","int","text"],"values":[["table","test","test",2,"CREATE TABLE test (id integer not null primary key, name text)"]]}]`, string(res)) 72 | }) 73 | 74 | t.Run("test insert, delete, select execution", func(t *testing.T) { 75 | db, path := openDB(t) 76 | defer db.Close() 77 | defer os.Remove(path) 78 | 79 | _, err := db.Execute([]string{"create table test (id integer not null primary key, name text)"}, false, false) 80 | require.NoError(t, err) 81 | 82 | // insert new record 83 | _, err = db.Execute([]string{`insert into test(name) values("ana")`}, false, false) 84 | require.NoError(t, err) 85 | 86 | // select table 87 | raw, err := db.Query([]string{"select * from test"}, false, false) 88 | require.NoError(t, err) 89 | res, err := json.Marshal(raw) 90 | require.NoError(t, err) 91 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"ana"]]}]`, string(res)) 92 | 93 | // insert new record 94 | _, err = db.Execute([]string{`insert into test(name) values("lenny")`}, false, false) 95 | require.NoError(t, err) 96 | 97 | // select table after insert 98 | raw, err = db.Query([]string{"select * from test"}, false, false) 99 | require.NoError(t, err) 100 | res, err = json.Marshal(raw) 101 | require.NoError(t, err) 102 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"ana"],[2,"lenny"]]}]`, string(res)) 103 | 104 | // select from table where name="ana", after insert 105 | raw, err = db.Query([]string{`select * from test where name="ana"`}, false, false) 106 | require.NoError(t, err) 107 | res, err = json.Marshal(raw) 108 | require.NoError(t, err) 109 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"ana"]]}]`, string(res)) 110 | 111 | // select from table when record not exist 112 | raw, err = db.Query([]string{`select * from test where name="petya"`}, false, false) 113 | require.NoError(t, err) 114 | res, err = json.Marshal(raw) 115 | require.NoError(t, err) 116 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"]}]`, string(res)) 117 | 118 | // update record 119 | _, err = db.Execute([]string{`update test SET Name='vasya' where id=2`}, false, false) 120 | require.NoError(t, err) 121 | 122 | raw, err = db.Query([]string{"select * from test"}, false, false) 123 | require.NoError(t, err) 124 | res, err = json.Marshal(raw) 125 | require.NoError(t, err) 126 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"ana"],[2,"vasya"]]}]`, string(res)) 127 | 128 | // delete 129 | _, err = db.Execute([]string{"DELETE FROM test WHERE Id=2"}, false, false) 130 | require.NoError(t, err) 131 | 132 | raw, err = db.Query([]string{"SELECT * FROM test"}, false, false) 133 | require.NoError(t, err) 134 | res, err = json.Marshal(raw) 135 | require.NoError(t, err) 136 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"ana"]]}]`, string(res)) 137 | }) 138 | 139 | t.Run("test insert, delete, select failed cases", func(t *testing.T) { 140 | db, path := openDB(t) 141 | defer db.Close() 142 | defer os.Remove(path) 143 | 144 | q, err := db.Query([]string{"SELECT * FROM test"}, false, false) 145 | require.NoError(t, err) 146 | res, err := json.Marshal(q) 147 | require.NoError(t, err) 148 | require.Equal(t, `[{"error":"no such table: test"}]`, string(res)) 149 | 150 | e, err := db.Execute([]string{`insert into test(name) values("ana")`}, false, false) 151 | require.NoError(t, err) 152 | res, err = json.Marshal(e) 153 | require.NoError(t, err) 154 | require.Equal(t, `[{"error":"no such table: test"}]`, string(res)) 155 | 156 | // duplicate table 157 | _, err = db.Execute([]string{"create table test (id integer not null primary key, name text)"}, false, false) 158 | require.NoError(t, err) 159 | e, err = db.Execute([]string{"create table test (id integer not null primary key, name text)"}, false, false) 160 | require.NoError(t, err) 161 | res, err = json.Marshal(e) 162 | require.NoError(t, err) 163 | require.Equal(t, `[{"error":"table test already exists"}]`, string(res)) 164 | 165 | _, err = db.Execute([]string{`insert into test(id, name) values(1, "ana")`}, false, false) 166 | require.NoError(t, err) 167 | 168 | e, err = db.Execute([]string{`insert into test(id, name) values(1, "ana")`}, false, false) 169 | require.NoError(t, err) 170 | res, err = json.Marshal(e) 171 | require.NoError(t, err) 172 | require.Equal(t, `[{"error":"UNIQUE constraint failed: test.id"}]`, string(res)) 173 | 174 | e, err = db.Execute([]string{"test test"}, false, false) 175 | require.NoError(t, err) 176 | res, err = json.Marshal(e) 177 | require.NoError(t, err) 178 | require.Equal(t, `[{"error":"near \"test\": syntax error"}]`, string(res)) 179 | }) 180 | } 181 | 182 | func openDB(t *testing.T) (*DB, string) { 183 | f, err := os.CreateTemp("", "shrek_test") 184 | require.NoError(t, err) 185 | 186 | f.Close() 187 | 188 | db, err := Open(f.Name()) 189 | require.NoError(t, err) 190 | 191 | return db, f.Name() 192 | } 193 | -------------------------------------------------------------------------------- /internal/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "fmt" 7 | "github.com/mattn/go-sqlite3" 8 | "io" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type Queryer interface { 15 | Query(query string, args []driver.Value) (driver.Rows, error) 16 | } 17 | 18 | type Execer interface { 19 | Exec(query string, args []driver.Value) (driver.Result, error) 20 | } 21 | 22 | type DB struct { 23 | dbConn *sqlite3.SQLiteConn // Driver connection to database 24 | path string // Path to database file 25 | } 26 | 27 | type Result struct { 28 | LastInsertID int64 `json:"last_insert_id,omitempty"` 29 | RowsAffected int64 `json:"rows_affected,omitempty"` 30 | Error string `json:"error,omitempty"` 31 | Time float64 `json:"time,omitempty"` 32 | } 33 | 34 | type Rows struct { 35 | Columns []string `json:"columns,omitempty"` 36 | Types []string `json:"types,omitempty"` 37 | Values [][]interface{} `json:"values,omitempty"` 38 | Error string `json:"error,omitempty"` 39 | Time float64 `json:"time,omitempty"` 40 | } 41 | 42 | func New(fileName string) (*DB, error) { 43 | err := os.Remove(fileName) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return Open(fileName) 48 | } 49 | 50 | func Open(path string) (*DB, error) { 51 | return open(formatDSNWithFilePath(path, "")) 52 | } 53 | 54 | func OpenWithDSN(path, dsn string) (*DB, error) { 55 | return open(formatDSNWithFilePath(path, dsn)) 56 | } 57 | 58 | func OpenInMemoryWithDSN(dsn string) (*DB, error) { 59 | return open(formatDSNWithFilePath(":memory:", dsn)) 60 | } 61 | 62 | func open(path string) (*DB, error) { 63 | d := sqlite3.SQLiteDriver{} 64 | dbc, err := d.Open(path) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return &DB{ 69 | dbConn: dbc.(*sqlite3.SQLiteConn), 70 | path: path, 71 | }, nil 72 | } 73 | 74 | func (db *DB) Close() error { 75 | return db.dbConn.Close() 76 | } 77 | 78 | func (db *DB) Query(queries []string, useTx, includeTimings bool) ([]*Rows, error) { 79 | var queryer Queryer 80 | var transaction driver.Tx 81 | var res []*Rows 82 | 83 | err := func() error { 84 | var err error 85 | 86 | defer func() { 87 | if transaction != nil { 88 | if err != nil { 89 | err := transaction.Rollback() 90 | if err != nil { 91 | return 92 | } 93 | return 94 | } 95 | err := transaction.Commit() 96 | if err != nil { 97 | return 98 | } 99 | } 100 | }() 101 | 102 | queryer = db.dbConn 103 | 104 | if useTx { 105 | transaction, err = db.dbConn.Begin() 106 | if err != nil { 107 | return err 108 | } 109 | } 110 | 111 | for _, query := range queries { 112 | if query == "" { 113 | continue 114 | } 115 | 116 | rows := &Rows{} 117 | start := time.Now() 118 | 119 | response, err := queryer.Query(query, nil) 120 | if err != nil { 121 | rows.Error = err.Error() 122 | res = append(res, rows) 123 | continue 124 | } 125 | 126 | defer response.Close() 127 | 128 | columns := response.Columns() 129 | 130 | rows.Columns = columns 131 | rows.Types = response.(*sqlite3.SQLiteRows).DeclTypes() 132 | dest := make([]driver.Value, len(rows.Columns)) 133 | 134 | for { 135 | err := response.Next(dest) 136 | if err != nil { 137 | if !errors.Is(err, io.EOF) { 138 | rows.Error = err.Error() 139 | } 140 | break 141 | } 142 | 143 | rows.Values = append(rows.Values, normalizeRowValues(dest, rows.Types)) 144 | } 145 | 146 | if includeTimings { 147 | rows.Time = time.Since(start).Seconds() 148 | } 149 | 150 | res = append(res, rows) 151 | } 152 | 153 | return nil 154 | }() 155 | 156 | return res, err 157 | } 158 | 159 | // Backup ... consistent database snapshot 160 | func (db *DB) Backup(path string) error { 161 | source, err := Open(path) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | defer func(db *DB, err *error) { 167 | cerr := db.Close() 168 | if *err == nil { 169 | *err = cerr 170 | } 171 | }(source, &err) 172 | 173 | if err := copyDatabase(source.dbConn, db.dbConn); err != nil { 174 | return err 175 | } 176 | 177 | return err 178 | } 179 | 180 | // Execute ... TODO improve logic 181 | func (db *DB) Execute(queries []string, useTx, includeTimings bool) ([]*Result, error) { 182 | var res []*Result 183 | var execer Execer 184 | var rollback bool 185 | var transaction driver.Tx 186 | 187 | err := func() error { 188 | var err error 189 | defer func() { 190 | if transaction != nil { 191 | if rollback { 192 | _ = transaction.Rollback() 193 | return 194 | } 195 | _ = transaction.Commit() 196 | } 197 | }() 198 | 199 | handleError := func(result *Result, err error) bool { 200 | result.Error = err.Error() 201 | res = append(res, result) 202 | if useTx { 203 | rollback = true // trigger the rollback 204 | return false 205 | } 206 | return true 207 | } 208 | 209 | execer = db.dbConn 210 | 211 | if useTx { 212 | transaction, err = db.dbConn.Begin() 213 | if err != nil { 214 | return err 215 | } 216 | } 217 | 218 | for _, query := range queries { 219 | if query == "" { 220 | continue 221 | } 222 | 223 | result := &Result{} 224 | startAt := time.Now() 225 | 226 | r, err := execer.Exec(query, nil) 227 | if err != nil { 228 | if handleError(result, err) { 229 | continue 230 | } 231 | break 232 | } 233 | 234 | if r == nil { 235 | continue 236 | } 237 | 238 | lastID, err := r.LastInsertId() 239 | if err != nil { 240 | if handleError(result, err) { 241 | continue 242 | } 243 | break 244 | } 245 | 246 | result.LastInsertID = lastID 247 | 248 | aRow, err := r.RowsAffected() 249 | if err != nil { 250 | if handleError(result, err) { 251 | continue 252 | } 253 | break 254 | } 255 | result.RowsAffected = aRow 256 | 257 | if includeTimings { 258 | result.Time = time.Since(startAt).Seconds() 259 | } 260 | 261 | res = append(res, result) 262 | } 263 | 264 | return nil 265 | }() 266 | 267 | return res, err 268 | } 269 | 270 | func copyDatabase(source *sqlite3.SQLiteConn, target *sqlite3.SQLiteConn) error { 271 | backup, err := source.Backup("main", target, "main") 272 | if err != nil { 273 | return err 274 | } 275 | 276 | for { 277 | done, err := backup.Step(-1) 278 | if err != nil { 279 | backupErr := backup.Finish() 280 | if backupErr != nil { 281 | return backupErr 282 | } 283 | return err 284 | } 285 | if done { 286 | break 287 | } 288 | time.Sleep(250 * time.Millisecond) 289 | } 290 | 291 | return backup.Finish() 292 | } 293 | 294 | func normalizeRowValues(row []driver.Value, types []string) []interface{} { 295 | values := make([]interface{}, len(types)) 296 | for i, v := range row { 297 | if isTextType(types[i]) { 298 | switch val := v.(type) { 299 | case []byte: 300 | values[i] = string(val) 301 | default: 302 | values[i] = val 303 | } 304 | } else { 305 | values[i] = v 306 | } 307 | } 308 | return values 309 | } 310 | 311 | // isTextType returns whether the given type has a SQLite text affinity. 312 | // http://www.sqlite.org/datatype3.html 313 | func isTextType(t string) bool { 314 | return t == "text" || 315 | t == "" || 316 | strings.HasPrefix(t, "varchar") || 317 | strings.HasPrefix(t, "varying character") || 318 | strings.HasPrefix(t, "nchar") || 319 | strings.HasPrefix(t, "native character") || 320 | strings.HasPrefix(t, "nvarchar") || 321 | strings.HasPrefix(t, "clob") 322 | } 323 | 324 | // formatDSNWithFilePath ... Format file path with data source name 325 | func formatDSNWithFilePath(filePath, dataSourceName string) string { 326 | if dataSourceName != "" { 327 | return fmt.Sprintf("file:%s?%s", filePath, dataSourceName) 328 | } 329 | return filePath 330 | } 331 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/draculaas/shrek/internal/config" 9 | sql "github.com/draculaas/shrek/internal/db" 10 | "github.com/draculaas/shrek/internal/logging" 11 | "github.com/draculaas/shrek/internal/shrek" 12 | ginzap "github.com/gin-contrib/zap" 13 | "github.com/gin-gonic/contrib/cors" 14 | "github.com/gin-gonic/gin" 15 | "go.uber.org/zap" 16 | "io" 17 | "net" 18 | "net/http" 19 | "net/url" 20 | "strconv" 21 | "time" 22 | ) 23 | 24 | const numAttempts = 3 25 | 26 | type Response struct { 27 | Results interface{} `json:"results,omitempty"` 28 | Error string `json:"error,omitempty"` 29 | Time float64 `json:"time,omitempty"` 30 | 31 | start time.Time 32 | end time.Time 33 | } 34 | 35 | type ExecuteRequest struct { 36 | Queries []string `json:"queries,omitempty"` 37 | UseTx bool `json:"useTx,omitempty"` 38 | IncludeTimings bool `json:"includeTimings,omitempty"` 39 | } 40 | 41 | type QueryRequest struct { 42 | Queries []string `json:"queries,omitempty"` 43 | UseTx bool `json:"useTx,omitempty"` 44 | IncludeTimings bool `json:"includeTimings,omitempty"` 45 | Lvl shrek.ConsistencyLevel `json:"lvl,omitempty"` 46 | } 47 | 48 | func (r *Response) SetTime() { 49 | r.Time = r.end.Sub(r.start).Seconds() 50 | } 51 | 52 | func NewResponse() *Response { 53 | return &Response{ 54 | start: time.Now(), 55 | } 56 | } 57 | 58 | type Server struct { 59 | ln net.Listener 60 | 61 | addr net.Addr 62 | cfg *config.ServerConfig 63 | storage Storage 64 | srv *http.Server 65 | 66 | start time.Time 67 | 68 | statuses map[string]StatsI 69 | 70 | Expvar bool 71 | Pprof bool 72 | 73 | BuildInfo map[string]interface{} 74 | 75 | logger *zap.Logger 76 | } 77 | 78 | // StatsI ... Interface that status providers are required to implement 79 | type StatsI interface { 80 | Stats() (interface{}, error) 81 | } 82 | 83 | type Storage interface { 84 | Execute(r *shrek.ExecuteRequest) ([]*sql.Result, error) 85 | 86 | Query(r *shrek.QueryRequest) ([]*sql.Rows, error) 87 | 88 | // Join ... Connects the node, 89 | // identified by the provided ID and reachable at the specified address, to the current node 90 | Join(id, addr string, metadata map[string]string) error 91 | 92 | // Remove ... Remove node from the cluster 93 | Remove(addr string) error 94 | 95 | // LeaderID ... Returns the raft leader 96 | LeaderID() (string, error) 97 | 98 | // Stats ... Returns info about the storage 99 | Stats() (map[string]interface{}, error) 100 | 101 | // GetMetadata ... Returns metadata for specific node_id and for a given key 102 | GetMetadata(id, key string) string 103 | } 104 | 105 | func New(ctx context.Context, cfg *config.ServerConfig, s Storage) *Server { 106 | logger := logging.WithContext(ctx) 107 | return &Server{ 108 | cfg: cfg, 109 | addr: cfg.HTTPAddr, 110 | storage: s, 111 | start: time.Now(), 112 | statuses: make(map[string]StatsI), 113 | logger: logger, 114 | } 115 | } 116 | 117 | func (s *Server) Run() error { 118 | router := gin.Default() 119 | 120 | router.Use(ginzap.Ginzap(s.logger, time.RFC3339, true)) 121 | router.Use(ginzap.RecoveryWithZap(s.logger, true)) 122 | 123 | if s.cfg.AllowedOrigins != nil && s.cfg.AllowedMethods != nil { 124 | allowAllOrigins := len(s.cfg.AllowedOrigins) == 1 && s.cfg.AllowedOrigins[0] == "*" 125 | allowedOrigins := s.cfg.AllowedOrigins 126 | if allowAllOrigins { 127 | allowedOrigins = nil 128 | } 129 | router.Use(cors.New(cors.Config{ 130 | AllowAllOrigins: allowAllOrigins, 131 | AllowedOrigins: allowedOrigins, 132 | AllowedMethods: s.cfg.AllowedMethods, 133 | AllowedHeaders: s.cfg.AllowedHeaders, 134 | })) 135 | } 136 | 137 | router.POST("/api/db/execute", s.handleExecute()) 138 | router.POST("/api/db/query", s.handleQuery()) 139 | router.GET("/api/db/stats", s.handleStats()) 140 | router.DELETE("/api/db/remove", s.handleRemove()) 141 | router.POST("/api/db/join", s.handleJoin()) 142 | 143 | srv := &http.Server{ 144 | Handler: router, 145 | ReadHeaderTimeout: 5 * time.Second, 146 | } 147 | 148 | ln, err := net.Listen("tcp", s.addr.String()) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | s.ln = ln 154 | s.srv = srv 155 | 156 | go func() { 157 | err := srv.Serve(s.ln) 158 | if err != nil { 159 | s.logger.Info("failed to execute HTTP Server() call", zap.Error(err)) 160 | } 161 | }() 162 | 163 | s.logger.Info("service running on", zap.String("addr", s.addr.String())) 164 | s.srv = srv 165 | 166 | return nil 167 | } 168 | 169 | func (s *Server) ShutDown() { 170 | _ = s.ln.Close() 171 | _ = s.srv.Close() 172 | } 173 | 174 | func (s *Server) Addr() net.Addr { 175 | return s.ln.Addr() 176 | } 177 | 178 | // Join ... Sequentially processes joinAddr, updating node ID (id) and Raft 179 | // address (addr) for each joining node. It returns the successfully used endpoint 180 | // for joining the cluster. 181 | // 182 | // The function walks through joinAddr, associating each node with its ID and Raft 183 | // address. The resulting endpoint is the successfully utilized address during the 184 | // cluster joining process. 185 | // 186 | // Example: 187 | // 188 | // endpoint, err := Join(joinAddr, id, addr, meta) 189 | // if err != nil { 190 | // log.Fatal("Failed to set joining node information:", err) 191 | // } 192 | // log.Printf("Successfully joined the cluster. Endpoint: %s", endpoint) 193 | func Join(jA []string, id string, addr *net.TCPAddr, meta map[string]string) (string, error) { 194 | var err error 195 | var fullURL string 196 | logger := logging.NoContext() 197 | joinAddr := make([]*url.URL, 0) 198 | for _, a := range jA { 199 | u, err := url.Parse(fmt.Sprintf("%s/api/db/join", a)) 200 | if err != nil { 201 | return "", err 202 | } 203 | joinAddr = append(joinAddr, u) 204 | } 205 | 206 | for i := 0; i < numAttempts; i++ { 207 | for _, joinAddr := range joinAddr { 208 | fullURL, err = join(joinAddr, id, addr, meta) 209 | if err == nil { 210 | return fullURL, nil 211 | } 212 | } 213 | time.Sleep(2 * time.Second) 214 | } 215 | 216 | logger.Error("failed to join raft cluster", 217 | zap.String("attempts", strconv.Itoa(numAttempts)), 218 | zap.Error(err), 219 | ) 220 | 221 | return "", err 222 | } 223 | 224 | func join(joinAddr *url.URL, id string, addr *net.TCPAddr, meta map[string]string) (string, error) { 225 | logger := logging.NoContext() 226 | if id == "" { 227 | return "", fmt.Errorf("node ID is empty") 228 | } 229 | 230 | tr := &http.Transport{} 231 | client := &http.Client{Transport: tr} 232 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 233 | return http.ErrUseLastResponse 234 | } 235 | 236 | for { 237 | b, err := json.Marshal(map[string]interface{}{ 238 | "id": id, 239 | "addr": addr.String(), 240 | "meta": meta, 241 | }) 242 | if err != nil { 243 | return "", err 244 | } 245 | 246 | resp, err := client.Post( //nolint:noctx // no need ctx 247 | joinAddr.String(), 248 | "application-type/json", 249 | bytes.NewReader(b), 250 | ) 251 | if err != nil { 252 | return "", err 253 | } 254 | defer resp.Body.Close() 255 | 256 | logger.Debug("Join request, method: POST", zap.String("url", joinAddr.String())) 257 | 258 | b, err = io.ReadAll(resp.Body) 259 | if err != nil { 260 | return "", err 261 | } 262 | 263 | switch resp.StatusCode { 264 | case http.StatusOK: 265 | return joinAddr.String(), nil 266 | case http.StatusMovedPermanently: 267 | redirectURL := resp.Header.Get("location") 268 | if redirectURL == "" { 269 | return "", fmt.Errorf("failed to join, invalid redirect received") 270 | } 271 | joinAddr, err = url.Parse(redirectURL) 272 | if err != nil { 273 | return "", fmt.Errorf("failed to join, invalid redirect received") 274 | } 275 | continue 276 | case http.StatusBadRequest: 277 | if joinAddr.Scheme == "https" { 278 | return "", fmt.Errorf("failed to join, node returned: %s: (%s)", resp.Status, string(b)) 279 | } 280 | logger.Info("join via HTTP failed, trying via HTTPS") 281 | joinAddr.Scheme = "https" 282 | continue 283 | default: 284 | return "", fmt.Errorf("failed to join, node returned: %s: (%s)", resp.Status, string(b)) 285 | } 286 | } 287 | } 288 | 289 | func (s *Server) leaderAPIAddr() string { 290 | id, err := s.storage.LeaderID() 291 | if err != nil { 292 | return "" 293 | } 294 | return s.storage.GetMetadata(id, "api_addr") 295 | } 296 | 297 | func (s *Server) leaderID() string { 298 | id, err := s.storage.LeaderID() 299 | if err != nil { 300 | return "" 301 | } 302 | return id 303 | } 304 | -------------------------------------------------------------------------------- /internal/shrek/shrek_test.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/draculaas/shrek/internal/config" 7 | "github.com/draculaas/shrek/internal/core" 8 | "github.com/draculaas/shrek/internal/utils" 9 | "github.com/stretchr/testify/require" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func Test_Shrek(t *testing.T) { 18 | t.Run("test open shrek single node", func(t *testing.T) { 19 | ms := createMockStore() 20 | defer func(path string) { 21 | err := os.RemoveAll(path) 22 | if err != nil { 23 | panic(err) 24 | } 25 | }(ms.Path()) 26 | 27 | err := ms.Open(true) 28 | require.NoError(t, err) 29 | 30 | _, err = ms.WaitForLeader(10 * time.Second) 31 | require.NoError(t, err) 32 | leaderAddr, _ := ms.LeaderAddr() 33 | require.Equal(t, string(leaderAddr), ms.Addr()) 34 | 35 | id, err := ms.LeaderID() 36 | require.NoError(t, err) 37 | 38 | require.Equal(t, id, ms.raftID) 39 | }) 40 | 41 | t.Run("test open shrek close single node", func(t *testing.T) { 42 | ms := createMockStore() 43 | defer func(path string) { 44 | err := os.RemoveAll(path) 45 | if err != nil { 46 | panic(err) 47 | } 48 | }(ms.Path()) 49 | 50 | err := ms.Open(true) 51 | require.NoError(t, err) 52 | 53 | _, err = ms.WaitForLeader(10 * time.Second) 54 | require.NoError(t, err) 55 | err = ms.Shutdown() 56 | require.NoError(t, err) 57 | }) 58 | 59 | t.Run("test single node in-memory execute query", func(t *testing.T) { 60 | ms := createMockStore() 61 | defer func(path string) { 62 | err := os.RemoveAll(path) 63 | if err != nil { 64 | panic(err) 65 | } 66 | }(ms.Path()) 67 | 68 | err := ms.Open(true) 69 | defer func(ms *Shrek) { 70 | err := ms.Shutdown() 71 | if err != nil { 72 | panic(err) 73 | } 74 | }(ms) 75 | require.NoError(t, err) 76 | 77 | _, err = ms.WaitForLeader(10 * time.Second) 78 | require.NoError(t, err) 79 | 80 | queries := []string{ 81 | `create table test (id integer not null primary key, name text)`, 82 | `insert into test(id, name) values (1, "vasya")`, 83 | } 84 | 85 | resp, err := ms.Execute(&ExecuteRequest{queries, false, false}) 86 | require.NoError(t, err) 87 | require.Equal(t, resp, resp) 88 | 89 | raw, err := ms.Query(&QueryRequest{[]string{`select * from test`}, false, false, None}) 90 | require.NoError(t, err) 91 | 92 | res, err := json.Marshal(raw) 93 | require.NoError(t, err) 94 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"]]}]`, string(res)) 95 | }) 96 | 97 | t.Run("test single node in-memory execute query", func(t *testing.T) { 98 | ms := createMockStore() 99 | defer func(path string) { 100 | err := os.RemoveAll(path) 101 | if err != nil { 102 | panic(err) 103 | } 104 | }(ms.Path()) 105 | 106 | err := ms.Open(true) 107 | defer func(ms *Shrek) { 108 | err := ms.Shutdown() 109 | if err != nil { 110 | panic(err) 111 | } 112 | }(ms) 113 | require.NoError(t, err) 114 | 115 | _, err = ms.WaitForLeader(10 * time.Second) 116 | require.NoError(t, err) 117 | 118 | queries := []string{ 119 | `insert into test(id, name) values (1, "vasya")`, 120 | } 121 | 122 | resp, err := ms.Execute(&ExecuteRequest{queries, false, false}) 123 | require.NoError(t, err) 124 | res, err := json.Marshal(resp) 125 | require.NoError(t, err) 126 | require.Equal(t, `[{"error":"no such table: test"}]`, string(res)) 127 | }) 128 | 129 | t.Run("test single node multi execution query", func(t *testing.T) { 130 | ms := createMockStore() 131 | defer func(path string) { 132 | err := os.RemoveAll(path) 133 | if err != nil { 134 | panic(err) 135 | } 136 | }(ms.Path()) 137 | 138 | err := ms.Open(true) 139 | defer func(ms *Shrek) { 140 | err := ms.Shutdown() 141 | if err != nil { 142 | panic(err) 143 | } 144 | }(ms) 145 | require.NoError(t, err) 146 | 147 | _, err = ms.WaitForLeader(10 * time.Second) 148 | require.NoError(t, err) 149 | 150 | queries := []string{ 151 | `create table test (id integer not null primary key, name TEXT)`, 152 | `insert into test(id, name) values (1, "vasya")`, 153 | } 154 | 155 | _, err = ms.Execute(&ExecuteRequest{queries, false, false}) 156 | require.NoError(t, err) 157 | 158 | for i := 0; i < 3; i++ { 159 | resp, err := ms.Query(&QueryRequest{ 160 | []string{`select * from test`}, 161 | false, 162 | false, 163 | None, 164 | }) 165 | require.NoError(t, err) 166 | res, err := json.Marshal(resp) 167 | require.NoError(t, err) 168 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"]]}]`, string(res)) 169 | } 170 | }) 171 | 172 | t.Run("test single node execution query tx", func(t *testing.T) { 173 | ms := createMockStore() 174 | defer func(path string) { 175 | err := os.RemoveAll(path) 176 | if err != nil { 177 | panic(err) 178 | } 179 | }(ms.Path()) 180 | 181 | err := ms.Open(true) 182 | defer func(ms *Shrek) { 183 | err := ms.Shutdown() 184 | if err != nil { 185 | panic(err) 186 | } 187 | }(ms) 188 | require.NoError(t, err) 189 | 190 | _, err = ms.WaitForLeader(10 * time.Second) 191 | require.NoError(t, err) 192 | 193 | queries := []string{ 194 | `create table test (id integer not null primary key, name TEXT)`, 195 | `insert into test(id, name) values (1, "vasya")`, 196 | } 197 | 198 | _, err = ms.Execute(&ExecuteRequest{queries, true, false}) 199 | require.NoError(t, err) 200 | 201 | for i := 0; i < 3; i++ { 202 | resp, err := ms.Query(&QueryRequest{ 203 | []string{`select * from test`}, 204 | false, 205 | true, 206 | None, 207 | }) 208 | require.NoError(t, err) 209 | res, err := json.Marshal(resp) 210 | require.NoError(t, err) 211 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"]]}]`, string(res)) 212 | } 213 | }) 214 | 215 | t.Run("test single node load", func(t *testing.T) { 216 | ms := createMockStore() 217 | defer func(path string) { 218 | err := os.RemoveAll(path) 219 | if err != nil { 220 | panic(err) 221 | } 222 | }(ms.Path()) 223 | 224 | err := ms.Open(true) 225 | defer func(ms *Shrek) { 226 | err := ms.Shutdown() 227 | if err != nil { 228 | panic(err) 229 | } 230 | }(ms) 231 | require.NoError(t, err) 232 | 233 | _, err = ms.WaitForLeader(10 * time.Second) 234 | require.NoError(t, err) 235 | 236 | dump := ` 237 | pragma foreign_keys=off; 238 | begin transaction; 239 | create table test(id integer not null primary key, name text); 240 | insert into "test" values(1,'vasya'); 241 | commit; 242 | ` 243 | 244 | queries := []string{dump} 245 | 246 | _, err = ms.Execute(&ExecuteRequest{queries, false, false}) 247 | require.NoError(t, err) 248 | 249 | resp, err := ms.Query(&QueryRequest{ 250 | []string{`select * from test`}, 251 | false, 252 | true, 253 | Strong, 254 | }) 255 | require.NoError(t, err) 256 | res, err := json.Marshal(resp) 257 | require.NoError(t, err) 258 | require.Equal(t, `[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"]]}]`, string(res)) 259 | }) 260 | } 261 | 262 | type mockListener struct { 263 | root net.Listener 264 | } 265 | 266 | func (m *mockListener) Dial(addr string, timeout time.Duration) (net.Conn, error) { 267 | return net.DialTimeout("tcp", addr, timeout) 268 | } 269 | 270 | func (m *mockListener) Accept() (net.Conn, error) { 271 | return m.root.Accept() 272 | } 273 | 274 | func (m *mockListener) Close() error { 275 | return m.root.Close() 276 | } 277 | 278 | func (m *mockListener) Addr() net.Addr { 279 | return m.root.Addr() 280 | } 281 | 282 | func createMockLister(addr string) Listener { 283 | ln, err := net.Listen("tcp", addr) 284 | if err != nil { 285 | panic("failed to create the listener") 286 | } 287 | return &mockListener{ 288 | root: ln, 289 | } 290 | } 291 | 292 | func createMockStore() *Shrek { 293 | dir := utils.TempDir("shrek-unit-test-") 294 | defer os.RemoveAll(dir) 295 | 296 | ln := createMockLister("localhost:0") 297 | 298 | httpAddr, _ := utils.GetTCPAddr("localhost:4001") 299 | raftAddr, _ := utils.GetTCPAddr("localhost:4002") 300 | raftHeartbeatTimeout, _ := time.ParseDuration("1s") 301 | raftElectionTimeout, _ := time.ParseDuration("1s") 302 | raftOpenTimeout, _ := time.ParseDuration("120s") 303 | raftApplyTimeout, _ := time.ParseDuration("10s") 304 | 305 | raftID := utils.RandomString(5) 306 | 307 | cfg := &config.Config{ 308 | Environment: core.Local, 309 | ServerConfig: &config.ServerConfig{ 310 | HTTPAddr: httpAddr, 311 | }, 312 | StorageConfig: &config.StorageConfig{ 313 | RaftID: raftID, 314 | RaftDir: filepath.Join(dir, raftID), 315 | RaftAddr: raftAddr, 316 | RaftHeartbeatTimeout: raftHeartbeatTimeout, 317 | RaftElectionTimeout: raftElectionTimeout, 318 | RaftApplyTimeout: raftApplyTimeout, 319 | RaftOpenTimeout: raftOpenTimeout, 320 | RaftSnapThreshold: uint64(8192), 321 | RaftShutdownOnRemove: false, 322 | DBCfg: &config.DBConfig{ 323 | DBFilename: filepath.Join(dir, "db.sqlite"), 324 | InMemory: false, 325 | DSN: "", 326 | }, 327 | }, 328 | } 329 | 330 | return New(context.TODO(), cfg, ln) 331 | } 332 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # Analysis timeout, e.g. 30s, 5m. 3 | # Default: 1m 4 | timeout: 5m 5 | 6 | # https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml 7 | linters-settings: 8 | cyclop: 9 | # The maximal code complexity to report. 10 | # Default: 10 11 | max-complexity: 30 12 | # The maximal average package complexity. 13 | # If it's higher than 0.0 (float) the check is enabled 14 | # Default: 0.0 15 | package-average: 20.0 16 | 17 | errcheck: 18 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 19 | # Default: false 20 | check-type-assertions: true 21 | 22 | exhaustive: 23 | # Program elements to check for exhaustiveness. 24 | # Default: [ switch ] 25 | check: 26 | - switch 27 | - map 28 | 29 | exhaustruct: 30 | # List of regular expressions to exclude struct packages and names from check. 31 | # Default: [] 32 | exclude: 33 | # std libs 34 | - "^os/exec.Cmd$" 35 | # public libs 36 | - "^github.com/stretchr/testify/mock.Mock$" 37 | 38 | funlen: 39 | # Assert a maximum number of lines for a function. 40 | # Default: 60 41 | lines: 150 42 | # Assert a maximum number of statements in a function. 43 | # Default: 40 44 | statements: 60 45 | 46 | gocognit: 47 | # Minimal code complexity to report. 48 | # Default: 30 49 | min-complexity: 60 50 | 51 | gocritic: 52 | # Settings passed to gocritic. 53 | # The settings key is the name of a supported gocritic checker. 54 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 55 | settings: 56 | captLocal: 57 | # Whether to restrict checker to params only. 58 | # Default: true 59 | paramsOnly: false 60 | underef: 61 | # Whether to skip (*x).method() calls where x is a pointer receiver. 62 | # Default: true 63 | skipRecvDeref: false 64 | 65 | # gomnd: 66 | # # List of function regex patterns to exclude from analysis. 67 | # # Default: [] 68 | # ignored-functions: 69 | # - 70 | gomodguard: 71 | blocked: 72 | # List of blocked modules. 73 | # Default: [] 74 | modules: 75 | - 76 | govet: 77 | # Enable all analyzers. 78 | # Default: false 79 | enable-all: true 80 | # Disable analyzers by name. 81 | # Run `go tool vet help` to see all analyzers. 82 | # Default: [] 83 | disable: 84 | - fieldalignment # too strict 85 | # Settings per analyzer. 86 | settings: 87 | shadow: 88 | # Whether to be strict about shadowing; can be noisy. 89 | # Default: false 90 | strict: true 91 | 92 | nakedret: 93 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 94 | # Default: 30 95 | max-func-lines: 0 96 | 97 | nolintlint: 98 | # Exclude following linters from requiring an explanation. 99 | # Default: [] 100 | allow-no-explanation: [ funlen, gocognit, lll ] 101 | # Enable to require an explanation of nonzero length after each nolint directive. 102 | # Default: false 103 | require-explanation: true 104 | # Enable to require nolint directives to mention the specific linter being suppressed. 105 | # Default: false 106 | require-specific: true 107 | 108 | tenv: 109 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. 110 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. 111 | # Default: false 112 | all: true 113 | 114 | 115 | linters: 116 | disable-all: true 117 | enable: 118 | ## enabled by default 119 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 120 | - gosimple # specializes in simplifying a code 121 | - ineffassign # detects when assignments to existing variables are not used 122 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 123 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code 124 | - unused # checks for unused constants, variables, functions and types 125 | ## disabled by default 126 | - asasalint # checks for pass []any as any in variadic func(...any) 127 | - asciicheck # checks that your code does not contain non-ASCII identifiers 128 | - bidichk # checks for dangerous unicode character sequences 129 | - bodyclose # checks whether HTTP response body is closed successfully 130 | - cyclop # checks function and package cyclomatic complexity 131 | - dupl # tool for code clone detection 132 | - durationcheck # checks for two durations multiplied together 133 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 134 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 135 | - execinquery # checks query string in Query function which reads your Go src files and warning it finds 136 | - exhaustive # checks exhaustiveness of enum switch statements 137 | - exportloopref # checks for pointers to enclosing loop variables 138 | - forbidigo # forbids identifiers 139 | - funlen # tool for detection of long functions 140 | - gocheckcompilerdirectives # validates go compiler directive comments (//go:) 141 | - gochecknoinits # checks that no init functions are present in Go code 142 | - gocognit # computes and checks the cognitive complexity of functions 143 | - goconst # finds repeated strings that could be replaced by a constant 144 | - gocritic # provides diagnostics that check for bugs, performance and style issues 145 | - gocyclo # computes and checks the cyclomatic complexity of functions 146 | #- goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 147 | # - gomnd # detects magic numbers 148 | - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations 149 | - goprintffuncname # checks that printf-like functions are named with f at the end 150 | - gosec # inspects source code for security problems 151 | - lll # reports long lines 152 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) 153 | - makezero # finds slice declarations with non-zero initial length 154 | - misspell # Ensures real english is used within strings 155 | - nakedret # finds naked returns in functions greater than a specified function length 156 | - nestif # reports deeply nested if statements 157 | - nilerr # finds the code that returns nil even if it checks that the error is not nil 158 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 159 | - noctx # finds sending server request without context.Context 160 | - nolintlint # reports ill-formed or insufficient nolint directives 161 | - nonamedreturns # reports all named returns 162 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 163 | - predeclared # finds code that shadows one of Go's predeclared identifiers 164 | - promlinter # checks Prometheus metrics naming via promlint 165 | - reassign # checks that package variables are not reassigned 166 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 167 | - rowserrcheck # checks whether Err of rows is checked successfully 168 | - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed 169 | - stylecheck # is a replacement for golint 170 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 171 | - testableexamples # checks if examples are testable (have an expected output) 172 | - testpackage # makes you use a separate _test package 173 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 174 | - unconvert # removes unnecessary type conversions 175 | - unparam # reports unused function parameters 176 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 177 | - wastedassign # finds wasted assignment statements 178 | - whitespace # detects leading and trailing whitespace 179 | 180 | ## May want to enable 181 | #- - gochecknoglobals # checks that no global variables exist 182 | #- - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 183 | #- - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod 184 | #- - godot # checks if comments end in a period 185 | #- decorder # checks declaration order and count of types, constants, variables and functions 186 | #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized 187 | #- gci # controls golang package import order and makes it always deterministic 188 | #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega 189 | #- godox # detects FIXME, TODO and other comment keywords 190 | #- goheader # checks is file header matches to pattern 191 | #- interfacebloat # checks the number of methods inside an interface 192 | #- ireturn # accept interfaces, return concrete types 193 | #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated 194 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope 195 | #- wrapcheck # checks that errors returned from external packages are wrapped 196 | 197 | issues: 198 | # Maximum count of issues with the same text. 199 | # Set to 0 to disable. 200 | # Default: 3 201 | max-same-issues: 50 202 | 203 | exclude-rules: 204 | - source: "(noinspection|TODO)" 205 | linters: [ godot ] 206 | - source: "// noinspection" 207 | linters: 208 | - gocritic 209 | - unparam 210 | 211 | - path: "_test\\.go" 212 | linters: 213 | - gocognit 214 | - govet 215 | - testpackage 216 | - bodyclose 217 | - dupl 218 | - funlen 219 | - goconst 220 | - gosec 221 | - noctx 222 | - wrapcheck 223 | - lll 224 | - whitespace 225 | -------------------------------------------------------------------------------- /e2e/cluster_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/draculaas/shrek/internal/server" 6 | "github.com/draculaas/shrek/internal/shrek" 7 | "github.com/stretchr/testify/require" 8 | "testing" 9 | ) 10 | 11 | func Test_SingleNodeCRUD(t *testing.T) { 12 | node := CreateLeaderNode() 13 | defer node.Shutdown() 14 | 15 | testCases := []struct { 16 | body server.QueryRequest 17 | expected string 18 | execute bool 19 | }{ 20 | { 21 | body: server.QueryRequest{ 22 | Queries: []string{`create table test(id integer not null primary key, name text)`}, 23 | UseTx: false, 24 | IncludeTimings: false, 25 | }, 26 | expected: `{"results":[{}]}`, 27 | execute: true, 28 | }, 29 | { 30 | body: server.QueryRequest{ 31 | Queries: []string{`insert into test(name) values ("vasya")`}, 32 | UseTx: false, 33 | IncludeTimings: false, 34 | }, 35 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 36 | execute: true, 37 | }, 38 | { 39 | body: server.QueryRequest{ 40 | Queries: []string{`insert into test2(name) values("ivan")`}, 41 | UseTx: false, 42 | IncludeTimings: false, 43 | }, 44 | expected: `{"results":[{"error":"no such table: test2"}]}`, 45 | execute: true, 46 | }, 47 | { 48 | body: server.QueryRequest{ 49 | Queries: []string{`insert ahah ahah`}, 50 | UseTx: false, 51 | IncludeTimings: false, 52 | }, 53 | expected: `{"results":[{"error":"near \"ahah\": syntax error"}]}`, 54 | execute: true, 55 | }, 56 | { 57 | body: server.QueryRequest{ 58 | Queries: []string{`select * from test`}, 59 | UseTx: false, 60 | IncludeTimings: false, 61 | Lvl: shrek.Strong, 62 | }, 63 | expected: `{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"]]}]}`, 64 | execute: false, 65 | }, 66 | { 67 | body: server.QueryRequest{ 68 | Queries: []string{`drop table test`}, 69 | UseTx: false, 70 | IncludeTimings: false, 71 | }, 72 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 73 | execute: true, 74 | }, 75 | { 76 | body: server.QueryRequest{ 77 | Queries: []string{`drop table test`}, 78 | UseTx: false, 79 | IncludeTimings: false, 80 | }, 81 | expected: `{"results":[{"error":"no such table: test"}]}`, 82 | execute: true, 83 | }, 84 | // TODO double check this test cases 85 | { 86 | body: server.QueryRequest{ 87 | Queries: []string{`create table aaa (id integer not null primary key, name text)`}, 88 | UseTx: false, 89 | IncludeTimings: false, 90 | }, 91 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 92 | execute: true, 93 | }, 94 | { 95 | body: server.QueryRequest{ 96 | Queries: []string{`create table bbb (id integer not null primary key, sequence integer)`}, 97 | UseTx: false, 98 | IncludeTimings: false, 99 | }, 100 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 101 | execute: true, 102 | }, 103 | { 104 | body: server.QueryRequest{ 105 | Queries: []string{`insert into aaa(name) values("ana")`}, 106 | UseTx: false, 107 | IncludeTimings: false, 108 | }, 109 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 110 | execute: true, 111 | }, 112 | { 113 | body: server.QueryRequest{ 114 | Queries: []string{`insert into aaa(name) values("denis")`}, 115 | UseTx: false, 116 | IncludeTimings: false, 117 | }, 118 | expected: `{"results":[{"last_insert_id":2,"rows_affected":1}]}`, 119 | execute: true, 120 | }, 121 | { 122 | body: server.QueryRequest{ 123 | Queries: []string{`insert into bbb(sequence) values(10)`}, 124 | UseTx: false, 125 | IncludeTimings: false, 126 | }, 127 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 128 | execute: true, 129 | }, 130 | } 131 | 132 | for _, tc := range testCases { 133 | var resp string 134 | var err error 135 | if tc.execute { 136 | resp, err = node.Execute(tc.body) 137 | } else { 138 | resp, err = node.Query(tc.body) 139 | } 140 | require.NoError(t, err) 141 | require.Equal(t, tc.expected, resp) 142 | } 143 | } 144 | 145 | func Test_SingleNodeMutilInsert(t *testing.T) { 146 | node := CreateLeaderNode() 147 | defer node.Shutdown() 148 | 149 | testCases := []struct { 150 | body server.QueryRequest 151 | expected string 152 | execute bool 153 | }{ 154 | { 155 | body: server.QueryRequest{ 156 | Queries: []string{`create table aaa (id integer not null primary key, name text)`}, 157 | UseTx: false, 158 | IncludeTimings: false, 159 | }, 160 | expected: `{"results":[{}]}`, 161 | execute: true, 162 | }, 163 | { 164 | body: server.QueryRequest{ 165 | Queries: []string{`create table bbb (id integer not null primary key, sequence integer)`}, 166 | UseTx: false, 167 | IncludeTimings: false, 168 | }, 169 | expected: `{"results":[{}]}`, 170 | execute: true, 171 | }, 172 | { 173 | body: server.QueryRequest{ 174 | Queries: []string{`insert into aaa(name) values("ana")`}, 175 | UseTx: false, 176 | IncludeTimings: false, 177 | }, 178 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 179 | execute: true, 180 | }, 181 | { 182 | body: server.QueryRequest{ 183 | Queries: []string{`insert into aaa(name) values("denis")`}, 184 | UseTx: false, 185 | IncludeTimings: false, 186 | }, 187 | expected: `{"results":[{"last_insert_id":2,"rows_affected":1}]}`, 188 | execute: true, 189 | }, 190 | { 191 | body: server.QueryRequest{ 192 | Queries: []string{`insert into bbb(sequence) values(10)`}, 193 | UseTx: false, 194 | IncludeTimings: false, 195 | }, 196 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 197 | execute: true, 198 | }, 199 | { 200 | body: server.QueryRequest{ 201 | Queries: []string{`select * from aaa`, `select * from bbb`}, 202 | UseTx: false, 203 | IncludeTimings: false, 204 | }, 205 | expected: `{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"ana"],[2,"denis"]]},{"columns":["id","sequence"],"types":["integer","integer"],"values":[[1,10]]}]}`, 206 | execute: false, 207 | }, 208 | } 209 | 210 | for _, tc := range testCases { 211 | var resp string 212 | var err error 213 | if tc.execute { 214 | resp, err = node.Execute(tc.body) 215 | } else { 216 | resp, err = node.Query(tc.body) 217 | } 218 | require.NoError(t, err) 219 | require.Equal(t, tc.expected, resp) 220 | } 221 | 222 | } 223 | 224 | func Test_APIStatus(t *testing.T) { 225 | node := CreateLeaderNode() 226 | defer node.Shutdown() 227 | 228 | resp, err := node.Status() 229 | require.NoError(t, err) 230 | 231 | var j map[string]interface{} 232 | err = json.Unmarshal([]byte(resp), &j) 233 | require.NoError(t, err) 234 | } 235 | 236 | func Test_MultiNodes(t *testing.T) { 237 | node1 := CreateLeaderNode() 238 | defer node1.Shutdown() 239 | 240 | node2 := CreateNewNode(false) 241 | defer node1.Shutdown() 242 | 243 | err := node2.Join(node1) 244 | require.NoError(t, err) 245 | 246 | lAddr, err := node2.WaitForLeader() 247 | require.NoError(t, err) 248 | require.Equal(t, node1.RaftAddr, lAddr) 249 | 250 | c := &Cluster{ 251 | nodes: []*Node{node1, node2}, 252 | } 253 | 254 | l, err := c.Leader() 255 | require.NoError(t, err) 256 | 257 | node3 := CreateNewNode(false) 258 | defer node3.Shutdown() 259 | err = node3.Join(l) 260 | require.NoError(t, err) 261 | 262 | lAddr, err = node3.WaitForLeader() 263 | require.NoError(t, err) 264 | require.Equal(t, node1.RaftAddr, lAddr) 265 | 266 | c = &Cluster{ 267 | nodes: []*Node{node1, node2, node3}, 268 | } 269 | l, err = c.Leader() 270 | require.NoError(t, err) 271 | 272 | testCases := []struct { 273 | body server.QueryRequest 274 | expected string 275 | execute bool 276 | }{ 277 | { 278 | body: server.QueryRequest{ 279 | Queries: []string{`create table test (id integer not null primary key, name text)`}, 280 | UseTx: false, 281 | IncludeTimings: false, 282 | }, 283 | expected: `{"results":[{}]}`, 284 | execute: true, 285 | }, 286 | { 287 | body: server.QueryRequest{ 288 | Queries: []string{`insert into test(name) values("vasya")`}, 289 | UseTx: false, 290 | IncludeTimings: false, 291 | }, 292 | expected: `{"results":[{"last_insert_id":1,"rows_affected":1}]}`, 293 | execute: true, 294 | }, 295 | { 296 | body: server.QueryRequest{ 297 | Queries: []string{`select * from test`}, 298 | UseTx: false, 299 | IncludeTimings: false, 300 | }, 301 | expected: `{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"]]}]}`, 302 | execute: false, 303 | }, 304 | } 305 | 306 | for _, tc := range testCases { 307 | var resp string 308 | var err error 309 | if tc.execute { 310 | resp, err = l.Execute(tc.body) 311 | } else { 312 | resp, err = l.Query(tc.body) 313 | } 314 | require.NoError(t, err) 315 | require.Equal(t, tc.expected, resp) 316 | } 317 | 318 | // kill the Leader qpwjl 319 | t.Logf("kill the Leader Node %s, waiting the new Leader...", l.ID) 320 | l.Shutdown() 321 | c.RemoveNode(l) 322 | 323 | l, err = c.WaitForNewLeader(l) 324 | require.NoError(t, err) 325 | 326 | t.Logf("elected the new Leader Node %s", l.ID) 327 | 328 | t.Log("now Cluster contains 2 nodes, running queries...") 329 | 330 | testCases = []struct { 331 | body server.QueryRequest 332 | expected string 333 | execute bool 334 | }{ 335 | { 336 | body: server.QueryRequest{ 337 | Queries: []string{`create table test (id integer not null primary key, name text)`}, 338 | UseTx: false, 339 | IncludeTimings: false, 340 | }, 341 | expected: `{"results":[{"error":"table test already exists"}]}`, 342 | execute: true, 343 | }, 344 | { 345 | body: server.QueryRequest{ 346 | Queries: []string{`insert into test(name) values("ana")`}, 347 | UseTx: false, 348 | IncludeTimings: false, 349 | }, 350 | expected: `{"results":[{"last_insert_id":2,"rows_affected":1}]}`, 351 | execute: true, 352 | }, 353 | { 354 | body: server.QueryRequest{ 355 | Queries: []string{`select * from test`}, 356 | UseTx: false, 357 | IncludeTimings: false, 358 | }, 359 | expected: `{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"],[2,"ana"]]}]}`, 360 | execute: false, 361 | }, 362 | } 363 | 364 | for _, tc := range testCases { 365 | var resp string 366 | var err error 367 | if tc.execute { 368 | resp, err = l.Execute(tc.body) 369 | } else { 370 | resp, err = l.Query(tc.body) 371 | } 372 | require.NoError(t, err) 373 | require.Equal(t, tc.expected, resp) 374 | } 375 | 376 | } 377 | -------------------------------------------------------------------------------- /internal/server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/draculaas/shrek/internal/config" 10 | "github.com/draculaas/shrek/internal/core" 11 | sql "github.com/draculaas/shrek/internal/db" 12 | "github.com/draculaas/shrek/internal/shrek" 13 | "github.com/draculaas/shrek/internal/utils" 14 | "github.com/stretchr/testify/require" 15 | "io" 16 | "net" 17 | "net/http" 18 | "net/http/httptest" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func Test_HTTPServer(t *testing.T) { 27 | t.Run("test create a new server server", func(t *testing.T) { 28 | m := &MockStorage{} 29 | cfg := createMockConfig() 30 | s := New(context.TODO(), cfg.ServerConfig, m) 31 | defer s.ShutDown() 32 | err := s.Run() 33 | require.NoError(t, err) 34 | }) 35 | 36 | t.Run("test content type", func(t *testing.T) { 37 | m := &MockStorage{} 38 | cfg := createMockConfig() 39 | s := New(context.TODO(), cfg.ServerConfig, m) 40 | defer s.ShutDown() 41 | err := s.Run() 42 | require.NoError(t, err) 43 | 44 | apiRequest := fmt.Sprintf("http://%s/api/db/stats", s.Addr().String()) 45 | client := &http.Client{} 46 | 47 | resp, err := client.Get(apiRequest) 48 | require.NoError(t, err) 49 | require.Equal(t, "application/json", resp.Header.Get("Content-Type")) 50 | require.Equal(t, http.StatusOK, resp.StatusCode) 51 | }) 52 | 53 | t.Run("test 404 route", func(t *testing.T) { 54 | m := &MockStorage{} 55 | cfg := createMockConfig() 56 | s := New(context.TODO(), cfg.ServerConfig, m) 57 | defer s.ShutDown() 58 | err := s.Run() 59 | require.NoError(t, err) 60 | 61 | apiRequest := fmt.Sprintf("http://%s/api/db", s.Addr().String()) 62 | client := &http.Client{} 63 | 64 | resp, err := client.Get(apiRequest + "/test") 65 | require.NoError(t, err) 66 | require.Equal(t, http.StatusNotFound, resp.StatusCode) 67 | 68 | resp, err = client.Post(apiRequest+"/test", "", nil) 69 | require.NoError(t, err) 70 | require.Equal(t, http.StatusNotFound, resp.StatusCode) 71 | }) 72 | 73 | t.Run("test execute abd query requests", func(t *testing.T) { 74 | m := &MockStorage{} 75 | cfg := createMockConfig() 76 | s := New(context.TODO(), cfg.ServerConfig, m) 77 | defer s.ShutDown() 78 | err := s.Run() 79 | require.NoError(t, err) 80 | 81 | host := fmt.Sprintf("http://%s/api/db", s.Addr().String()) 82 | client := &http.Client{} 83 | 84 | resp, err := client.Get(host + "/execute") 85 | require.NoError(t, err) 86 | require.Equal(t, http.StatusNotFound, resp.StatusCode) 87 | 88 | testCases := []struct { 89 | api string 90 | body QueryRequest 91 | expected string 92 | }{ 93 | { 94 | api: "/execute", 95 | body: QueryRequest{ 96 | Queries: []string{`create table test (id integer not null primary key, name text)`}, 97 | UseTx: false, 98 | IncludeTimings: false, 99 | }, 100 | expected: `{"results":[{"last_insert_id":1,"time":3}]}`, 101 | }, 102 | 103 | { 104 | api: "/query", 105 | body: QueryRequest{ 106 | Queries: []string{`select * from test`}, 107 | UseTx: false, 108 | IncludeTimings: false, 109 | Lvl: shrek.Weak, 110 | }, 111 | expected: `{"results":[{"columns":["id","name"],"types":["integer","text"],"values":[[1,"vasya"]]}]}`, 112 | }, 113 | } 114 | 115 | for _, tc := range testCases { 116 | jsonData, err := json.Marshal(tc.body) 117 | require.NoError(t, err) 118 | reader := bytes.NewReader(jsonData) 119 | resp, err = client.Post(host+tc.api, "application/json", reader) 120 | require.NoError(t, err) 121 | defer resp.Body.Close() 122 | 123 | require.Equal(t, http.StatusOK, resp.StatusCode) 124 | 125 | body, err := io.ReadAll(resp.Body) 126 | require.NoError(t, err) 127 | require.Equal(t, tc.expected, string(body)) 128 | } 129 | }) 130 | 131 | t.Run("test set join node", func(t *testing.T) { 132 | mockSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 133 | if r.Method != http.MethodPost { 134 | t.Fatalf("invalid method name: %s", r.Method) 135 | } 136 | w.WriteHeader(http.StatusOK) 137 | })) 138 | defer mockSrv.Close() 139 | 140 | addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9090") 141 | resp, err := Join([]string{mockSrv.URL}, "node1", addr, nil) 142 | require.NoError(t, err) 143 | require.Equal(t, resp, mockSrv.URL+"/api/db/join") 144 | }) 145 | 146 | t.Run("test set join node and parse meta", func(t *testing.T) { 147 | var body map[string]interface{} 148 | mockSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 149 | if r.Method != http.MethodPost { 150 | t.Fatalf("invalid method name: %s", r.Method) 151 | } 152 | w.WriteHeader(http.StatusOK) 153 | 154 | b, err := io.ReadAll(r.Body) 155 | if err != nil { 156 | w.WriteHeader(http.StatusBadRequest) 157 | return 158 | } 159 | 160 | if err := json.Unmarshal(b, &body); err != nil { 161 | w.WriteHeader(http.StatusBadRequest) 162 | return 163 | } 164 | })) 165 | defer mockSrv.Close() 166 | 167 | addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9090") 168 | meta := map[string]string{"test": "test"} 169 | 170 | resp, err := Join([]string{mockSrv.URL}, "node1", addr, meta) 171 | require.NoError(t, err) 172 | require.Equal(t, mockSrv.URL+"/api/db/join", resp) 173 | 174 | val, ok := body["id"] 175 | require.True(t, ok) 176 | require.Equal(t, "node1", val) 177 | 178 | val, ok = body["addr"] 179 | require.True(t, ok) 180 | require.Equal(t, addr.String(), val) 181 | 182 | val, ok = body["meta"] 183 | require.True(t, ok) 184 | 185 | val1, _ := json.Marshal(val) 186 | val2, _ := json.Marshal(meta) 187 | require.Equal(t, string(val1), string(val2)) 188 | }) 189 | 190 | t.Run("test set join node failed", func(t *testing.T) { 191 | mockSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 192 | w.WriteHeader(http.StatusBadRequest) 193 | })) 194 | defer mockSrv.Close() 195 | 196 | addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9090") 197 | _, err := Join([]string{mockSrv.URL}, "node1", addr, nil) 198 | require.Error(t, err) 199 | }) 200 | 201 | t.Run("test multi set join first node", func(t *testing.T) { 202 | mockSrv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 203 | w.WriteHeader(http.StatusOK) 204 | })) 205 | defer mockSrv1.Close() 206 | 207 | mockSrv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 208 | w.WriteHeader(http.StatusOK) 209 | })) 210 | defer mockSrv2.Close() 211 | 212 | addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9090") 213 | resp, err := Join([]string{mockSrv1.URL, mockSrv2.URL}, "node1", addr, nil) 214 | require.NoError(t, err) 215 | require.Equal(t, mockSrv1.URL+"/api/db/join", resp) 216 | }) 217 | 218 | t.Run("test multi set join second node", func(t *testing.T) { 219 | mockSrv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 220 | w.WriteHeader(http.StatusBadRequest) 221 | })) 222 | defer mockSrv1.Close() 223 | 224 | mockSrv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 225 | w.WriteHeader(http.StatusOK) 226 | })) 227 | defer mockSrv2.Close() 228 | 229 | addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:9090") 230 | resp, err := Join([]string{mockSrv1.URL, mockSrv2.URL}, "node1", addr, nil) 231 | require.NoError(t, err) 232 | require.Equal(t, mockSrv2.URL+"/api/db/join", resp) 233 | }) 234 | 235 | t.Run("test multi set join second node redirect", func(t *testing.T) { 236 | mockSrv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 237 | w.WriteHeader(http.StatusOK) 238 | })) 239 | defer mockSrv1.Close() 240 | redirectAddr := fmt.Sprintf("%s%s", mockSrv1.URL, "/api/db/join") 241 | 242 | mockSrv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 243 | http.Redirect(w, r, redirectAddr, http.StatusMovedPermanently) 244 | })) 245 | defer mockSrv2.Close() 246 | 247 | addr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:0") 248 | resp, err := Join([]string{mockSrv2.URL}, "node2", addr, nil) 249 | require.NoError(t, err) 250 | require.Equal(t, redirectAddr, resp) 251 | }) 252 | } 253 | 254 | type MockStorage struct { 255 | } 256 | 257 | func (s *MockStorage) LeaderID() (string, error) { 258 | return "", nil 259 | } 260 | 261 | func (s *MockStorage) Stats() (map[string]interface{}, error) { 262 | return nil, nil //nolint:nilnil //Ignore it 263 | } 264 | 265 | func (s *MockStorage) GetMetadata(_, _ string) string { 266 | return "" 267 | } 268 | 269 | func (s *MockStorage) Execute(r *shrek.ExecuteRequest) ([]*sql.Result, error) { 270 | if !strings.HasPrefix(r.Queries[0], "create") { 271 | return nil, errors.New("wrong create SQL") 272 | } 273 | resp := &sql.Result{ 274 | LastInsertID: 1, 275 | RowsAffected: 0, 276 | Error: "", 277 | Time: float64(3), 278 | } 279 | return []*sql.Result{resp}, nil 280 | } 281 | 282 | func (s *MockStorage) Query(r *shrek.QueryRequest) ([]*sql.Rows, error) { 283 | if !strings.HasPrefix(r.Queries[0], "select") { 284 | return nil, errors.New("wrong select SQL") 285 | } 286 | 287 | resp := &sql.Rows{ 288 | Columns: []string{"id", "name"}, 289 | Types: []string{"integer", "text"}, 290 | Values: [][]interface{}{[]interface{}{1, "vasya"}}, 291 | Error: "", 292 | Time: 0, 293 | } 294 | 295 | return []*sql.Rows{resp}, nil 296 | } 297 | 298 | func (s *MockStorage) Join(_, _ string, _ map[string]string) error { 299 | return nil 300 | } 301 | 302 | func (s *MockStorage) Remove(_ string) error { 303 | return nil 304 | } 305 | 306 | func (s *MockStorage) Leader() string { 307 | return "" 308 | } 309 | 310 | func tempDir() string { 311 | path, err := os.MkdirTemp("", "shrek-test-") 312 | if err != nil { 313 | panic("failed to create temp dir") 314 | } 315 | return path 316 | } 317 | 318 | func createMockConfig() *config.Config { 319 | dir := tempDir() 320 | defer os.RemoveAll(dir) 321 | 322 | httpAddr, _ := utils.GetTCPAddr("localhost:4001") 323 | raftAddr, _ := utils.GetTCPAddr("localhost:4002") 324 | raftHeartbeatTimeout, _ := time.ParseDuration("1s") 325 | raftElectionTimeout, _ := time.ParseDuration("1s") 326 | raftOpenTimeout, _ := time.ParseDuration("120s") 327 | raftApplyTimeout, _ := time.ParseDuration("10s") 328 | 329 | raftID := utils.RandomString(5) 330 | 331 | return &config.Config{ 332 | Environment: core.Local, 333 | ServerConfig: &config.ServerConfig{ 334 | HTTPAddr: httpAddr, 335 | }, 336 | StorageConfig: &config.StorageConfig{ 337 | RaftID: raftID, 338 | RaftDir: filepath.Join(dir, raftID), 339 | RaftAddr: raftAddr, 340 | RaftHeartbeatTimeout: raftHeartbeatTimeout, 341 | RaftElectionTimeout: raftElectionTimeout, 342 | RaftApplyTimeout: raftApplyTimeout, 343 | RaftOpenTimeout: raftOpenTimeout, 344 | RaftSnapThreshold: uint64(8192), 345 | RaftShutdownOnRemove: false, 346 | DBCfg: &config.DBConfig{ 347 | DBFilename: filepath.Join(dir, "db.sqlite"), 348 | InMemory: false, 349 | DSN: "", 350 | }, 351 | }, 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /internal/shrek/shrek.go: -------------------------------------------------------------------------------- 1 | package shrek 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/draculaas/shrek/internal/config" 10 | sql "github.com/draculaas/shrek/internal/db" 11 | "github.com/draculaas/shrek/internal/logging" 12 | "github.com/draculaas/shrek/internal/utils" 13 | "github.com/hashicorp/raft" 14 | raftboltdb "github.com/hashicorp/raft-boltdb" 15 | "go.uber.org/zap" 16 | "io" 17 | "os" 18 | "path/filepath" 19 | "strconv" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | type storagePayload struct { 25 | Queries []string `json:"queries,omitempty"` 26 | UseTx bool `json:"useTx,omitempty"` 27 | IncludeTimings bool `json:"includeTimings,omitempty"` 28 | } 29 | 30 | type payload struct { 31 | Typ payloadType `json:"typ,omitempty"` 32 | Sub json.RawMessage `json:"sub,omitempty"` 33 | } 34 | 35 | type metadataPayload struct { 36 | RaftID string `json:"raft_id,omitempty"` 37 | Data map[string]string `json:"data,omitempty"` 38 | } 39 | 40 | var ( 41 | // ErrNotLeader is returned when a node attempts to execute a leader-only 42 | // operation. 43 | ErrNotLeader = errors.New("not leader") 44 | 45 | // ErrOpenTimeout is returned when the Shrek does not apply its initial 46 | // logs within the specified time. 47 | ErrOpenTimeout = errors.New("timeout waiting for initial logs application") 48 | 49 | // ErrInvalidBackupFormat is returned when the requested backup format 50 | // is not valid. 51 | ErrInvalidBackupFormat = errors.New("invalid backup format") 52 | ) 53 | 54 | const ( 55 | retainSnapshotCount = 2 56 | ) 57 | 58 | // Shrek - SQLite database, supporting Raft as consensus protocol 59 | type Shrek struct { 60 | // raft 61 | raft *raft.Raft 62 | 63 | raftLayer Listener 64 | raftTransport *raft.NetworkTransport 65 | 66 | // physical store 67 | boltStore *raftboltdb.BoltStore 68 | // kvs persistent store 69 | raftStable raft.StableStore 70 | 71 | // log storage 72 | raftLog raft.LogStore 73 | 74 | raftDir string 75 | raftID string 76 | 77 | mu sync.RWMutex 78 | 79 | ctx context.Context 80 | 81 | // SQLite store 82 | db *sql.DB 83 | // SQLite database config 84 | dbConf *config.DBConfig 85 | 86 | meta map[string]map[string]string 87 | metaMu sync.RWMutex 88 | 89 | ShutdownOnRemove bool 90 | SnapshotThreshold uint64 91 | SnapshotInterval time.Duration 92 | HeartbeatTimeout time.Duration 93 | ElectionTimeout time.Duration 94 | ApplyTimeout time.Duration 95 | OpenTimeout time.Duration 96 | 97 | logger *zap.Logger 98 | 99 | shutdown bool 100 | shutdownLock sync.Mutex 101 | } 102 | 103 | // Apply ... Applies a Raft log entry to the database 104 | func (s *Shrek) Apply(l *raft.Log) interface{} { 105 | var c payload 106 | if err := json.Unmarshal(l.Data, &c); err != nil { 107 | panic(fmt.Sprintf("failed to unmarshal raft command: %s", err.Error())) 108 | } 109 | 110 | switch c.Typ { 111 | case Execute, Query: 112 | var d storagePayload 113 | if err := json.Unmarshal(c.Sub, &d); err != nil { 114 | return &fsmGenericResponse{error: err} 115 | } 116 | if c.Typ == Execute { 117 | r, err := s.db.Execute(d.Queries, d.UseTx, d.IncludeTimings) 118 | return &fsmExecuteResponse{results: r, error: err} 119 | } 120 | r, err := s.db.Query(d.Queries, d.UseTx, d.IncludeTimings) 121 | return &fsmQueryResponse{rows: r, error: err} 122 | case Peer: 123 | var data metadataPayload 124 | if err := json.Unmarshal(c.Sub, &data); err != nil { 125 | return &fsmGenericResponse{error: err} 126 | } 127 | func() { 128 | s.metaMu.Lock() 129 | defer s.metaMu.Unlock() 130 | if _, ok := s.meta[data.RaftID]; !ok { 131 | s.meta[data.RaftID] = make(map[string]string) 132 | } 133 | for k, v := range data.Data { 134 | s.meta[data.RaftID][k] = v 135 | } 136 | }() 137 | return &fsmGenericResponse{} 138 | default: 139 | return &fsmGenericResponse{error: fmt.Errorf("unknown command: %v", c.Typ)} 140 | } 141 | } 142 | 143 | func (s *Shrek) Database(leader bool) ([]byte, error) { 144 | if leader && s.raft.State() != raft.Leader { 145 | return nil, errors.New("leader is missing") 146 | } 147 | 148 | s.mu.Lock() 149 | defer s.mu.Unlock() 150 | 151 | f, err := os.CreateTemp("", "shrek-snap-") 152 | if err != nil { 153 | return nil, err 154 | } 155 | _ = f.Close() 156 | defer func(name string) { 157 | _ = os.Remove(name) 158 | }(f.Name()) 159 | 160 | if err := s.db.Backup(f.Name()); err != nil { 161 | return nil, err 162 | } 163 | 164 | return os.ReadFile(f.Name()) 165 | } 166 | 167 | func (s *Shrek) Snapshot() (raft.FSMSnapshot, error) { 168 | fsm := &fsmSnapshot{} 169 | logger := logging.WithContext(s.ctx) 170 | var err error 171 | fsm.database, err = s.Database(false) 172 | if err != nil { 173 | logger.Error("error connecting to database", zap.Error(err)) 174 | return nil, err 175 | } 176 | 177 | fsm.meta, err = json.Marshal(s.meta) 178 | if err != nil { 179 | logger.Error("error encode metadata for snapshot", zap.Error(err)) 180 | return nil, err 181 | } 182 | 183 | return fsm, nil 184 | } 185 | 186 | // Restore ... Restores the node to a previous state 187 | func (s *Shrek) Restore(snapshot io.ReadCloser) error { 188 | if err := s.db.Close(); err != nil { 189 | return err 190 | } 191 | 192 | // get size of the database 193 | var size uint64 194 | if err := binary.Read(snapshot, binary.LittleEndian, &size); err != nil { 195 | return err 196 | } 197 | 198 | // read in the database file data and restore 199 | database := make([]byte, size) 200 | if _, err := io.ReadFull(snapshot, database); err != nil { 201 | return err 202 | } 203 | 204 | var db *sql.DB 205 | var err error 206 | 207 | if err := os.WriteFile(s.dbConf.DBFilename, database, 0600); err != nil { 208 | return err 209 | } 210 | 211 | db, err = sql.OpenWithDSN(s.dbConf.DBFilename, s.dbConf.DSN) 212 | if err != nil { 213 | return err 214 | } 215 | 216 | s.db = db 217 | 218 | // Read remaining bytes, and set to cluster meta. 219 | b, err := io.ReadAll(snapshot) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | s.metaMu.Lock() 225 | defer s.metaMu.Unlock() 226 | err = json.Unmarshal(b, &s.meta) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | return nil 232 | } 233 | 234 | // GetMetadata ... Returns metadata for specific node_id and for a given key 235 | func (s *Shrek) GetMetadata(id, key string) string { 236 | s.metaMu.RLock() 237 | defer s.metaMu.RUnlock() 238 | 239 | if _, ok := s.meta[id]; !ok { 240 | return "" 241 | } 242 | v, ok := s.meta[id][key] 243 | if !ok { 244 | return "" 245 | } 246 | return v 247 | } 248 | 249 | func (s *Shrek) SetMetadata(md map[string]string) error { 250 | return s.setMetadata(s.raftID, md) 251 | } 252 | 253 | func (s *Shrek) setMetadata(id string, md map[string]string) error { 254 | if func() bool { 255 | s.metaMu.RLock() 256 | defer s.metaMu.RUnlock() 257 | 258 | if _, ok := s.meta[id]; ok { 259 | for k, v := range md { 260 | if s.meta[id][k] != v { 261 | return false 262 | } 263 | } 264 | return true 265 | } 266 | return false 267 | }() { 268 | return nil 269 | } 270 | 271 | p, err := json.Marshal(metadataPayload{ 272 | RaftID: id, 273 | Data: md, 274 | }) 275 | if err != nil { 276 | return err 277 | } 278 | 279 | b, err := json.Marshal(payload{Typ: Peer, Sub: p}) 280 | if err != nil { 281 | return err 282 | } 283 | 284 | f := s.raft.Apply(b, s.ApplyTimeout) 285 | if e, ok := f.(raft.Future); ok && e.Error() != nil { 286 | if errors.Is(e.Error(), raft.ErrNotLeader) { 287 | return ErrNotLeader 288 | } 289 | err := e.Error() 290 | if err != nil { 291 | return err 292 | } 293 | } 294 | 295 | return nil 296 | } 297 | 298 | func (s *Shrek) Execute(r *ExecuteRequest) ([]*sql.Result, error) { 299 | if s.raft.State() != raft.Leader { 300 | return nil, ErrNotLeader 301 | } 302 | 303 | body, err := json.Marshal(&storagePayload{ 304 | Queries: r.Queries, 305 | UseTx: r.UseTx, 306 | IncludeTimings: r.IncludeTimings, 307 | }) 308 | if err != nil { 309 | return nil, err 310 | } 311 | 312 | b, err := json.Marshal(&payload{ 313 | Typ: Execute, 314 | Sub: body, 315 | }) 316 | if err != nil { 317 | return nil, err 318 | } 319 | 320 | f := s.raft.Apply(b, s.ApplyTimeout) 321 | if e, ok := f.(raft.Future); ok && e.Error() != nil { 322 | if errors.Is(e.Error(), raft.ErrNotLeader) { 323 | return nil, ErrNotLeader 324 | } 325 | return nil, e.Error() 326 | } 327 | 328 | res, ok := f.Response().(*fsmExecuteResponse) 329 | if !ok { 330 | return nil, errors.New("failed response type") 331 | } 332 | return res.results, res.error 333 | } 334 | 335 | func (s *Shrek) Query(r *QueryRequest) ([]*sql.Rows, error) { 336 | s.mu.RLock() 337 | defer s.mu.RUnlock() 338 | 339 | if r.Lvl == Strong { //nolint:nestif //todo 340 | body, err := json.Marshal(&storagePayload{ 341 | UseTx: r.UseTx, 342 | Queries: r.Queries, 343 | IncludeTimings: r.IncludeTimings, 344 | }) 345 | if err != nil { 346 | return nil, err 347 | } 348 | b, err := json.Marshal(&payload{ 349 | Typ: Query, 350 | Sub: body, 351 | }) 352 | if err != nil { 353 | return nil, err 354 | } 355 | f := s.raft.Apply(b, s.ApplyTimeout) 356 | if e, ok := f.(raft.Future); ok && e.Error() != nil { 357 | if errors.Is(e.Error(), raft.ErrNotLeader) { 358 | return nil, ErrNotLeader 359 | } 360 | return nil, e.Error() 361 | } 362 | 363 | res, ok := f.Response().(*fsmQueryResponse) 364 | if !ok { 365 | return nil, errors.New("invalid response") 366 | } 367 | return res.rows, res.error 368 | } 369 | 370 | if r.Lvl == Weak && s.raft.State() != raft.Leader { 371 | return nil, ErrNotLeader 372 | } 373 | 374 | return s.db.Query(r.Queries, r.UseTx, r.IncludeTimings) 375 | } 376 | 377 | func (s *Shrek) Join(id, addr string, metadata map[string]string) error { 378 | s.logger.Info("Received a request to join node", zap.String("addr", addr)) 379 | 380 | if s.raft.State() != raft.Leader { 381 | return ErrNotLeader 382 | } 383 | 384 | f := s.raft.AddVoter(raft.ServerID(id), raft.ServerAddress(addr), 0, 0) 385 | 386 | if e, ok := f.(raft.Future); ok && e.Error() != nil { 387 | if errors.Is(e.Error(), raft.ErrNotLeader) { 388 | return ErrNotLeader 389 | } 390 | return e.Error() 391 | } 392 | 393 | if err := s.setMetadata(id, metadata); err != nil { 394 | return err 395 | } 396 | 397 | s.logger.Info("Successfully joined the node", zap.String("addr", addr)) 398 | 399 | return nil 400 | } 401 | 402 | // Remove ... Removes a node from the store 403 | func (s *Shrek) Remove(addr string) error { 404 | s.logger.Info("Received a request to join node", zap.String("addr", addr)) 405 | 406 | if s.raft.State() != raft.Leader { 407 | return ErrNotLeader 408 | } 409 | 410 | f := s.raft.RemoveServer(raft.ServerID(addr), 0, 0) 411 | if f.Error() != nil { 412 | if errors.Is(f.Error(), raft.ErrNotLeader) { 413 | return ErrNotLeader 414 | } 415 | return f.Error() 416 | } 417 | 418 | s.logger.Info("Successfully removed the node", zap.String("addr", addr)) 419 | 420 | return nil 421 | } 422 | 423 | func (s *Shrek) Leader() string { 424 | // TODO implement me 425 | panic("implement me") 426 | } 427 | 428 | func (s *Shrek) Stats() (map[string]interface{}, error) { 429 | dbInfo := map[string]interface{}{ 430 | "dsn": s.dbConf.DSN, 431 | } 432 | 433 | if !s.dbConf.InMemory { 434 | dbInfo["path"] = s.dbConf.DBFilename 435 | stat, err := os.Stat(s.dbConf.DBFilename) 436 | if err != nil { 437 | return nil, err 438 | } 439 | dbInfo["size"] = stat.Size() 440 | } else { 441 | dbInfo["path"] = "in memory" 442 | } 443 | 444 | leaderID, err := s.LeaderID() 445 | if err != nil { 446 | return nil, err 447 | } 448 | 449 | raftLeaderAddr, _ := s.raft.LeaderWithID() 450 | 451 | stats := map[string]interface{}{ 452 | "node_id": s.raftID, 453 | "raft": s.raft.Stats(), 454 | "raft_dir": s.raftDir, 455 | "addr": s.Addr(), 456 | "leader": map[string]string{ 457 | "node_id": leaderID, 458 | "addr": string(raftLeaderAddr), 459 | }, 460 | "apply_timeout": s.ApplyTimeout.String(), 461 | "heartbeat_timeout": s.HeartbeatTimeout.String(), 462 | "election_timeout": s.ElectionTimeout.String(), 463 | "snapshot_threshold": s.SnapshotThreshold, 464 | "db_info": dbInfo, 465 | } 466 | 467 | return stats, nil 468 | } 469 | 470 | type QueryRequest struct { 471 | Queries []string 472 | IncludeTimings bool 473 | UseTx bool 474 | Lvl ConsistencyLevel 475 | } 476 | 477 | type ExecuteRequest struct { 478 | Queries []string // slice of queries 479 | UseTx bool // either all queries will be executed successfully or it will as though none executed 480 | IncludeTimings bool // timing information 481 | } 482 | 483 | // New ... Initializer 484 | func New(ctx context.Context, cfg *config.Config, raftLayer Listener) *Shrek { 485 | logger := logging.WithContext(ctx) 486 | 487 | return &Shrek{ 488 | ctx: ctx, 489 | logger: logger, 490 | raftLayer: raftLayer, 491 | dbConf: cfg.StorageConfig.DBCfg, 492 | meta: make(map[string]map[string]string), 493 | raftID: cfg.StorageConfig.RaftID, 494 | raftDir: cfg.StorageConfig.RaftDir, 495 | ShutdownOnRemove: cfg.StorageConfig.RaftShutdownOnRemove, 496 | SnapshotThreshold: cfg.StorageConfig.RaftSnapThreshold, 497 | ElectionTimeout: cfg.StorageConfig.RaftElectionTimeout, 498 | HeartbeatTimeout: cfg.StorageConfig.RaftHeartbeatTimeout, 499 | ApplyTimeout: cfg.StorageConfig.RaftApplyTimeout, 500 | OpenTimeout: cfg.StorageConfig.RaftOpenTimeout, 501 | } 502 | } 503 | 504 | func (s *Shrek) Open(allowSingle bool) error { 505 | s.logger.Info("Running Raft", zap.String("raftDir", s.raftDir), zap.String("NODE_ID", s.raftID)) 506 | 507 | if err := os.MkdirAll(s.raftDir, 0750); err != nil { 508 | s.logger.Fatal("Failed to create raft dir") 509 | return err 510 | } 511 | 512 | // open database file or open it in-memory mode 513 | db, err := s.openDB() 514 | if err != nil { 515 | return err 516 | } 517 | 518 | // init db instance 519 | s.db = db 520 | 521 | isNewNode := !utils.PathExists(filepath.Join(s.raftDir, "raft.db")) 522 | 523 | // Create a transport layer 524 | trans := raft.NewNetworkTransport( 525 | NewRaftLayer(s.raftLayer), 526 | 3, 527 | 10*time.Second, 528 | os.Stderr, 529 | ) 530 | 531 | s.raftTransport = trans 532 | 533 | // get raft config for the store 534 | raftCfg := s.getRaftConfig() 535 | raftCfg.LocalID = raft.ServerID(s.raftID) 536 | 537 | /* 538 | Snapshot stores the state to recover and restore data 539 | Example: 540 | If EC2 instance failed and an autoscaling group brought up another instance for the Raft server 541 | Rather than streaming all the data from the Raft leader, the new server would restore 542 | from the snapshot (which you could store in S3 or a similar storage service) 543 | and then get the latest changes from the leader 544 | */ 545 | snapshots, err := raft.NewFileSnapshotStore( 546 | s.raftDir, 547 | retainSnapshotCount, 548 | os.Stderr, 549 | ) 550 | if err != nil { 551 | s.logger.Fatal("Error create file snapshot store", zap.Error(err)) 552 | return err 553 | } 554 | 555 | // create the log store and stable store 556 | s.boltStore, err = raftboltdb.NewBoltStore(filepath.Join(s.raftDir, "raft.db")) 557 | if err != nil { 558 | s.logger.Fatal("Error create new bolt store", zap.Error(err)) 559 | return err 560 | } 561 | s.raftStable = s.boltStore 562 | s.raftLog, err = raft.NewLogCache(512, s.boltStore) 563 | if err != nil { 564 | return err 565 | } 566 | 567 | // create the Raft instance and bootstrap the cluster 568 | rf, err := raft.NewRaft( 569 | raftCfg, 570 | s, 571 | s.raftLog, 572 | s.raftStable, 573 | snapshots, 574 | s.raftTransport, 575 | ) 576 | if err != nil { 577 | s.logger.Fatal("Error creating raft system", zap.Error(err)) 578 | return err 579 | } 580 | 581 | if allowSingle && isNewNode { 582 | s.logger.Debug("Running bootstrapping app...") 583 | s.logger.Debug("Opening store for node", zap.String("NODE_ID", s.raftID)) 584 | cfg := raft.Configuration{ 585 | Servers: []raft.Server{ 586 | raft.Server{ 587 | ID: raftCfg.LocalID, 588 | Address: trans.LocalAddr(), 589 | }, 590 | }, 591 | } 592 | rf.BootstrapCluster(cfg) 593 | } else { 594 | s.logger.Debug("Bootstrapping app not needed") 595 | } 596 | 597 | // init raft instance 598 | s.raft = rf 599 | 600 | return nil 601 | } 602 | 603 | func (s *Shrek) openDB() (*sql.DB, error) { 604 | var db *sql.DB 605 | var err error 606 | if s.dbConf.InMemory { 607 | db, err = sql.OpenInMemoryWithDSN(s.dbConf.DSN) 608 | if err != nil { 609 | return nil, err 610 | } 611 | s.logger.Info("Successfully opened the in-memory database", zap.String("db path", s.dbConf.DBFilename)) 612 | } else { 613 | if err := os.Remove(s.dbConf.DBFilename); err != nil && !os.IsNotExist(err) { 614 | return nil, err 615 | } 616 | db, err = sql.OpenWithDSN(s.dbConf.DBFilename, s.dbConf.DSN) 617 | if err != nil { 618 | return nil, err 619 | } 620 | s.logger.Info("Successfully opened the database", zap.String("db path", s.dbConf.DBFilename)) 621 | } 622 | 623 | return db, nil 624 | } 625 | 626 | // LeaderID ... Returns leader ID 627 | func (s *Shrek) LeaderID() (string, error) { 628 | addr, serverID := s.LeaderAddr() 629 | s.logger.Info("Current leader info", zap.String("addr", string(addr)), zap.String("server ID", string(serverID))) 630 | 631 | cfg := s.raft.GetConfiguration() 632 | if err := cfg.Error(); err != nil { 633 | return "", err 634 | } 635 | raftServers := cfg.Configuration().Servers 636 | 637 | for _, server := range raftServers { 638 | if server.Address == addr { 639 | return string(server.ID), nil 640 | } 641 | } 642 | 643 | return "", nil 644 | } 645 | 646 | func (s *Shrek) Shutdown() error { 647 | s.logger.Info("shutting down server") 648 | s.shutdownLock.Lock() 649 | defer s.shutdownLock.Unlock() 650 | 651 | if s.shutdown { 652 | return nil 653 | } 654 | 655 | s.shutdown = true 656 | 657 | if err := s.db.Close(); err != nil { 658 | return err 659 | } 660 | 661 | if s.raft != nil { 662 | _ = s.raftTransport.Close() 663 | _ = s.raftLayer.Close() 664 | future := s.raft.Shutdown() 665 | if err := future.Error(); err != nil { 666 | s.logger.Warn("error shutting down raft", zap.Error(err)) 667 | } 668 | } 669 | 670 | return nil 671 | } 672 | 673 | func (s *Shrek) RegisterObserver(o *raft.Observer) { 674 | s.raft.RegisterObserver(o) 675 | } 676 | 677 | func (s *Shrek) DeregisterObserver(o *raft.Observer) { 678 | s.raft.DeregisterObserver(o) 679 | } 680 | 681 | func (s *Shrek) WaitForLeader(timeout time.Duration) (string, error) { 682 | tick := time.NewTicker(100 * time.Millisecond) 683 | defer tick.Stop() 684 | tmr := time.NewTimer(timeout) 685 | defer tmr.Stop() 686 | 687 | for { 688 | select { 689 | case <-tick.C: 690 | leader, _ := s.LeaderAddr() 691 | if leader != "" { 692 | return string(leader), nil 693 | } 694 | case <-tmr.C: 695 | return "", fmt.Errorf("timeout expired") 696 | } 697 | } 698 | } 699 | 700 | func (s *Shrek) WaitForAppliedIndex(idx uint64, timeout time.Duration) error { 701 | tick := time.NewTicker(100 * time.Millisecond) 702 | defer tick.Stop() 703 | tmr := time.NewTicker(timeout) 704 | defer tmr.Stop() 705 | 706 | for { 707 | select { 708 | case <-tick.C: 709 | if s.raft.AppliedIndex() >= idx { 710 | return nil 711 | } 712 | case <-tmr.C: 713 | return fmt.Errorf("timeout expired") 714 | } 715 | } 716 | } 717 | 718 | func (s *Shrek) WaitForApplied(timeout time.Duration) error { 719 | if timeout == 0 { 720 | return nil 721 | } 722 | s.logger.Info( 723 | "Waiting for the app of all Raft log entries to be completed on the database", 724 | zap.String("timeout", strconv.FormatInt(int64(timeout), 10)), 725 | ) 726 | return s.WaitForAppliedIndex(s.raft.LastIndex(), timeout) 727 | } 728 | 729 | // ID ... Returns the raft ID 730 | func (s *Shrek) ID() string { 731 | return s.raftID 732 | } 733 | 734 | // Addr ... Returns the address of the store 735 | func (s *Shrek) Addr() string { 736 | return string(s.raftTransport.LocalAddr()) 737 | } 738 | 739 | func (s *Shrek) LeaderAddr() (raft.ServerAddress, raft.ServerID) { 740 | return s.raft.LeaderWithID() 741 | } 742 | 743 | // Path ... Returns the path to the store's storage directory 744 | func (s *Shrek) Path() string { 745 | return s.raftDir 746 | } 747 | 748 | // getRaftConfig ... Return raft config file 749 | func (s *Shrek) getRaftConfig() *raft.Config { 750 | cfg := raft.DefaultConfig() 751 | cfg.ShutdownOnRemove = s.ShutdownOnRemove 752 | if s.SnapshotThreshold != 0 { 753 | cfg.SnapshotThreshold = s.SnapshotThreshold 754 | } 755 | if s.SnapshotInterval != 0 { 756 | cfg.SnapshotInterval = s.SnapshotInterval 757 | } 758 | if s.HeartbeatTimeout != 0 { 759 | cfg.HeartbeatTimeout = s.HeartbeatTimeout 760 | } 761 | if s.ElectionTimeout != 0 { 762 | cfg.ElectionTimeout = s.ElectionTimeout 763 | } 764 | return cfg 765 | } 766 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 2 | github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 3 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= 9 | github.com/armon/go-metrics v0.3.8/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= 10 | github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= 11 | github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 13 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 16 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 17 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 18 | github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= 19 | github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk= 20 | github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= 21 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 22 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 23 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 24 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= 25 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= 26 | github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= 27 | github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= 28 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 29 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 30 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 31 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 32 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 33 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 35 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 37 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 38 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 39 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 40 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 41 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 42 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 43 | github.com/gin-contrib/zap v0.2.0 h1:HLvt3rZXyC8XC+s2lHzMFow3UDqiEbfrBWJyHHS6L8A= 44 | github.com/gin-contrib/zap v0.2.0/go.mod h1:eqfbe9ZmI+GgTZF6nRiC2ZwDeM4DK1Viwc8OxTCphh0= 45 | github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 h1:dyuNlYlG1faymw39NdJddnzJICy6587tiGSVioWhYoE= 46 | github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= 47 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 48 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 49 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 50 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 51 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 52 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 53 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 54 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 55 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 56 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 57 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 58 | github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo= 59 | github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 60 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 61 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 62 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 63 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 64 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 65 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 68 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 69 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 70 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 71 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 72 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 73 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 74 | github.com/hashicorp/go-hclog v0.9.1/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 75 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 76 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 77 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 78 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 79 | github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI= 80 | github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 81 | github.com/hashicorp/go-msgpack/v2 v2.1.1 h1:xQEY9yB2wnHitoSzk/B9UjXWRQ67QKu5AOm8aFp8N3I= 82 | github.com/hashicorp/go-msgpack/v2 v2.1.1/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4= 83 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 84 | github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= 85 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 86 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 87 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 88 | github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM= 89 | github.com/hashicorp/raft v1.6.0 h1:tkIAORZy2GbJ2Trp5eUSggLXDPOJLXC+JJLNMMqtgtM= 90 | github.com/hashicorp/raft v1.6.0/go.mod h1:Xil5pDgeGwRWuX4uPUmwa+7Vagg4N804dz6mhNi6S7o= 91 | github.com/hashicorp/raft-boltdb v0.0.0-20231211162105-6c830fa4535e h1:SK4y8oR4ZMHPvwVHryKI88kJPJda4UyWYvG5A6iEQxc= 92 | github.com/hashicorp/raft-boltdb v0.0.0-20231211162105-6c830fa4535e/go.mod h1:EMz/UIuG93P0MBeHh6CbXQAEe8ckVJLZjhD17lBzK5Q= 93 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 94 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 95 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 96 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 97 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 98 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 99 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 100 | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= 101 | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 102 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 103 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 104 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 105 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 106 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 107 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 108 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 109 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 110 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 111 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 112 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 113 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 114 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 115 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 116 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 117 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 118 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 119 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 120 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 121 | github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= 122 | github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 123 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 124 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 125 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 126 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 127 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 128 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 129 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 130 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 131 | github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= 132 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 133 | github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 134 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= 135 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= 136 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 137 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 138 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 139 | github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 140 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 141 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 142 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 143 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 144 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 145 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 146 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 147 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 148 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 149 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 150 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 151 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 152 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 153 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 154 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 155 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 156 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 157 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 158 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 159 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 160 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 161 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 162 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 163 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 164 | github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989 h1:rq2/kILQnPtq5oL4+IAjgVOjh5e2yj2aaCYi7squEvI= 165 | github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+yDFh9SZXUTvspXTjbFXgZGP/UvhU1S65A4A= 166 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 167 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 168 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 169 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 170 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 171 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 172 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 173 | github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 174 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 175 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 176 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 177 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 178 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 179 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 180 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 181 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 182 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 183 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 184 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 185 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 186 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 187 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 188 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 189 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 190 | github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= 191 | github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= 192 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 193 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 194 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 195 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 196 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= 197 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= 198 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 199 | golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= 200 | golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 201 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 202 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 203 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 204 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 205 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 206 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 207 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 208 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 209 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 210 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 212 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 213 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 214 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 215 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 216 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 217 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 218 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 219 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 220 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 221 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 223 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 224 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 225 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 226 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 227 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 228 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 235 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 236 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 237 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 238 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 239 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 240 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 241 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 242 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 243 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 244 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 245 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 246 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 247 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 248 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 249 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 250 | golang.org/x/tools v0.0.0-20200331202046-9d5940d49312/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 251 | golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= 252 | golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 253 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 254 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 255 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 256 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 257 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 258 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 259 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 260 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 261 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 262 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 263 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 264 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 265 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 266 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 267 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 268 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 269 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 270 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 271 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 272 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 273 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 274 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 275 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 276 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 277 | --------------------------------------------------------------------------------