mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-23 12:47:48 +00:00
feat: predastore multinode (#19)
This commit is contained in:
+17
-11
@@ -557,26 +557,31 @@ ip addr add 10.11.12.2/24 dev eth0
|
||||
ip addr add 10.11.12.3/24 dev eth0
|
||||
```
|
||||
|
||||
### Initialize the Leader Node
|
||||
|
||||
Next, `hive` will be installed and initiated in `$HOME/node1` for configuration files.
|
||||
|
||||
```bash
|
||||
# Initialize node1
|
||||
./bin/hive admin init \
|
||||
--node node1 \
|
||||
--bind 10.11.12.1 \
|
||||
--cluster-bind 10.11.12.1 \
|
||||
--cluster-routes 10.11.12.1:4248 \
|
||||
--predastore-nodes 10.11.12.1,10.11.12.2,10.11.12.3 \
|
||||
--port 4432 \
|
||||
--hive-dir ~/node1/ \
|
||||
--config-dir ~/node1/config/ \
|
||||
--region ap-southeast-2 \
|
||||
--az ap-southeast-2a \
|
||||
|
||||
./scripts/start-dev.sh ~/node1/
|
||||
```
|
||||
|
||||
### Join Follower Nodes
|
||||
|
||||
Two other nodes will be created, joining node1 to exchange configuration files and identify as a new node.
|
||||
|
||||
```bash
|
||||
# Join cluster
|
||||
./bin/hive admin join \
|
||||
--node node2 \
|
||||
--bind 10.11.12.2 \
|
||||
@@ -588,11 +593,10 @@ Two other nodes will be created, joining node1 to exchange configuration files a
|
||||
--region ap-southeast-2 \
|
||||
--az ap-southeast-2a \
|
||||
|
||||
# Start node2
|
||||
./scripts/start-dev.sh ~/node2/
|
||||
```
|
||||
|
||||
echo "Node3 Setup:"
|
||||
|
||||
```bash
|
||||
./bin/hive admin join \
|
||||
--node node3 \
|
||||
--bind 10.11.12.3 \
|
||||
@@ -600,22 +604,24 @@ echo "Node3 Setup:"
|
||||
--cluster-routes 10.11.12.1:4248 \
|
||||
--host 10.11.12.1:4432 \
|
||||
--data-dir ~/node3/ \
|
||||
--config-dir ~/node3/config/
|
||||
--config-dir ~/node3/config/ \
|
||||
--region ap-southeast-2 \
|
||||
--az ap-southeast-2a \
|
||||
|
||||
./scripts/start-dev.sh ~/node3/
|
||||
```
|
||||
|
||||
Once configured, each node will have its own storage for config, data and state in `~/node[1,2,3]` to simulate a unique node and network.
|
||||
### Verify the Cluster
|
||||
|
||||
Once the multi-node is configured, follow the original installation instructions above to:
|
||||
Each node will have its own storage for config, data and state in `~/node[1,2,3]` to simulate a unique node and network.
|
||||
|
||||
Once the multi-node cluster is configured, follow the original installation instructions above to:
|
||||
|
||||
- Import an SSH key
|
||||
- Clone an AMI
|
||||
- Launch an instance
|
||||
- Import an AMI
|
||||
- Launch instances
|
||||
|
||||
Note - when using the AWS CLI tool you must specify the IP address of the node (10.11.12.1, 10.11.12.2, or 10.11.12.3).
|
||||
Note - when using the AWS CLI tool you must specify the IP address of a gateway node (10.11.12.1, 10.11.12.2, or 10.11.12.3).
|
||||
|
||||
```bash
|
||||
aws --endpoint-url https://10.11.12.3:9999 ec2 describe-instances --instance-ids i-9f5f648adc57ea46d
|
||||
|
||||
+107
-53
@@ -55,6 +55,9 @@ var predastoreTomlTemplate string
|
||||
//go:embed templates/nats.conf
|
||||
var natsConfTemplate string
|
||||
|
||||
//go:embed templates/predastore-multinode.toml
|
||||
var predastoreMultiNodeTemplate string
|
||||
|
||||
var supportedArchs = map[string]bool{
|
||||
"x86_64": true,
|
||||
"aarch64": true, // alias for arm64
|
||||
@@ -157,6 +160,7 @@ func init() {
|
||||
adminInitCmd.Flags().String("bind", "0.0.0.0", "IP address to bind services to (e.g., 10.11.12.1 for multi-node)")
|
||||
adminInitCmd.Flags().String("cluster-bind", "", "IP address to bind NATS cluster services to (e.g., 10.11.12.1 for multi-node)")
|
||||
adminInitCmd.Flags().String("cluster-routes", "", "NATS cluster hosts for routing specify multiple with comma (e.g., 10.11.12.1:4248,10.11.12.2:4248 for multi-node)")
|
||||
adminInitCmd.Flags().String("predastore-nodes", "", "Comma-separated IPs for multi-node Predastore cluster (e.g., 10.11.12.1,10.11.12.2,10.11.12.3). Requires >= 3 nodes.")
|
||||
|
||||
// Flags for admin join
|
||||
adminJoinCmd.Flags().String("region", "ap-southeast-2", "Region for this node")
|
||||
@@ -474,6 +478,7 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
clusterBind, _ := cmd.Flags().GetString("cluster-bind")
|
||||
clusterRoutesStr, _ := cmd.Flags().GetString("cluster-routes")
|
||||
clusterRoutes := strings.Split(clusterRoutesStr, ",")
|
||||
predastoreNodesStr, _ := cmd.Flags().GetString("predastore-nodes")
|
||||
|
||||
// Validate IP address format
|
||||
if net.ParseIP(bindIP) == nil {
|
||||
@@ -561,6 +566,49 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
|
||||
portStr := fmt.Sprintf("%d", port)
|
||||
|
||||
// Parse multi-node predastore configuration
|
||||
var predastoreNodeID int
|
||||
if predastoreNodesStr != "" {
|
||||
ips := strings.Split(predastoreNodesStr, ",")
|
||||
if len(ips) < 3 {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error: --predastore-nodes requires at least 3 IPs, got %d\n", len(ips))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var predastoreNodes []admin.PredastoreNodeConfig
|
||||
for i, ip := range ips {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if net.ParseIP(ip) == nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error: Invalid IP in --predastore-nodes: %s\n", ip)
|
||||
os.Exit(1)
|
||||
}
|
||||
predastoreNodes = append(predastoreNodes, admin.PredastoreNodeConfig{
|
||||
ID: i + 1,
|
||||
Host: ip,
|
||||
})
|
||||
}
|
||||
|
||||
predastoreNodeID = admin.FindNodeIDByIP(predastoreNodes, bindIP)
|
||||
if predastoreNodeID == 0 {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error: --bind IP %s not found in --predastore-nodes list\n", bindIP)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate multi-node predastore.toml
|
||||
predastoreContent, err := admin.GenerateMultiNodePredastoreConfig(predastoreMultiNodeTemplate, predastoreNodes, accessKey, secretKey, region)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating multi-node predastore config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
predastorePath := filepath.Join(predastoreDir, "predastore.toml")
|
||||
if err := os.WriteFile(predastorePath, []byte(predastoreContent), 0600); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing predastore config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Created: multi-node predastore.toml (node ID: %d)\n", predastoreNodeID)
|
||||
}
|
||||
|
||||
configSettings := admin.ConfigSettings{
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
@@ -575,30 +623,21 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
BindIP: bindIP,
|
||||
ClusterBindIP: clusterBind,
|
||||
ClusterRoutes: clusterRoutes,
|
||||
|
||||
PredastoreNodeID: predastoreNodeID,
|
||||
}
|
||||
|
||||
// Generate all config files
|
||||
// Generate config files
|
||||
configs := []admin.ConfigFile{
|
||||
{
|
||||
Name: "hive.toml",
|
||||
Path: hiveTomlPath,
|
||||
Template: hiveTomlTemplate,
|
||||
},
|
||||
{
|
||||
Name: filepath.Join(awsgwDir, "awsgw.toml"),
|
||||
Path: filepath.Join(awsgwDir, "awsgw.toml"),
|
||||
Template: awsgwTomlTemplate,
|
||||
},
|
||||
{
|
||||
Name: filepath.Join(predastoreDir, "predastore.toml"),
|
||||
Path: filepath.Join(predastoreDir, "predastore.toml"),
|
||||
Template: predastoreTomlTemplate,
|
||||
},
|
||||
{
|
||||
Name: filepath.Join(natsDir, "nats.conf"),
|
||||
Path: filepath.Join(natsDir, "nats.conf"),
|
||||
Template: natsConfTemplate,
|
||||
},
|
||||
{Name: "hive.toml", Path: hiveTomlPath, Template: hiveTomlTemplate},
|
||||
{Name: filepath.Join(awsgwDir, "awsgw.toml"), Path: filepath.Join(awsgwDir, "awsgw.toml"), Template: awsgwTomlTemplate},
|
||||
{Name: filepath.Join(natsDir, "nats.conf"), Path: filepath.Join(natsDir, "nats.conf"), Template: natsConfTemplate},
|
||||
}
|
||||
// Skip template-based predastore.toml if multi-node was already generated
|
||||
if predastoreNodesStr == "" {
|
||||
configs = append(configs, admin.ConfigFile{
|
||||
Name: filepath.Join(predastoreDir, "predastore.toml"), Path: filepath.Join(predastoreDir, "predastore.toml"), Template: predastoreTomlTemplate,
|
||||
})
|
||||
}
|
||||
|
||||
err := admin.GenerateConfigFiles(configs, configSettings)
|
||||
@@ -836,6 +875,41 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
|
||||
// Write individual node config files
|
||||
portStr := fmt.Sprintf("%d", port)
|
||||
|
||||
awsgwDir := filepath.Join(configDir, "awsgw")
|
||||
predastoreDir := filepath.Join(configDir, "predastore")
|
||||
natsDir := filepath.Join(configDir, "nats")
|
||||
|
||||
for _, dir := range []string{awsgwDir, predastoreDir, natsDir} {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating directory %s: %v\n", dir, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle multi-node predastore config from leader
|
||||
var predastoreNodeID int
|
||||
hasLeaderPredastoreConfig := joinResp.PredastoreConfig != ""
|
||||
|
||||
if hasLeaderPredastoreConfig {
|
||||
// Write the leader's predastore.toml
|
||||
predastorePath := filepath.Join(predastoreDir, "predastore.toml")
|
||||
if err := os.WriteFile(predastorePath, []byte(joinResp.PredastoreConfig), 0600); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing predastore config from leader: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Predastore config received from leader: %s\n", predastorePath)
|
||||
|
||||
predastoreNodeID = admin.ParsePredastoreNodeIDFromConfig(joinResp.PredastoreConfig, bindIP)
|
||||
|
||||
if predastoreNodeID > 0 {
|
||||
fmt.Printf("✅ Detected Predastore node ID: %d (for bind IP %s)\n", predastoreNodeID, bindIP)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "ERROR: Could not detect Predastore node ID for bind IP %s in leader config\n", bindIP)
|
||||
fmt.Fprintf(os.Stderr, "This node's IP must be listed in the --predastore-nodes used during init\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
configSettings := admin.ConfigSettings{
|
||||
AccessKey: localConfig.Nodes[node].AccessKey,
|
||||
SecretKey: localConfig.Nodes[node].SecretKey,
|
||||
@@ -849,41 +923,21 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
|
||||
BindIP: bindIP,
|
||||
ClusterBindIP: clusterBind,
|
||||
ClusterRoutes: clusterRoutes,
|
||||
|
||||
PredastoreNodeID: predastoreNodeID,
|
||||
}
|
||||
|
||||
awsgwDir := filepath.Join(configDir, "awsgw")
|
||||
predastoreDir := filepath.Join(configDir, "predastore")
|
||||
natsDir := filepath.Join(configDir, "nats")
|
||||
|
||||
for _, dir := range []string{awsgwDir, predastoreDir, natsDir} {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating directory %s: %v\n", dir, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate all config files
|
||||
// Generate config files
|
||||
configs := []admin.ConfigFile{
|
||||
{
|
||||
Name: "hive.toml",
|
||||
Path: hiveTomlPath,
|
||||
Template: hiveTomlTemplate,
|
||||
},
|
||||
{
|
||||
Name: filepath.Join(awsgwDir, "awsgw.toml"),
|
||||
Path: filepath.Join(awsgwDir, "awsgw.toml"),
|
||||
Template: awsgwTomlTemplate,
|
||||
},
|
||||
{
|
||||
Name: filepath.Join(predastoreDir, "predastore.toml"),
|
||||
Path: filepath.Join(predastoreDir, "predastore.toml"),
|
||||
Template: predastoreTomlTemplate,
|
||||
},
|
||||
{
|
||||
Name: filepath.Join(natsDir, "nats.conf"),
|
||||
Path: filepath.Join(natsDir, "nats.conf"),
|
||||
Template: natsConfTemplate,
|
||||
},
|
||||
{Name: "hive.toml", Path: hiveTomlPath, Template: hiveTomlTemplate},
|
||||
{Name: filepath.Join(awsgwDir, "awsgw.toml"), Path: filepath.Join(awsgwDir, "awsgw.toml"), Template: awsgwTomlTemplate},
|
||||
{Name: filepath.Join(natsDir, "nats.conf"), Path: filepath.Join(natsDir, "nats.conf"), Template: natsConfTemplate},
|
||||
}
|
||||
// Skip template-based predastore.toml if received from leader
|
||||
if !hasLeaderPredastoreConfig {
|
||||
configs = append(configs, admin.ConfigFile{
|
||||
Name: filepath.Join(predastoreDir, "predastore.toml"), Path: filepath.Join(predastoreDir, "predastore.toml"), Template: predastoreTomlTemplate,
|
||||
})
|
||||
}
|
||||
|
||||
err = admin.GenerateConfigFiles(configs, configSettings)
|
||||
|
||||
@@ -37,4 +37,7 @@ host = "{{.BindIP}}:8443"
|
||||
region = "{{.Region}}"
|
||||
bucket = "predastore"
|
||||
accesskey = "{{.AccessKey}}"
|
||||
secretkey = "{{.SecretKey}}"
|
||||
secretkey = "{{.SecretKey}}"
|
||||
{{- if gt .PredastoreNodeID 0}}
|
||||
node_id = {{.PredastoreNodeID}}
|
||||
{{- end}}
|
||||
@@ -0,0 +1,45 @@
|
||||
# Predastore S3-compatible storage configuration file in TOML format.
|
||||
# Generated for multi-node cluster
|
||||
version = "1.0"
|
||||
region = "{{.Region}}"
|
||||
|
||||
host = "0.0.0.0"
|
||||
port = 8443
|
||||
|
||||
debug = false
|
||||
|
||||
[rs]
|
||||
data = 2
|
||||
parity = 1
|
||||
{{range .Nodes}}
|
||||
[[db]]
|
||||
id = {{.ID}}
|
||||
host = "{{.Host}}"
|
||||
port = 6660
|
||||
path = "distributed/db/node-{{.ID}}/"
|
||||
access_key_id = "{{$.AccessKey}}"
|
||||
secret_access_key = "{{$.SecretKey}}"
|
||||
{{- if eq .ID 1}}
|
||||
leader = true
|
||||
{{- end}}
|
||||
{{end}}
|
||||
{{- range .Nodes}}
|
||||
[[nodes]]
|
||||
id = {{.ID}}
|
||||
host = "{{.Host}}"
|
||||
port = 9991
|
||||
path = "distributed/nodes/node-{{.ID}}/"
|
||||
{{end}}
|
||||
[[buckets]]
|
||||
name = "predastore"
|
||||
region = "{{.Region}}"
|
||||
type = "distributed"
|
||||
public = false
|
||||
encryption = ""
|
||||
|
||||
[[auth]]
|
||||
access_key_id = "{{.AccessKey}}"
|
||||
secret_access_key = "{{.SecretKey}}"
|
||||
policy = [
|
||||
{ bucket = "predastore", actions = ["s3:ListBucket", "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListAllMyBuckets"] },
|
||||
]
|
||||
@@ -0,0 +1,154 @@
|
||||
# Multi-Node Predastore Configuration
|
||||
|
||||
## Overview
|
||||
|
||||
Predastore uses Reed-Solomon erasure coding RS(2,1) -- 2 data shards and 1 parity shard -- requiring a minimum of 3 distinct nodes. Each shard is placed on a separate node via a consistent hash ring (`GetClosestN(hash, 3)`), which means every node must know the full cluster topology at startup to make identical placement decisions.
|
||||
|
||||
This is fundamentally different from NATS, which uses gossip-based discovery and can add members dynamically. Predastore's hash ring is static -- if nodes disagree about membership, they'll compute different shard placements and data becomes unreachable.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Init (leader node)
|
||||
|
||||
```
|
||||
hive admin init --predastore-nodes "10.11.12.1,10.11.12.2,10.11.12.3" --bind 10.11.12.1 ...
|
||||
```
|
||||
|
||||
1. The comma-separated IPs are parsed and validated. Each gets a sequential node ID (1, 2, 3).
|
||||
2. The leader's `--bind` IP is matched against the list to determine its own node ID. If the bind IP isn't in the list, init fails.
|
||||
3. `GenerateMultiNodePredastoreConfig` renders a single `predastore.toml` containing the full cluster topology -- all DB entries (port 6660), all shard entries (port 9991), RS config, auth, and buckets. Node ID 1 is marked as the Raft bootstrap leader.
|
||||
4. The predastore.toml is written to `config/predastore/predastore.toml`.
|
||||
5. The leader's node ID is written into `hive.toml` as `node_id = 1` under the `[predastore]` section.
|
||||
|
||||
### Join (follower nodes)
|
||||
|
||||
```
|
||||
hive admin join --leader https://10.11.12.1:7777 --bind 10.11.12.2 ...
|
||||
```
|
||||
|
||||
1. The follower contacts the leader's `/join` HTTP endpoint.
|
||||
2. The leader reads its `predastore.toml` from disk and includes the full content in the join response (`PredastoreConfig` field).
|
||||
3. The follower writes this verbatim to its own `config/predastore/predastore.toml`. Now all nodes have an identical copy.
|
||||
4. `ParsePredastoreNodeIDFromConfig` unmarshals the TOML, extracts the `[[db]]` array, and matches the follower's `--bind` IP to find its node ID. If the IP isn't found, join fails with an error.
|
||||
5. The detected node ID is written into the follower's `hive.toml` (e.g., `node_id = 2`).
|
||||
|
||||
### Startup
|
||||
|
||||
When `start-dev.sh` runs, it reads `node_id` from `hive.toml` using awk and exports it as `HIVE_PREDASTORE_NODE_ID`. Predastore reads this environment variable to know which DB and shard entries in the shared config belong to it.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### Shared config, per-node identity
|
||||
|
||||
Every node gets the **same** `predastore.toml` (the full cluster topology). The only difference per node is `node_id` in `hive.toml`. This separation exists because:
|
||||
|
||||
- Predastore needs all members known at startup for consistent hash ring agreement.
|
||||
- The `predastore.toml` describes the cluster. The `node_id` in `hive.toml` says "I am this member."
|
||||
- This makes config distribution simple: the leader just sends one file, and the follower figures out its own identity by matching its bind IP.
|
||||
|
||||
### Why IPs must be specified at init time
|
||||
|
||||
Unlike NATS (gossip discovery), Predastore requires static membership. The hash ring maps `GetClosestN(hash, 3)` to determine which 3 nodes store each object's shards. If a node joins without being in the ring, existing nodes won't route data to it. If a node is in the ring but missing, data placed on it becomes unavailable.
|
||||
|
||||
This means all Predastore node IPs must be known when the leader is initialized. The `--predastore-nodes` flag captures this.
|
||||
|
||||
### Node ID 1 is the Raft bootstrap leader
|
||||
|
||||
The generated config marks `leader = true` on the `[[db]]` entry with `id = 1`. This is the node that bootstraps the Raft cluster. It's always the first IP in the `--predastore-nodes` list, which is also the `--bind` IP of the init command (the leader node).
|
||||
|
||||
### Config distribution via join protocol
|
||||
|
||||
Rather than requiring operators to manually copy `predastore.toml` to each node, the existing HTTP join protocol distributes it. The leader's daemon reads its predastore.toml and includes it in the join response. This is the same pattern used for CA certificates.
|
||||
|
||||
The leader currently sends its predastore.toml unconditionally (even in single-node setups). The follower treats any non-empty `PredastoreConfig` in the response as "leader provided a multi-node config" and skips generating its own template-based config.
|
||||
|
||||
### Node ID detection uses proper TOML parsing
|
||||
|
||||
The follower determines its node ID by parsing the received predastore.toml with `go-toml/v2` and calling `FindNodeIDByIP` against the `[[db]]` entries. If parsing fails or the bind IP isn't found, join exits with an error. This mirrors the init path, which also exits on failure.
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
When `--predastore-nodes` is omitted from `hive admin init`:
|
||||
|
||||
- No multi-node predastore.toml is generated.
|
||||
- `PredastoreNodeID` stays at 0, so `node_id` is not emitted in hive.toml.
|
||||
- The default single-node predastore.toml template is used instead.
|
||||
- `start-dev.sh` finds no `node_id` in hive.toml and leaves `HIVE_PREDASTORE_NODE_ID` empty.
|
||||
- Predastore runs in its default single-node mode.
|
||||
|
||||
Existing single-node deployments are unaffected.
|
||||
|
||||
## Files Involved
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `cmd/hive/cmd/admin.go` | CLI flag parsing, init/join orchestration |
|
||||
| `hive/admin/admin.go` | `GenerateMultiNodePredastoreConfig`, `FindNodeIDByIP`, `ParsePredastoreNodeIDFromConfig`, `BaseConfigFiles` |
|
||||
| `hive/config/config.go` | `PredastoreConfig.NodeID` field, `NodeJoinResponse.PredastoreConfig` field |
|
||||
| `hive/daemon/daemon.go` | `/join` endpoint reads and sends predastore.toml |
|
||||
| `cmd/hive/cmd/templates/hive.toml` | Conditional `node_id` rendering |
|
||||
| `scripts/start-dev.sh` | Auto-detects `NODE_ID` from hive.toml at startup |
|
||||
|
||||
## Generated Config Example
|
||||
|
||||
For `--predastore-nodes "10.11.12.1,10.11.12.2,10.11.12.3"`:
|
||||
|
||||
```toml
|
||||
version = "1.0"
|
||||
region = "ap-southeast-2"
|
||||
|
||||
host = "0.0.0.0"
|
||||
port = 8443
|
||||
|
||||
[rs]
|
||||
data = 2
|
||||
parity = 1
|
||||
|
||||
[[db]]
|
||||
id = 1
|
||||
host = "10.11.12.1"
|
||||
port = 6660
|
||||
path = "distributed/db/node-1/"
|
||||
leader = true
|
||||
|
||||
[[db]]
|
||||
id = 2
|
||||
host = "10.11.12.2"
|
||||
port = 6660
|
||||
path = "distributed/db/node-2/"
|
||||
|
||||
[[db]]
|
||||
id = 3
|
||||
host = "10.11.12.3"
|
||||
port = 6660
|
||||
path = "distributed/db/node-3/"
|
||||
|
||||
[[nodes]]
|
||||
id = 1
|
||||
host = "10.11.12.1"
|
||||
port = 9991
|
||||
path = "distributed/nodes/node-1/"
|
||||
|
||||
[[nodes]]
|
||||
id = 2
|
||||
host = "10.11.12.2"
|
||||
port = 9991
|
||||
path = "distributed/nodes/node-2/"
|
||||
|
||||
[[nodes]]
|
||||
id = 3
|
||||
host = "10.11.12.3"
|
||||
port = 9991
|
||||
path = "distributed/nodes/node-3/"
|
||||
|
||||
[[buckets]]
|
||||
name = "predastore"
|
||||
region = "ap-southeast-2"
|
||||
type = "distributed"
|
||||
|
||||
[[auth]]
|
||||
access_key_id = "..."
|
||||
secret_access_key = "..."
|
||||
```
|
||||
|
||||
Each node receives this identical file. Node 1's `hive.toml` has `node_id = 1`, node 2 has `node_id = 2`, etc.
|
||||
@@ -8,13 +8,16 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
toml "github.com/pelletier/go-toml/v2"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
@@ -35,6 +38,15 @@ type ConfigSettings struct {
|
||||
// Cluster settings
|
||||
ClusterBindIP string
|
||||
ClusterRoutes []string
|
||||
|
||||
// Predastore multi-node
|
||||
PredastoreNodeID int
|
||||
}
|
||||
|
||||
// PredastoreNodeConfig describes a single Predastore node for multi-node config generation.
|
||||
type PredastoreNodeConfig struct {
|
||||
ID int
|
||||
Host string
|
||||
}
|
||||
|
||||
type ConfigFile struct {
|
||||
@@ -563,3 +575,56 @@ func SetupAWSCredentials(accessKey, secretKey, region, certPath string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateMultiNodePredastoreConfig produces a complete predastore.toml for a
|
||||
// multi-node Predastore cluster. Each node gets its own DB entry (port 6660)
|
||||
// and shard entry (port 9991) on a distinct IP. Node ID 1 is the bootstrap leader.
|
||||
func GenerateMultiNodePredastoreConfig(templateStr string, nodes []PredastoreNodeConfig, accessKey, secretKey, region string) (string, error) {
|
||||
if len(nodes) < 3 {
|
||||
return "", fmt.Errorf("multi-node predastore requires at least 3 nodes, got %d", len(nodes))
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Nodes []PredastoreNodeConfig
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Region string
|
||||
}{nodes, accessKey, secretKey, region}
|
||||
|
||||
tmpl, err := template.New("predastore-multinode").Parse(templateStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse predastore template: %w", err)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
if err := tmpl.Execute(&b, data); err != nil {
|
||||
return "", fmt.Errorf("failed to execute predastore template: %w", err)
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// FindNodeIDByIP returns the node ID for the given IP in the node list,
|
||||
// or 0 if the IP is not found.
|
||||
func FindNodeIDByIP(nodes []PredastoreNodeConfig, ip string) int {
|
||||
for _, n := range nodes {
|
||||
if n.Host == ip {
|
||||
return n.ID
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ParsePredastoreNodeIDFromConfig parses a predastore.toml string and returns
|
||||
// the node ID whose host matches the given IP, or 0 if not found.
|
||||
func ParsePredastoreNodeIDFromConfig(tomlContent string, ip string) int {
|
||||
var cfg struct {
|
||||
DB []PredastoreNodeConfig `toml:"db"`
|
||||
}
|
||||
if err := toml.Unmarshal([]byte(tomlContent), &cfg); err != nil {
|
||||
slog.Warn("Failed to parse predastore.toml content", "error", err)
|
||||
return 0
|
||||
}
|
||||
return FindNodeIDByIP(cfg.DB, ip)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ type PredastoreConfig struct {
|
||||
AccessKey string `mapstructure:"accesskey"`
|
||||
SecretKey string `mapstructure:"secretkey"`
|
||||
BaseDir string `mapstructure:"base_dir"`
|
||||
NodeID int `mapstructure:"node_id"`
|
||||
}
|
||||
|
||||
// DaemonConfig holds the daemon configuration
|
||||
@@ -188,6 +189,8 @@ type NodeJoinResponse struct {
|
||||
// CA distribution for per-node certificate generation
|
||||
CACert string `json:"ca_cert,omitempty"` // PEM-encoded CA certificate
|
||||
CAKey string `json:"ca_key,omitempty"` // PEM-encoded CA private key
|
||||
// Predastore config distribution for multi-node clusters
|
||||
PredastoreConfig string `json:"predastore_config,omitempty"` // Full predastore.toml content
|
||||
}
|
||||
|
||||
type NodeHealthResponse struct {
|
||||
|
||||
+20
-9
@@ -766,8 +766,9 @@ func (d *Daemon) ClusterManager() error {
|
||||
}
|
||||
|
||||
// Read CA certificate and key to share with joining node for per-node cert generation
|
||||
caCertPath := filepath.Join(d.config.BaseDir, "config", "ca.pem")
|
||||
caKeyPath := filepath.Join(d.config.BaseDir, "config", "ca.key")
|
||||
configDir := filepath.Dir(d.configPath)
|
||||
caCertPath := filepath.Join(configDir, "ca.pem")
|
||||
caKeyPath := filepath.Join(configDir, "ca.key")
|
||||
|
||||
var caCert, caKey string
|
||||
if caCertPEM, err := os.ReadFile(caCertPath); err == nil {
|
||||
@@ -782,14 +783,24 @@ func (d *Daemon) ClusterManager() error {
|
||||
slog.Warn("Could not read CA key for join response", "error", err)
|
||||
}
|
||||
|
||||
// Read predastore.toml to share with joining node (for multi-node predastore)
|
||||
var predastoreConfig string
|
||||
predastorePath := filepath.Join(configDir, "predastore", "predastore.toml")
|
||||
if content, err := os.ReadFile(predastorePath); err == nil {
|
||||
predastoreConfig = string(content)
|
||||
} else {
|
||||
slog.Warn("Could not read predastore.toml for join response", "path", predastorePath, "error", err)
|
||||
}
|
||||
|
||||
return c.JSON(config.NodeJoinResponse{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("node %s successfully joined cluster", req.Node),
|
||||
SharedData: sharedData,
|
||||
ConfigHash: configHash,
|
||||
JoiningNode: req.Node,
|
||||
CACert: caCert,
|
||||
CAKey: caKey,
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("node %s successfully joined cluster", req.Node),
|
||||
SharedData: sharedData,
|
||||
ConfigHash: configHash,
|
||||
JoiningNode: req.Node,
|
||||
CACert: caCert,
|
||||
CAKey: caKey,
|
||||
PredastoreConfig: predastoreConfig,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+12
-1
@@ -180,7 +180,18 @@ export HIVE_PREDASTORE_PORT=8443
|
||||
# Default, distributed backend. For testing, all nodes running locally.
|
||||
# Specify NODE_ID to run a specific node (e.g multi-server)
|
||||
export HIVE_PREDASTORE_BACKEND=distributed
|
||||
export HIVE_PREDASTORE_NODE_ID=
|
||||
|
||||
# Auto-detect Predastore NODE_ID from hive.toml if not already set
|
||||
if [ -z "$HIVE_PREDASTORE_NODE_ID" ]; then
|
||||
if [ -f "$CONFIG_DIR/hive.toml" ]; then
|
||||
DETECTED_NODE_ID=$(awk -F'= *' '/node_id/{gsub(/ /,"",$2); print $2; exit}' "$CONFIG_DIR/hive.toml")
|
||||
if [ -n "$DETECTED_NODE_ID" ] && [ "$DETECTED_NODE_ID" != "0" ]; then
|
||||
export HIVE_PREDASTORE_NODE_ID="$DETECTED_NODE_ID"
|
||||
echo " Auto-detected Predastore NODE_ID=$DETECTED_NODE_ID from hive.toml"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
export HIVE_PREDASTORE_NODE_ID="${HIVE_PREDASTORE_NODE_ID:-}"
|
||||
|
||||
# Use air for hot reloading (dev!)
|
||||
#PREDASTORE_CMD="air -c .air-predastore.toml"
|
||||
|
||||
@@ -454,6 +454,7 @@ init_leader_node() {
|
||||
--bind "${NODE1_IP}" \
|
||||
--cluster-bind "${NODE1_IP}" \
|
||||
--cluster-routes "${NODE1_IP}:${NATS_CLUSTER_PORT}" \
|
||||
--predastore-nodes "${NODE1_IP},${NODE2_IP},${NODE3_IP}" \
|
||||
--port ${CLUSTER_PORT} \
|
||||
--region ap-southeast-2 \
|
||||
--az ap-southeast-2a \
|
||||
@@ -488,8 +489,36 @@ join_follower_node() {
|
||||
--region ap-southeast-2 \
|
||||
--az "ap-southeast-2a"
|
||||
|
||||
# Point predastore to node1 (only node1 runs predastore until multi-node predastore is implemented)
|
||||
sed -i "s|host = \"${node_ip}:8443\"|host = \"${NODE1_IP}:8443\"|" "$data_dir/config/hive.toml"
|
||||
|
||||
echo "Node$node_num joined cluster"
|
||||
}
|
||||
|
||||
# Verify Predastore cluster health
|
||||
# Checks that Predastore is reachable on all node IPs
|
||||
# Usage: verify_predastore_cluster [expected_nodes]
|
||||
verify_predastore_cluster() {
|
||||
local expected_nodes="${1:-3}"
|
||||
local healthy=0
|
||||
|
||||
echo "Verifying Predastore cluster health (expecting $expected_nodes nodes)..."
|
||||
|
||||
for i in $(seq 1 "$expected_nodes"); do
|
||||
local node_ip="${SIMULATED_NETWORK}.$i"
|
||||
|
||||
if curl -k -s "https://${node_ip}:${PREDASTORE_PORT}" > /dev/null 2>&1; then
|
||||
echo " Node$i ($node_ip:${PREDASTORE_PORT}): reachable"
|
||||
healthy=$((healthy + 1))
|
||||
else
|
||||
echo " Node$i ($node_ip:${PREDASTORE_PORT}): NOT reachable"
|
||||
fi
|
||||
done
|
||||
|
||||
echo " Healthy Predastore nodes: $healthy/$expected_nodes"
|
||||
|
||||
if [ "$healthy" -ge "$expected_nodes" ]; then
|
||||
echo " Predastore cluster is healthy"
|
||||
return 0
|
||||
else
|
||||
echo " WARNING: Predastore cluster may not be fully formed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -129,6 +129,14 @@ verify_nats_cluster 3 || {
|
||||
echo "WARNING: NATS cluster verification failed, continuing anyway..."
|
||||
}
|
||||
|
||||
# Verify Predastore cluster
|
||||
echo ""
|
||||
verify_predastore_cluster 3 || {
|
||||
echo "ERROR: Predastore cluster verification failed"
|
||||
dump_all_node_logs
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Wait for gateway on node1 (primary gateway)
|
||||
echo ""
|
||||
wait_for_gateway "${NODE1_IP}" 30
|
||||
|
||||
Reference in New Issue
Block a user