🧱 Introduce etcd for high availability (HA)
This commit is contained in:
parent
f6ff7178b9
commit
85783aa331
@ -18,5 +18,5 @@ type Command struct {
|
||||
// The implementation of the command, the handler is the service that will be invoked
|
||||
Handler []*ServiceInstance `json:"handler"`
|
||||
|
||||
robinIndex uint
|
||||
RobinIndex uint `json:"robin_index"`
|
||||
}
|
||||
|
@ -1,67 +1,79 @@
|
||||
package directory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/internal/kv"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// In commands, we use the map and the mutex because it is usually read and only sometimes write
|
||||
var commandDirectory = make(map[string]*Command)
|
||||
var commandDirectoryMutex sync.Mutex
|
||||
|
||||
func AddCommand(id, method string, tags []string, handler *ServiceInstance) {
|
||||
commandDirectoryMutex.Lock()
|
||||
defer commandDirectoryMutex.Unlock()
|
||||
const CommandInfoKvPrefix = "nexus.command/"
|
||||
|
||||
func AddCommand(id, method string, tags []string, handler *ServiceInstance) error {
|
||||
if tags == nil {
|
||||
tags = make([]string, 0)
|
||||
}
|
||||
|
||||
ky := nex.GetCommandKey(id, method)
|
||||
if _, ok := commandDirectory[ky]; !ok {
|
||||
commandDirectory[ky] = &Command{
|
||||
ID: id,
|
||||
Method: method,
|
||||
Tags: tags,
|
||||
Handler: []*ServiceInstance{handler},
|
||||
}
|
||||
} else {
|
||||
commandDirectory[ky].Handler = append(commandDirectory[ky].Handler, handler)
|
||||
commandDirectory[ky].Tags = lo.Uniq(append(commandDirectory[ky].Tags, tags...))
|
||||
ky := CommandInfoKvPrefix + nex.GetCommandKey(id, method)
|
||||
|
||||
command := &Command{
|
||||
ID: id,
|
||||
Method: method,
|
||||
Tags: tags,
|
||||
Handler: []*ServiceInstance{handler},
|
||||
}
|
||||
|
||||
commandDirectory[ky].Handler = lo.UniqBy(commandDirectory[ky].Handler, func(item *ServiceInstance) string {
|
||||
command.Handler = lo.UniqBy(command.Handler, func(item *ServiceInstance) string {
|
||||
return item.ID
|
||||
})
|
||||
|
||||
log.Info().Str("id", id).Str("method", method).Str("tags", strings.Join(tags, ",")).Msg("New command registered")
|
||||
commandJSON, err := json.Marshal(command)
|
||||
if err != nil {
|
||||
log.Printf("Error marshaling command: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = kv.Kv.Put(context.Background(), ky, string(commandJSON))
|
||||
return err
|
||||
}
|
||||
|
||||
func GetCommandHandler(id, method string) *ServiceInstance {
|
||||
commandDirectoryMutex.Lock()
|
||||
defer commandDirectoryMutex.Unlock()
|
||||
ky := CommandInfoKvPrefix + nex.GetCommandKey(id, method)
|
||||
|
||||
ky := nex.GetCommandKey(id, method)
|
||||
if val, ok := commandDirectory[ky]; ok {
|
||||
if len(val.Handler) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := val.robinIndex % uint(len(val.Handler))
|
||||
val.robinIndex = idx + 1
|
||||
return val.Handler[idx]
|
||||
resp, err := kv.Kv.Get(context.Background(), ky)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
if len(resp.Kvs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var command Command
|
||||
if err := json.Unmarshal(resp.Kvs[0].Value, &command); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(command.Handler) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := command.RobinIndex % uint(len(command.Handler))
|
||||
command.RobinIndex = idx + 1
|
||||
|
||||
raw, err := json.Marshal(&command)
|
||||
if err == nil {
|
||||
_, _ = kv.Kv.Put(context.Background(), ky, string(raw))
|
||||
}
|
||||
|
||||
return command.Handler[idx]
|
||||
}
|
||||
|
||||
func RemoveCommand(id, method string) {
|
||||
commandDirectoryMutex.Lock()
|
||||
defer commandDirectoryMutex.Unlock()
|
||||
func RemoveCommand(id, method string) error {
|
||||
ky := CommandInfoKvPrefix + nex.GetCommandKey(id, method)
|
||||
|
||||
ky := nex.GetCommandKey(id, method)
|
||||
delete(commandDirectory, ky)
|
||||
_, err := kv.Kv.Delete(context.Background(), ky)
|
||||
return err
|
||||
}
|
||||
|
@ -2,77 +2,137 @@ package directory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/internal/kv"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
"sync"
|
||||
"github.com/goccy/go-json"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// In services, we use sync.Map because it will be both often read and write
|
||||
var serviceDirectory sync.Map
|
||||
const ServiceInfoKvPrefix = "nexus.service/"
|
||||
|
||||
func GetServiceInstance(id string) *ServiceInstance {
|
||||
val, ok := serviceDirectory.Load(id)
|
||||
if ok {
|
||||
return val.(*ServiceInstance)
|
||||
} else {
|
||||
return nil
|
||||
func AddServiceInstance(in *ServiceInstance) error {
|
||||
key := ServiceInfoKvPrefix + in.ID
|
||||
data, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = kv.Kv.Put(context.Background(), key, string(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func GetServiceInstanceByType(t string) *ServiceInstance {
|
||||
var result *ServiceInstance
|
||||
serviceDirectory.Range(func(key, value any) bool {
|
||||
if value.(*ServiceInstance).Type == t {
|
||||
result = value.(*ServiceInstance)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return result
|
||||
func GetServiceInstance(id string) *ServiceInstance {
|
||||
key := ServiceInfoKvPrefix + id
|
||||
resp, err := kv.Kv.Get(context.Background(), key)
|
||||
if err != nil || len(resp.Kvs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var instance ServiceInstance
|
||||
err = json.Unmarshal(resp.Kvs[0].Value, &instance)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &instance
|
||||
}
|
||||
|
||||
func ListServiceInstance() []*ServiceInstance {
|
||||
resp, err := kv.Kv.Get(context.Background(), ServiceInfoKvPrefix, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []*ServiceInstance
|
||||
serviceDirectory.Range(func(key, value interface{}) bool {
|
||||
result = append(result, value.(*ServiceInstance))
|
||||
return true
|
||||
})
|
||||
for _, val := range resp.Kvs {
|
||||
var instance ServiceInstance
|
||||
if err := json.Unmarshal(val.Value, &instance); err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, &instance)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ListServiceInstanceByType(t string) []*ServiceInstance {
|
||||
resp, err := kv.Kv.Get(context.Background(), ServiceInfoKvPrefix, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result []*ServiceInstance
|
||||
serviceDirectory.Range(func(key, value interface{}) bool {
|
||||
if value.(*ServiceInstance).Type == t {
|
||||
result = append(result, value.(*ServiceInstance))
|
||||
for _, val := range resp.Kvs {
|
||||
var instance ServiceInstance
|
||||
if err := json.Unmarshal(val.Value, &instance); err != nil {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
})
|
||||
if instance.Type == t {
|
||||
result = append(result, &instance)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func AddServiceInstance(in *ServiceInstance) {
|
||||
serviceDirectory.Store(in.ID, in)
|
||||
var srvRng = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
func GetServiceInstanceByType(t string) *ServiceInstance {
|
||||
resp, err := kv.Kv.Get(context.Background(), ServiceInfoKvPrefix, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var instances []*ServiceInstance
|
||||
for _, val := range resp.Kvs {
|
||||
var instance ServiceInstance
|
||||
if err := json.Unmarshal(val.Value, &instance); err != nil {
|
||||
continue
|
||||
}
|
||||
if instance.Type == t {
|
||||
instances = append(instances, &instance)
|
||||
}
|
||||
}
|
||||
|
||||
if len(instances) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
idx := srvRng.Intn(len(instances))
|
||||
return instances[idx]
|
||||
}
|
||||
|
||||
func RemoveServiceInstance(id string) {
|
||||
serviceDirectory.Delete(id)
|
||||
func RemoveServiceInstance(id string) error {
|
||||
key := ServiceInfoKvPrefix + id
|
||||
_, err := kv.Kv.Delete(context.Background(), key)
|
||||
return err
|
||||
}
|
||||
|
||||
func BroadcastEvent(event string, data any) {
|
||||
serviceDirectory.Range(func(key, value any) bool {
|
||||
conn, err := value.(*ServiceInstance).GetGrpcConn()
|
||||
func BroadcastEvent(event string, data any) error {
|
||||
resp, err := kv.Kv.Get(context.Background(), ServiceInfoKvPrefix, clientv3.WithPrefix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, val := range resp.Kvs {
|
||||
var instance ServiceInstance
|
||||
if err := json.Unmarshal(val.Value, &instance); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
conn, err := instance.GetGrpcConn()
|
||||
if err != nil {
|
||||
return true
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_, _ = proto.NewDirectoryServiceClient(conn).BroadcastEvent(ctx, &proto.EventInfo{
|
||||
Event: event,
|
||||
Data: nex.EncodeMap(data),
|
||||
})
|
||||
return true
|
||||
})
|
||||
cancel()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -75,17 +75,25 @@ func (v *ServiceRpcServer) AddService(ctx context.Context, info *proto.ServiceIn
|
||||
GrpcAddr: info.GetGrpcAddr(),
|
||||
HttpAddr: info.HttpAddr,
|
||||
}
|
||||
AddServiceInstance(in)
|
||||
log.Info().Str("id", clientId).Str("label", info.GetLabel()).Msg("New service registered")
|
||||
err = AddServiceInstance(in)
|
||||
if err == nil {
|
||||
log.Info().Str("id", clientId).Str("label", info.GetLabel()).Msg("New service registered")
|
||||
} else {
|
||||
log.Error().Str("id", clientId).Str("label", info.GetLabel()).Err(err).Msg("Unable to register a service")
|
||||
}
|
||||
return &proto.AddServiceResponse{
|
||||
IsSuccess: true,
|
||||
IsSuccess: err == nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *ServiceRpcServer) RemoveService(ctx context.Context, request *proto.RemoveServiceRequest) (*proto.RemoveServiceResponse, error) {
|
||||
RemoveServiceInstance(request.GetId())
|
||||
log.Info().Str("id", request.GetId()).Msg("A service removed.")
|
||||
err := RemoveServiceInstance(request.GetId())
|
||||
if err == nil {
|
||||
log.Info().Str("id", request.GetId()).Msg("A service removed")
|
||||
} else {
|
||||
log.Error().Str("id", request.GetId()).Err(err).Msg("Unable to remove a service")
|
||||
}
|
||||
return &proto.RemoveServiceResponse{
|
||||
IsSuccess: true,
|
||||
IsSuccess: err == nil,
|
||||
}, nil
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
directory2 "git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/internal/directory"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func listExistsService(c *fiber.Ctx) error {
|
||||
services := directory2.ListServiceInstance()
|
||||
services := directory.ListServiceInstance()
|
||||
|
||||
return c.JSON(lo.Map(services, func(item *directory2.ServiceInstance, index int) map[string]any {
|
||||
return c.JSON(lo.Map(services, func(item *directory.ServiceInstance, index int) map[string]any {
|
||||
return map[string]any{
|
||||
"id": item.ID,
|
||||
"type": item.Type,
|
||||
|
@ -1,6 +1,10 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"time"
|
||||
)
|
||||
@ -12,8 +16,22 @@ func ConnectEtcd(endpoints []string) error {
|
||||
Endpoints: endpoints,
|
||||
DialTimeout: 10 * time.Second,
|
||||
})
|
||||
if err == nil {
|
||||
Kv = conn
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var status []bool
|
||||
for _, endpoint := range endpoints {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
_, err := conn.Status(ctx, endpoint)
|
||||
if err != nil {
|
||||
log.Warn().Str("endpoint", endpoint).Err(err).Msg("An KV endpoint is not available...")
|
||||
}
|
||||
status = append(status, err == nil)
|
||||
cancel()
|
||||
}
|
||||
if len(lo.Filter(status, func(s bool, _ int) bool { return s })) == 0 {
|
||||
return fmt.Errorf("unable to connect to all KV endpoints")
|
||||
}
|
||||
Kv = conn
|
||||
return err
|
||||
}
|
||||
|
@ -47,12 +47,16 @@ func main() {
|
||||
}
|
||||
|
||||
// Connect to kv (etcd)
|
||||
log.Info().Msg("Connecting to kv (etcd)...")
|
||||
if err := kv.ConnectEtcd(viper.GetStringSlice("kv.endpoints")); err != nil {
|
||||
log.Error().Err(err).Msg("An error occurred when connecting to kv (etcd), please check your configuration in kv section.")
|
||||
log.Fatal().Msg("Kv is required for service discovery and directory feature, cannot be disabled.")
|
||||
} else {
|
||||
log.Info().Msg("Connected to kv (etcd)!")
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
log.Info().Msg("Connecting to database...")
|
||||
if db, err := database.Connect(viper.GetString("database.dsn")); err != nil {
|
||||
log.Error().Err(err).Msg("An error occurred when connecting to database. Database related features will be disabled.")
|
||||
} else {
|
||||
@ -62,7 +66,7 @@ func main() {
|
||||
log.Error().Err(err).Msg("An error occurred when querying database version. Database related features will be disabled.")
|
||||
database.Kdb = nil
|
||||
} else {
|
||||
log.Info().Str("version", version).Msg("Connected to database")
|
||||
log.Info().Str("version", version).Msg("Connected to database!")
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user