mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-14 02:25:50 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8500a3bc56 | ||
|
|
678c52e74f | ||
|
|
8fac0cdab8 | ||
|
|
09bfb28db9 | ||
|
|
3bd990d2ea | ||
|
|
5e040ba03d |
@@ -9,7 +9,6 @@ on:
|
||||
- 'weed/storage/**'
|
||||
- 'weed/pb/volume_server.proto'
|
||||
- 'weed/pb/volume_server_pb/**'
|
||||
- 'rust/volume_server/**'
|
||||
- '.github/workflows/volume-server-integration-tests.yml'
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
@@ -19,7 +18,6 @@ on:
|
||||
- 'weed/storage/**'
|
||||
- 'weed/pb/volume_server.proto'
|
||||
- 'weed/pb/volume_server_pb/**'
|
||||
- 'rust/volume_server/**'
|
||||
- '.github/workflows/volume-server-integration-tests.yml'
|
||||
|
||||
concurrency:
|
||||
@@ -122,49 +120,3 @@ jobs:
|
||||
echo "## Volume Server Integration Test Summary (${{ matrix.test-type }} - Shard ${{ matrix.shard }})" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Suite: test/volume_server/${{ matrix.test-type }} (Pattern: ${TEST_PATTERN})" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Command: go test -v -count=1 -timeout=${{ env.TEST_TIMEOUT }} ./test/volume_server/${{ matrix.test-type }}/... -run \"${TEST_PATTERN}\"" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
volume-server-rust-smoke:
|
||||
name: Volume Server Rust Smoke
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 40
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Set up Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Build SeaweedFS binary
|
||||
run: |
|
||||
cd weed
|
||||
go build -o weed .
|
||||
chmod +x weed
|
||||
./weed version
|
||||
|
||||
- name: Run Rust-mode HTTP smoke tests
|
||||
env:
|
||||
WEED_BINARY: ${{ github.workspace }}/weed/weed
|
||||
VOLUME_SERVER_IMPL: rust
|
||||
run: |
|
||||
go test -v -count=1 -timeout=25m ./test/volume_server/http/... -run "TestAdminStatusAndHealthz|TestUploadReadRangeHeadDeleteRoundTrip"
|
||||
|
||||
- name: Run Rust-mode gRPC smoke tests
|
||||
env:
|
||||
WEED_BINARY: ${{ github.workspace }}/weed/weed
|
||||
VOLUME_SERVER_IMPL: rust
|
||||
run: |
|
||||
go test -v -count=1 -timeout=25m ./test/volume_server/grpc/... -run "TestStateAndStatusRPCs|TestVolumeSyncStatusAndReadVolumeFileStatus"
|
||||
|
||||
- name: Rust smoke summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Volume Server Rust Smoke Summary" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- Mode: Go master + Rust volume server launcher (VOLUME_SERVER_IMPL=rust)" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- HTTP Command: go test -v -count=1 -timeout=25m ./test/volume_server/http/... -run \"TestAdminStatusAndHealthz|TestUploadReadRangeHeadDeleteRoundTrip\"" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- gRPC Command: go test -v -count=1 -timeout=25m ./test/volume_server/grpc/... -run \"TestStateAndStatusRPCs|TestVolumeSyncStatusAndReadVolumeFileStatus\"" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
Generated
-7
@@ -1,7 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "weed-volume-rs"
|
||||
version = "0.1.0"
|
||||
@@ -1,6 +0,0 @@
|
||||
[package]
|
||||
name = "weed-volume-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -1,61 +0,0 @@
|
||||
# Rust Volume Server Rewrite Dev Plan
|
||||
|
||||
## Goal
|
||||
Build a Rust implementation of SeaweedFS volume server that is behavior-compatible with the current Go implementation and can pass the existing integration suites under `/Users/chris/dev/seaweedfs2/test/volume_server/http` and `/Users/chris/dev/seaweedfs2/test/volume_server/grpc`.
|
||||
|
||||
## Compatibility Target
|
||||
- CLI compatibility for volume-server startup flags used by integration harness.
|
||||
- HTTP and gRPC behavioral parity for tested paths.
|
||||
- Drop-in process integration with current Go master in transition phases.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0: Bootstrap and Harness Integration
|
||||
- [x] Add Rust volume-server crate.
|
||||
- [x] Implement Rust launcher that can run as a volume-server process entrypoint.
|
||||
- [x] Add integration harness switches so tests can run with:
|
||||
- Go master + Go volume (default)
|
||||
- Go master + Rust volume (`VOLUME_SERVER_IMPL=rust` or `VOLUME_SERVER_BINARY=...`)
|
||||
- [ ] Add CI smoke coverage for Rust volume-server mode.
|
||||
|
||||
### Phase 1: Native Rust Control Plane Skeleton
|
||||
- [ ] Native Rust HTTP server with admin endpoints:
|
||||
- [ ] `GET /status`
|
||||
- [ ] `GET /healthz`
|
||||
- [ ] static/UI endpoints used by tests
|
||||
- [ ] Native Rust gRPC server with basic lifecycle/state RPCs:
|
||||
- [ ] `GetState`, `SetState`, `VolumeServerStatus`, `Ping`, `VolumeServerLeave`
|
||||
- [ ] Flag/config parser parity for currently exercised startup options.
|
||||
|
||||
### Phase 2: Native Data Path (HTTP + core gRPC)
|
||||
- [ ] HTTP read/write/delete parity:
|
||||
- [ ] path variants, conditional headers, ranges, auth, throttling
|
||||
- [ ] chunk manifest read/delete behavior
|
||||
- [ ] image and compression transform branches
|
||||
- [ ] gRPC data RPC parity:
|
||||
- [ ] `ReadNeedleBlob`, `ReadNeedleMeta`, `WriteNeedleBlob`
|
||||
- [ ] `BatchDelete`, `ReadAllNeedles`
|
||||
- [ ] copy/receive/sync baseline
|
||||
|
||||
### Phase 3: Advanced gRPC Surface
|
||||
- [ ] Vacuum RPC family.
|
||||
- [ ] Tail sender/receiver.
|
||||
- [ ] Erasure coding family.
|
||||
- [ ] Tiering/remote fetch family.
|
||||
- [ ] Query/Scrub family.
|
||||
|
||||
### Phase 4: Hardening and Cutover
|
||||
- [ ] Determinism/flake hardening in integration runtime.
|
||||
- [ ] Performance and resource-baseline checks versus Go.
|
||||
- [ ] Optional dual-run diff tooling for payload/header parity.
|
||||
- [ ] Default harness/CI mode switch to Rust volume server once parity threshold is met.
|
||||
|
||||
## Integration Test Mapping
|
||||
- HTTP suite: `/Users/chris/dev/seaweedfs2/test/volume_server/http`
|
||||
- gRPC suite: `/Users/chris/dev/seaweedfs2/test/volume_server/grpc`
|
||||
- Harness: `/Users/chris/dev/seaweedfs2/test/volume_server/framework`
|
||||
|
||||
## Progress Log
|
||||
- Date: 2026-02-15
|
||||
- Change: Created Rust volume-server crate (`weed-volume-rs`) as compatibility launcher and wired harness binary selection (`VOLUME_SERVER_IMPL`/`VOLUME_SERVER_BINARY`).
|
||||
- Validation: Rust-mode conformance smoke execution pending CI and local subset runs.
|
||||
@@ -1,177 +0,0 @@
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, ExitCode};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(err) => {
|
||||
eprintln!("weed-volume-rs: {err}");
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
|
||||
if args.iter().any(|a| a == "-h" || a == "--help") {
|
||||
print_help();
|
||||
return Ok(());
|
||||
}
|
||||
if args.iter().any(|a| a == "--version") {
|
||||
println!("weed-volume-rs 0.1.0");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut forwarded = Vec::<String>::new();
|
||||
let has_volume_subcommand = args.iter().any(|a| a == "volume");
|
||||
if has_volume_subcommand {
|
||||
forwarded.extend(args);
|
||||
} else {
|
||||
forwarded.push("volume".to_string());
|
||||
forwarded.extend(args);
|
||||
}
|
||||
|
||||
let weed_binary = resolve_weed_binary()?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let exec_err = Command::new(&weed_binary).args(&forwarded).exec();
|
||||
return Err(format!(
|
||||
"exec {} failed: {}",
|
||||
weed_binary.display(),
|
||||
exec_err
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let status = Command::new(&weed_binary)
|
||||
.args(&forwarded)
|
||||
.status()
|
||||
.map_err(|e| format!("spawn {} failed: {}", weed_binary.display(), e))?;
|
||||
if status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!(
|
||||
"delegated process {} exited with status {}",
|
||||
weed_binary.display(),
|
||||
status
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("weed-volume-rs");
|
||||
println!();
|
||||
println!("Rust compatibility launcher for SeaweedFS volume server.");
|
||||
println!("It forwards all volume-server flags to the Go weed binary.");
|
||||
println!();
|
||||
println!("Examples:");
|
||||
println!(" weed-volume-rs -ip=127.0.0.1 -port=8080 -master=127.0.0.1:9333 ...");
|
||||
println!(" weed-volume-rs volume -ip=127.0.0.1 -port=8080 -master=127.0.0.1:9333 ...");
|
||||
}
|
||||
|
||||
fn resolve_weed_binary() -> Result<PathBuf, String> {
|
||||
if let Some(from_env) = env::var_os("WEED_BINARY") {
|
||||
let path = PathBuf::from(from_env);
|
||||
if is_executable_file(&path) {
|
||||
return Ok(path);
|
||||
}
|
||||
return Err(format!(
|
||||
"WEED_BINARY is set but not executable: {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let repo_root = resolve_repo_root()?;
|
||||
let local_weed = repo_root.join("weed").join("weed");
|
||||
if is_executable_file(&local_weed) {
|
||||
return Ok(local_weed);
|
||||
}
|
||||
|
||||
let bin_dir = env::temp_dir().join("seaweedfs_volume_server_it_bin");
|
||||
std::fs::create_dir_all(&bin_dir)
|
||||
.map_err(|e| format!("create binary directory {}: {}", bin_dir.display(), e))?;
|
||||
let bin_path = bin_dir.join("weed");
|
||||
if is_executable_file(&bin_path) {
|
||||
return Ok(bin_path);
|
||||
}
|
||||
|
||||
build_weed_binary(&repo_root, &bin_path)?;
|
||||
if !is_executable_file(&bin_path) {
|
||||
return Err(format!(
|
||||
"built weed binary is not executable: {}",
|
||||
bin_path.display()
|
||||
));
|
||||
}
|
||||
Ok(bin_path)
|
||||
}
|
||||
|
||||
fn resolve_repo_root() -> Result<PathBuf, String> {
|
||||
if let Some(from_env) = env::var_os("SEAWEEDFS_REPO_ROOT") {
|
||||
return Ok(PathBuf::from(from_env));
|
||||
}
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.ok_or_else(|| "unable to detect repository root from CARGO_MANIFEST_DIR".to_string())?;
|
||||
Ok(repo_root.to_path_buf())
|
||||
}
|
||||
|
||||
fn build_weed_binary(repo_root: &PathBuf, output_path: &PathBuf) -> Result<(), String> {
|
||||
let mut cmd = Command::new("go");
|
||||
cmd.arg("build").arg("-o").arg(output_path).arg(".");
|
||||
cmd.current_dir(repo_root.join("weed"));
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|e| format!("failed to execute go build: {}", e))?;
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut msg = String::new();
|
||||
msg.push_str("go build failed");
|
||||
if !output.stdout.is_empty() {
|
||||
msg.push_str("\nstdout:\n");
|
||||
msg.push_str(&String::from_utf8_lossy(&output.stdout));
|
||||
}
|
||||
if !output.stderr.is_empty() {
|
||||
msg.push_str("\nstderr:\n");
|
||||
msg.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
Err(msg)
|
||||
}
|
||||
|
||||
fn is_executable_file(path: &PathBuf) -> bool {
|
||||
let metadata = match std::fs::metadata(path) {
|
||||
Ok(v) => v,
|
||||
Err(_) => return false,
|
||||
};
|
||||
if !metadata.is_file() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
metadata.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _collect_os_args() -> Vec<OsString> {
|
||||
env::args_os().collect()
|
||||
}
|
||||
@@ -77,14 +77,14 @@ func TestObjectLockValidation(t *testing.T) {
|
||||
require.NoError(t, err, "Setting Object Lock retention should succeed")
|
||||
t.Log(" Object Lock retention applied successfully")
|
||||
|
||||
// Verify retention allows simple DELETE (creates delete marker) but blocks version deletion
|
||||
// AWS S3 behavior: Simple DELETE (without version ID) is ALWAYS allowed and creates delete marker
|
||||
// Verify retention blocks simple DELETE (COMPLIANCE mode is strict WORM)
|
||||
// AWS S3 behavior: Simple DELETE (without version ID) is blocked by COMPLIANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker (AWS S3 behavior)")
|
||||
t.Log(" Simple DELETE succeeded (creates delete marker - correct AWS behavior)")
|
||||
require.Error(t, err, "Simple DELETE should be blocked by COMPLIANCE retention (AWS S3 behavior)")
|
||||
t.Log(" Simple DELETE correctly blocked by COMPLIANCE retention")
|
||||
|
||||
// Now verify that DELETE with version ID is properly blocked by retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
|
||||
@@ -329,12 +329,12 @@ func TestRetentionModeCompliance(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, types.ObjectLockRetentionModeCompliance, retentionResp.Retention.Mode)
|
||||
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
// Try simple DELETE - should fail for COMPLIANCE mode (strict WORM)
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
require.Error(t, err, "Simple DELETE should be blocked by COMPLIANCE retention")
|
||||
|
||||
// Try DELETE with version ID - should fail for COMPLIANCE mode
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
@@ -388,12 +388,12 @@ func TestLegalHoldWorkflow(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, types.ObjectLockLegalHoldStatusOn, legalHoldResp.LegalHold.Status)
|
||||
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
// Try simple DELETE - should fail due to legal hold
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
require.Error(t, err, "Simple DELETE should be blocked by legal hold")
|
||||
|
||||
// Try DELETE with version ID - should fail due to legal hold
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
@@ -591,12 +591,12 @@ func TestRetentionAndLegalHoldCombination(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
// Try simple DELETE - should fail due to legal hold + COMPLIANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
require.Error(t, err, "Simple DELETE should be blocked by legal hold")
|
||||
|
||||
// Try DELETE with version ID and bypass - should still fail due to legal hold
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
@@ -607,7 +607,7 @@ func TestRetentionAndLegalHoldCombination(t *testing.T) {
|
||||
})
|
||||
require.Error(t, err, "Legal hold should prevent deletion even with governance bypass")
|
||||
|
||||
// Remove legal hold (must specify version ID since latest version is now delete marker)
|
||||
// Remove legal hold (must specify version ID)
|
||||
_, err = client.PutObjectLegalHold(context.TODO(), &s3.PutObjectLegalHoldInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
|
||||
@@ -42,12 +42,12 @@ func TestWORMRetentionIntegration(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
// Try simple DELETE - should fail due to GOVERNANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
require.Error(t, err, "Simple DELETE should be blocked by GOVERNANCE retention")
|
||||
|
||||
// Try DELETE with version ID - should fail due to GOVERNANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
@@ -325,12 +325,12 @@ func TestRetentionWithMultipartUpload(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try simple DELETE - should succeed and create delete marker (AWS S3 behavior)
|
||||
// Try simple DELETE - should fail due to GOVERNANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucketName),
|
||||
Key: aws.String(key),
|
||||
})
|
||||
require.NoError(t, err, "Simple DELETE should succeed and create delete marker")
|
||||
require.Error(t, err, "Simple DELETE should be blocked by GOVERNANCE retention")
|
||||
|
||||
// Try DELETE with version ID - should fail due to GOVERNANCE retention
|
||||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
||||
|
||||
@@ -1127,10 +1127,3 @@ Update this section during implementation:
|
||||
- Profiles covered: P1.
|
||||
- Gaps introduced/remaining: `499` cancellation and transport-interruption branches remain pending.
|
||||
- Commit: `d1e5f390a`
|
||||
|
||||
- Date: 2026-02-15
|
||||
- Change: Added Rust volume-server migration bootstrap wiring.
|
||||
- APIs covered: Harness now supports Go master + selectable volume binary (`VOLUME_SERVER_IMPL=rust` or `VOLUME_SERVER_BINARY`), with Rust-mode smoke coverage for representative HTTP/gRPC integration tests.
|
||||
- Profiles covered: P1 smoke and default Go matrix unchanged.
|
||||
- Gaps introduced/remaining: native Rust handler implementations are tracked in `/Users/chris/dev/seaweedfs2/rust/volume_server/DEV_PLAN.md`; current Rust mode is a compatibility launcher phase.
|
||||
- Commit: pending
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
.PHONY: test-volume-server test-volume-server-short test-volume-server-rust-smoke
|
||||
.PHONY: test-volume-server test-volume-server-short
|
||||
|
||||
test-volume-server:
|
||||
go test ./test/volume_server/... -v
|
||||
|
||||
test-volume-server-short:
|
||||
go test ./test/volume_server/... -short -v
|
||||
|
||||
test-volume-server-rust-smoke:
|
||||
VOLUME_SERVER_IMPL=rust go test ./test/volume_server/http ./test/volume_server/grpc -run 'TestAdminStatusAndHealthz|TestUploadReadRangeHeadDeleteRoundTrip|TestStateAndStatusRPCs|TestVolumeSyncStatusAndReadVolumeFileStatus' -v
|
||||
|
||||
@@ -15,9 +15,6 @@ If a `weed` binary is not found, the harness will build one automatically.
|
||||
## Optional environment variables
|
||||
|
||||
- `WEED_BINARY`: explicit path to the `weed` executable (disables auto-build).
|
||||
- `VOLUME_SERVER_IMPL`: select volume-server implementation (`go` default, `rust` for Rust launcher mode).
|
||||
- `VOLUME_SERVER_BINARY`: explicit path to the volume-server executable (overrides `VOLUME_SERVER_IMPL`).
|
||||
- `VOLUME_SERVER_RUST_REBUILD=1`: force rebuild of Rust volume-server binary in Rust mode.
|
||||
- `VOLUME_SERVER_IT_KEEP_LOGS=1`: keep temporary test directories and process logs.
|
||||
|
||||
## Current scope (Phase 0)
|
||||
|
||||
@@ -32,12 +32,11 @@ type Cluster struct {
|
||||
testingTB testing.TB
|
||||
profile matrix.Profile
|
||||
|
||||
weedBinary string
|
||||
volumeBinary string
|
||||
baseDir string
|
||||
configDir string
|
||||
logsDir string
|
||||
keepLogs bool
|
||||
weedBinary string
|
||||
baseDir string
|
||||
configDir string
|
||||
logsDir string
|
||||
keepLogs bool
|
||||
|
||||
masterPort int
|
||||
masterGrpcPort int
|
||||
@@ -55,9 +54,9 @@ type Cluster struct {
|
||||
func StartSingleVolumeCluster(t testing.TB, profile matrix.Profile) *Cluster {
|
||||
t.Helper()
|
||||
|
||||
weedBinary, volumeBinary, err := FindOrBuildServerBinaries()
|
||||
weedBinary, err := FindOrBuildWeedBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve server binaries: %v", err)
|
||||
t.Fatalf("resolve weed binary: %v", err)
|
||||
}
|
||||
|
||||
baseDir, keepLogs, err := newWorkDir()
|
||||
@@ -93,7 +92,6 @@ func StartSingleVolumeCluster(t testing.TB, profile matrix.Profile) *Cluster {
|
||||
testingTB: t,
|
||||
profile: profile,
|
||||
weedBinary: weedBinary,
|
||||
volumeBinary: volumeBinary,
|
||||
baseDir: baseDir,
|
||||
configDir: configDir,
|
||||
logsDir: logsDir,
|
||||
@@ -209,7 +207,7 @@ func (c *Cluster) startVolume(dataDir string) error {
|
||||
args = append(args, "-inflightDownloadDataTimeout="+c.profile.InflightDownloadTimeout.String())
|
||||
}
|
||||
|
||||
c.volumeCmd = exec.Command(c.volumeBinary, args...)
|
||||
c.volumeCmd = exec.Command(c.weedBinary, args...)
|
||||
c.volumeCmd.Dir = c.baseDir
|
||||
c.volumeCmd.Stdout = logFile
|
||||
c.volumeCmd.Stderr = logFile
|
||||
@@ -343,12 +341,17 @@ func FindOrBuildWeedBinary() (string, error) {
|
||||
return "", fmt.Errorf("WEED_BINARY is set but not executable: %s", fromEnv)
|
||||
}
|
||||
|
||||
repoRoot, err := detectRepoRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
repoRoot := ""
|
||||
if _, file, _, ok := runtime.Caller(0); ok {
|
||||
repoRoot = filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", ".."))
|
||||
candidate := filepath.Join(repoRoot, "weed", "weed")
|
||||
if isExecutableFile(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
if candidate := filepath.Join(repoRoot, "weed", "weed"); isExecutableFile(candidate) {
|
||||
return candidate, nil
|
||||
|
||||
if repoRoot == "" {
|
||||
return "", errors.New("unable to detect repository root")
|
||||
}
|
||||
|
||||
binDir := filepath.Join(os.TempDir(), "seaweedfs_volume_server_it_bin")
|
||||
@@ -374,82 +377,6 @@ func FindOrBuildWeedBinary() (string, error) {
|
||||
return binPath, nil
|
||||
}
|
||||
|
||||
// FindOrBuildServerBinaries returns master and volume executables.
|
||||
// Master always runs from the Go weed binary; volume can be switched via env.
|
||||
func FindOrBuildServerBinaries() (masterBinary string, volumeBinary string, err error) {
|
||||
masterBinary, err = FindOrBuildWeedBinary()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
volumeBinary, err = FindOrBuildVolumeServerBinary(masterBinary)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return masterBinary, volumeBinary, nil
|
||||
}
|
||||
|
||||
// FindOrBuildVolumeServerBinary resolves the executable used for volume-server processes.
|
||||
//
|
||||
// Behavior:
|
||||
// - `VOLUME_SERVER_BINARY=/path/to/bin`: use explicit executable path.
|
||||
// - `VOLUME_SERVER_IMPL=rust`: build/use Rust volume server launcher.
|
||||
// - default: use the same Go `weed` binary.
|
||||
func FindOrBuildVolumeServerBinary(defaultBinary string) (string, error) {
|
||||
if fromEnv := os.Getenv("VOLUME_SERVER_BINARY"); fromEnv != "" {
|
||||
if isExecutableFile(fromEnv) {
|
||||
return fromEnv, nil
|
||||
}
|
||||
return "", fmt.Errorf("VOLUME_SERVER_BINARY is set but not executable: %s", fromEnv)
|
||||
}
|
||||
|
||||
impl := strings.ToLower(strings.TrimSpace(os.Getenv("VOLUME_SERVER_IMPL")))
|
||||
if impl == "" || impl == "go" {
|
||||
return defaultBinary, nil
|
||||
}
|
||||
if impl != "rust" {
|
||||
return "", fmt.Errorf("unsupported VOLUME_SERVER_IMPL %q (supported: go, rust)", impl)
|
||||
}
|
||||
|
||||
repoRoot, err := detectRepoRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return FindOrBuildRustVolumeServerBinary(repoRoot)
|
||||
}
|
||||
|
||||
// FindOrBuildRustVolumeServerBinary builds the Rust volume server launcher when needed.
|
||||
func FindOrBuildRustVolumeServerBinary(repoRoot string) (string, error) {
|
||||
manifestPath := filepath.Join(repoRoot, "rust", "volume_server", "Cargo.toml")
|
||||
if _, err := os.Stat(manifestPath); err != nil {
|
||||
return "", fmt.Errorf("rust volume server manifest not found at %s: %w", manifestPath, err)
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(os.TempDir(), "seaweedfs_volume_server_it_rust_target")
|
||||
binPath := filepath.Join(targetDir, "release", "weed-volume-rs")
|
||||
if isExecutableFile(binPath) && os.Getenv("VOLUME_SERVER_RUST_REBUILD") != "1" {
|
||||
return binPath, nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("cargo", "build", "--release", "--manifest-path", manifestPath, "--target-dir", targetDir)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("build rust volume server binary: %w\n%s", err, out.String())
|
||||
}
|
||||
if !isExecutableFile(binPath) {
|
||||
return "", fmt.Errorf("built rust volume server binary is not executable: %s", binPath)
|
||||
}
|
||||
return binPath, nil
|
||||
}
|
||||
|
||||
func detectRepoRoot() (string, error) {
|
||||
if _, file, _, ok := runtime.Caller(0); ok {
|
||||
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "..")), nil
|
||||
}
|
||||
return "", errors.New("unable to detect repository root")
|
||||
}
|
||||
|
||||
func isExecutableFile(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
|
||||
@@ -17,12 +17,11 @@ type DualVolumeCluster struct {
|
||||
testingTB testing.TB
|
||||
profile matrix.Profile
|
||||
|
||||
weedBinary string
|
||||
volumeBinary string
|
||||
baseDir string
|
||||
configDir string
|
||||
logsDir string
|
||||
keepLogs bool
|
||||
weedBinary string
|
||||
baseDir string
|
||||
configDir string
|
||||
logsDir string
|
||||
keepLogs bool
|
||||
|
||||
masterPort int
|
||||
masterGrpcPort int
|
||||
@@ -34,7 +33,7 @@ type DualVolumeCluster struct {
|
||||
volumeGrpcPort1 int
|
||||
volumePubPort1 int
|
||||
|
||||
masterCmd *exec.Cmd
|
||||
masterCmd *exec.Cmd
|
||||
volumeCmd0 *exec.Cmd
|
||||
volumeCmd1 *exec.Cmd
|
||||
|
||||
@@ -44,9 +43,9 @@ type DualVolumeCluster struct {
|
||||
func StartDualVolumeCluster(t testing.TB, profile matrix.Profile) *DualVolumeCluster {
|
||||
t.Helper()
|
||||
|
||||
weedBinary, volumeBinary, err := FindOrBuildServerBinaries()
|
||||
weedBinary, err := FindOrBuildWeedBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("resolve server binaries: %v", err)
|
||||
t.Fatalf("resolve weed binary: %v", err)
|
||||
}
|
||||
|
||||
baseDir, keepLogs, err := newWorkDir()
|
||||
@@ -80,22 +79,21 @@ func StartDualVolumeCluster(t testing.TB, profile matrix.Profile) *DualVolumeClu
|
||||
}
|
||||
|
||||
c := &DualVolumeCluster{
|
||||
testingTB: t,
|
||||
profile: profile,
|
||||
weedBinary: weedBinary,
|
||||
volumeBinary: volumeBinary,
|
||||
baseDir: baseDir,
|
||||
configDir: configDir,
|
||||
logsDir: logsDir,
|
||||
keepLogs: keepLogs,
|
||||
masterPort: masterPort,
|
||||
masterGrpcPort: masterGrpcPort,
|
||||
volumePort0: ports[0],
|
||||
testingTB: t,
|
||||
profile: profile,
|
||||
weedBinary: weedBinary,
|
||||
baseDir: baseDir,
|
||||
configDir: configDir,
|
||||
logsDir: logsDir,
|
||||
keepLogs: keepLogs,
|
||||
masterPort: masterPort,
|
||||
masterGrpcPort: masterGrpcPort,
|
||||
volumePort0: ports[0],
|
||||
volumeGrpcPort0: ports[1],
|
||||
volumePubPort0: ports[0],
|
||||
volumePort1: ports[2],
|
||||
volumePubPort0: ports[0],
|
||||
volumePort1: ports[2],
|
||||
volumeGrpcPort1: ports[3],
|
||||
volumePubPort1: ports[2],
|
||||
volumePubPort1: ports[2],
|
||||
}
|
||||
if profile.SplitPublicPort {
|
||||
c.volumePubPort0 = ports[4]
|
||||
@@ -229,7 +227,7 @@ func (c *DualVolumeCluster) startVolume(index int, dataDir string) error {
|
||||
args = append(args, "-inflightDownloadDataTimeout="+c.profile.InflightDownloadTimeout.String())
|
||||
}
|
||||
|
||||
cmd := exec.Command(c.volumeBinary, args...)
|
||||
cmd := exec.Command(c.weedBinary, args...)
|
||||
cmd.Dir = c.baseDir
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/maintenance"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/plugin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/cluster"
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
@@ -109,9 +108,6 @@ type AdminServer struct {
|
||||
// Worker gRPC server
|
||||
workerGrpcServer *WorkerGrpcServer
|
||||
|
||||
// Plugin system manager
|
||||
PluginManager *plugin.Manager
|
||||
|
||||
// Collection statistics caching
|
||||
collectionStatsCache map[string]collectionStats
|
||||
lastCollectionStatsUpdate time.Time
|
||||
|
||||
@@ -26,7 +26,6 @@ type AdminHandlers struct {
|
||||
maintenanceHandlers *MaintenanceHandlers
|
||||
mqHandlers *MessageQueueHandlers
|
||||
serviceAccountHandlers *ServiceAccountHandlers
|
||||
pluginHandlers *PluginHandlers
|
||||
}
|
||||
|
||||
// NewAdminHandlers creates a new instance of AdminHandlers
|
||||
@@ -39,7 +38,6 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
maintenanceHandlers := NewMaintenanceHandlers(adminServer)
|
||||
mqHandlers := NewMessageQueueHandlers(adminServer)
|
||||
serviceAccountHandlers := NewServiceAccountHandlers(adminServer)
|
||||
pluginHandlers := NewPluginHandlers(adminServer)
|
||||
return &AdminHandlers{
|
||||
adminServer: adminServer,
|
||||
authHandlers: authHandlers,
|
||||
@@ -50,7 +48,6 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
|
||||
maintenanceHandlers: maintenanceHandlers,
|
||||
mqHandlers: mqHandlers,
|
||||
serviceAccountHandlers: serviceAccountHandlers,
|
||||
pluginHandlers: pluginHandlers,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,11 +128,6 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
|
||||
protected.POST("/maintenance/config/:taskType", dash.RequireWriteAccess(), h.maintenanceHandlers.UpdateTaskConfig)
|
||||
protected.GET("/maintenance/tasks/:id", h.maintenanceHandlers.ShowTaskDetail)
|
||||
|
||||
// Plugin system routes
|
||||
protected.GET("/plugins", h.pluginHandlers.ShowPlugins)
|
||||
protected.GET("/plugins/jobs/:jobType", h.pluginHandlers.ShowPluginJobs)
|
||||
protected.GET("/plugins/config/:jobType", h.pluginHandlers.ShowPluginConfig)
|
||||
|
||||
// API routes for AJAX calls
|
||||
api := r.Group("/api")
|
||||
api.Use(dash.RequireAuthAPI()) // Use API-specific auth middleware
|
||||
@@ -250,19 +242,6 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser,
|
||||
maintenanceApi.PUT("/config", dash.RequireWriteAccess(), h.adminServer.UpdateMaintenanceConfigAPI)
|
||||
}
|
||||
|
||||
// Plugin system API routes
|
||||
pluginApi := api.Group("/plugin")
|
||||
{
|
||||
pluginApi.GET("/list", h.pluginHandlers.ListPluginsAPI)
|
||||
pluginApi.GET("/jobs/:jobType", h.pluginHandlers.ListJobsAPI)
|
||||
pluginApi.GET("/config/:jobType", h.pluginHandlers.GetConfigAPI)
|
||||
pluginApi.POST("/config/:jobType/apply", dash.RequireWriteAccess(), h.pluginHandlers.SaveConfigAPI)
|
||||
pluginApi.GET("/detection/history/:jobType", h.pluginHandlers.GetDetectionHistoryAPI)
|
||||
pluginApi.GET("/execution/history/:jobType", h.pluginHandlers.GetExecutionHistoryAPI)
|
||||
pluginApi.POST("/jobs/:jobType/trigger-detection", dash.RequireWriteAccess(), h.pluginHandlers.TriggerDetectionAPI)
|
||||
pluginApi.POST("/jobs/:jobID/cancel", dash.RequireWriteAccess(), h.pluginHandlers.CancelJobAPI)
|
||||
}
|
||||
|
||||
// Message Queue API routes
|
||||
mqApi := api.Group("/mq")
|
||||
{
|
||||
|
||||
@@ -1,533 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/plugin"
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// PluginHandlers handles HTTP requests for the plugin system
|
||||
type PluginHandlers struct {
|
||||
adminServer *dash.AdminServer
|
||||
}
|
||||
|
||||
// NewPluginHandlers creates a new plugin handlers instance
|
||||
func NewPluginHandlers(adminServer *dash.AdminServer) *PluginHandlers {
|
||||
return &PluginHandlers{
|
||||
adminServer: adminServer,
|
||||
}
|
||||
}
|
||||
|
||||
// ShowPlugins renders the main plugins page
|
||||
func (h *PluginHandlers) ShowPlugins(c *gin.Context) {
|
||||
data := h.getPluginsData()
|
||||
html := app.PluginsPage(data)
|
||||
c.Header("Content-Type", "text/html")
|
||||
html.Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// ShowPluginJobs renders the job monitoring page
|
||||
func (h *PluginHandlers) ShowPluginJobs(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
data := h.getJobsData(jobType)
|
||||
html := app.PluginJobsPage(data)
|
||||
c.Header("Content-Type", "text/html")
|
||||
html.Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// ShowPluginConfig renders the plugin configuration page
|
||||
func (h *PluginHandlers) ShowPluginConfig(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
data := h.getPluginConfigData(jobType)
|
||||
html := app.PluginConfigPage(data)
|
||||
c.Header("Content-Type", "text/html")
|
||||
html.Render(c.Request.Context(), c.Writer)
|
||||
}
|
||||
|
||||
// ListPluginsAPI returns JSON list of registered plugins
|
||||
func (h *PluginHandlers) ListPluginsAPI(c *gin.Context) {
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
plugins := h.adminServer.PluginManager.ListConnectedPlugins()
|
||||
|
||||
// Convert to API response
|
||||
var pluginList []map[string]interface{}
|
||||
for _, p := range plugins {
|
||||
pluginList = append(pluginList, map[string]interface{}{
|
||||
"id": p.ID,
|
||||
"name": p.Name,
|
||||
"version": p.Version,
|
||||
"protocol_version": p.ProtocolVersion,
|
||||
"connected_at": p.ConnectedAt,
|
||||
"last_heartbeat": p.LastHeartbeat,
|
||||
"state": p.State.String(),
|
||||
"healthy": p.IsHealthy(),
|
||||
"pending_jobs": p.PendingJobs,
|
||||
"running_jobs": p.RunningJobs,
|
||||
"cpu_usage": p.CPUUsagePercent,
|
||||
"memory_usage_mb": p.MemoryUsageMB,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"plugins": pluginList})
|
||||
}
|
||||
|
||||
// ListJobsAPI returns JSON list of jobs for a job type
|
||||
func (h *PluginHandlers) ListJobsAPI(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
stateStr := c.DefaultQuery("state", "")
|
||||
limit := 100
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
var state plugin.JobState
|
||||
if stateStr != "" {
|
||||
// Parse state string
|
||||
switch stateStr {
|
||||
case "pending":
|
||||
state = plugin.JobStatePending
|
||||
case "running":
|
||||
state = plugin.JobStateRunning
|
||||
case "completed":
|
||||
state = plugin.JobStateCompleted
|
||||
case "failed":
|
||||
state = plugin.JobStateFailed
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
jobs := h.adminServer.PluginManager.ListJobs(jobType, state)
|
||||
if limit > 0 && len(jobs) > limit {
|
||||
jobs = jobs[:limit]
|
||||
}
|
||||
|
||||
var jobList []map[string]interface{}
|
||||
for _, j := range jobs {
|
||||
jobList = append(jobList, map[string]interface{}{
|
||||
"id": j.ID,
|
||||
"type": j.Type,
|
||||
"description": j.Description,
|
||||
"state": j.State.String(),
|
||||
"created_at": j.CreatedAt,
|
||||
"updated_at": j.UpdatedAt,
|
||||
"executor_id": j.ExecutorID,
|
||||
"progress": j.ProgressPercent,
|
||||
"retries": j.Retries,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"jobs": jobList, "job_type": jobType})
|
||||
}
|
||||
|
||||
// GetConfigAPI returns the configuration for a job type
|
||||
func (h *PluginHandlers) GetConfigAPI(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
config, err := h.adminServer.PluginManager.GetConfig(jobType)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("config not found: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
adminConfigMap := convertConfigFieldsToMap(config.AdminConfig)
|
||||
workerConfigMap := convertConfigFieldsToMap(config.WorkerConfig)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"job_type": jobType,
|
||||
"enabled": config.Enabled,
|
||||
"admin_config": adminConfigMap,
|
||||
"worker_config": workerConfigMap,
|
||||
"created_at": config.CreatedAt,
|
||||
"updated_at": config.UpdatedAt,
|
||||
"created_by": config.CreatedBy,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveConfigAPI saves the configuration for a job type
|
||||
func (h *PluginHandlers) SaveConfigAPI(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AdminConfig map[string]interface{} `json:"admin_config"`
|
||||
WorkerConfig map[string]interface{} `json:"worker_config"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid request: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert maps back to config fields
|
||||
adminConfigFields := convertMapToConfigFields(req.AdminConfig)
|
||||
workerConfigFields := convertMapToConfigFields(req.WorkerConfig)
|
||||
|
||||
err := h.adminServer.PluginManager.SaveConfig(jobType, req.Enabled, adminConfigFields, workerConfigFields)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to save config: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "configuration saved"})
|
||||
}
|
||||
|
||||
// GetDetectionHistoryAPI returns detection history for a job type
|
||||
func (h *PluginHandlers) GetDetectionHistoryAPI(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
records := h.adminServer.PluginManager.GetDetectionHistory(jobType, limit)
|
||||
|
||||
var historyList []map[string]interface{}
|
||||
for _, rec := range records {
|
||||
historyList = append(historyList, map[string]interface{}{
|
||||
"timestamp": rec.Timestamp,
|
||||
"duration_ms": rec.DurationMs,
|
||||
"jobs_detected": rec.JobsDetected,
|
||||
"status": rec.Status,
|
||||
"error": rec.Error,
|
||||
"worker_id": rec.WorkerID,
|
||||
"detailed_logs": rec.DetailedLogs,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"job_type": jobType,
|
||||
"history": historyList,
|
||||
"count": len(historyList),
|
||||
})
|
||||
}
|
||||
|
||||
// GetExecutionHistoryAPI returns execution history for a job type
|
||||
func (h *PluginHandlers) GetExecutionHistoryAPI(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
limitStr := c.DefaultQuery("limit", "10")
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit > 100 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
records := h.adminServer.PluginManager.GetExecutionHistory(jobType, limit)
|
||||
|
||||
var historyList []map[string]interface{}
|
||||
for _, rec := range records {
|
||||
historyList = append(historyList, map[string]interface{}{
|
||||
"job_id": rec.JobID,
|
||||
"job_type": rec.JobType,
|
||||
"started_at": rec.StartedAt,
|
||||
"completed_at": rec.CompletedAt,
|
||||
"duration_ms": rec.DurationMs,
|
||||
"success": rec.Success,
|
||||
"error": rec.Error,
|
||||
"worker_id": rec.WorkerID,
|
||||
"items_processed": rec.ItemsProcessed,
|
||||
"execution_logs": rec.ExecutionLogs,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"job_type": jobType,
|
||||
"history": historyList,
|
||||
"count": len(historyList),
|
||||
})
|
||||
}
|
||||
|
||||
// TriggerDetectionAPI manually triggers detection for a job type
|
||||
func (h *PluginHandlers) TriggerDetectionAPI(c *gin.Context) {
|
||||
jobType := c.Param("jobType")
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.PluginManager.TriggerDetection(jobType)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("detection failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "detection triggered"})
|
||||
}
|
||||
|
||||
// CancelJobAPI cancels a running job
|
||||
func (h *PluginHandlers) CancelJobAPI(c *gin.Context) {
|
||||
jobID := c.Param("jobID")
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "plugin manager not available"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.adminServer.PluginManager.CancelJob(jobID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("cancel failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "job cancelled"})
|
||||
}
|
||||
|
||||
// getPluginsData prepares data for the main plugins page
|
||||
func (h *PluginHandlers) getPluginsData() map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"plugins": []map[string]interface{}{},
|
||||
"job_types": []string{},
|
||||
"total_plugins": 0,
|
||||
"total_jobs": 0,
|
||||
}
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
return data
|
||||
}
|
||||
|
||||
// Get connected plugins
|
||||
plugins := h.adminServer.PluginManager.ListConnectedPlugins()
|
||||
var pluginList []map[string]interface{}
|
||||
for _, p := range plugins {
|
||||
pluginList = append(pluginList, map[string]interface{}{
|
||||
"id": p.ID,
|
||||
"name": p.Name,
|
||||
"version": p.Version,
|
||||
"connected_at": p.ConnectedAt.Format(time.RFC3339),
|
||||
"state": p.State.String(),
|
||||
"healthy": p.IsHealthy(),
|
||||
"pending_jobs": p.PendingJobs,
|
||||
"running_jobs": p.RunningJobs,
|
||||
"cpu_usage": fmt.Sprintf("%.1f%%", p.CPUUsagePercent),
|
||||
"memory_usage": fmt.Sprintf("%.1f MB", p.MemoryUsageMB),
|
||||
})
|
||||
}
|
||||
|
||||
// Get registered job types
|
||||
jobTypes := h.adminServer.PluginManager.ListJobTypes()
|
||||
sort.Strings(jobTypes) // Sort alphabetically
|
||||
|
||||
// Count jobs
|
||||
totalJobs := 0
|
||||
for _, jobType := range jobTypes {
|
||||
jobs := h.adminServer.PluginManager.ListJobs(jobType, 0)
|
||||
totalJobs += len(jobs)
|
||||
}
|
||||
|
||||
data["plugins"] = pluginList
|
||||
data["job_types"] = jobTypes
|
||||
data["total_plugins"] = len(plugins)
|
||||
data["total_jobs"] = totalJobs
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// getJobsData prepares data for the job monitoring page
|
||||
func (h *PluginHandlers) getJobsData(jobType string) map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"job_type": jobType,
|
||||
"jobs": []map[string]interface{}{},
|
||||
"pending_count": 0,
|
||||
"running_count": 0,
|
||||
"completed_count": 0,
|
||||
"failed_count": 0,
|
||||
}
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
return data
|
||||
}
|
||||
|
||||
// Get all jobs for this type
|
||||
allJobs := h.adminServer.PluginManager.ListJobs(jobType, 0)
|
||||
|
||||
var jobsList []map[string]interface{}
|
||||
pendingCount, runningCount, completedCount, failedCount := 0, 0, 0, 0
|
||||
|
||||
for _, job := range allJobs {
|
||||
jobsList = append(jobsList, map[string]interface{}{
|
||||
"id": job.ID,
|
||||
"description": job.Description,
|
||||
"state": job.State.String(),
|
||||
"created_at": job.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": job.UpdatedAt.Format(time.RFC3339),
|
||||
"progress": job.ProgressPercent,
|
||||
"executor_id": job.ExecutorID,
|
||||
"retries": job.Retries,
|
||||
})
|
||||
|
||||
switch job.State {
|
||||
case plugin.JobStatePending:
|
||||
pendingCount++
|
||||
case plugin.JobStateRunning:
|
||||
runningCount++
|
||||
case plugin.JobStateCompleted:
|
||||
completedCount++
|
||||
case plugin.JobStateFailed:
|
||||
failedCount++
|
||||
}
|
||||
}
|
||||
|
||||
data["jobs"] = jobsList
|
||||
data["pending_count"] = pendingCount
|
||||
data["running_count"] = runningCount
|
||||
data["completed_count"] = completedCount
|
||||
data["failed_count"] = failedCount
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// getPluginConfigData prepares data for the plugin configuration page
|
||||
func (h *PluginHandlers) getPluginConfigData(jobType string) map[string]interface{} {
|
||||
data := map[string]interface{}{
|
||||
"job_type": jobType,
|
||||
"enabled": false,
|
||||
"admin_config": map[string]interface{}{},
|
||||
"worker_config": map[string]interface{}{},
|
||||
"schema": map[string]interface{}{},
|
||||
"detection_history": []map[string]interface{}{},
|
||||
"execution_history": []map[string]interface{}{},
|
||||
}
|
||||
|
||||
if h.adminServer.PluginManager == nil {
|
||||
return data
|
||||
}
|
||||
|
||||
// Get config
|
||||
config, err := h.adminServer.PluginManager.GetConfig(jobType)
|
||||
if err == nil && config != nil {
|
||||
data["enabled"] = config.Enabled
|
||||
data["admin_config"] = convertConfigFieldsToMap(config.AdminConfig)
|
||||
data["worker_config"] = convertConfigFieldsToMap(config.WorkerConfig)
|
||||
}
|
||||
|
||||
// Get schema
|
||||
// detector, err := h.adminServer.PluginManager.GetDetectorForJobType(jobType)
|
||||
// if err == nil && detector != nil {
|
||||
// data["schema"] = detector.ConfigSchema // Will be extended later
|
||||
// }
|
||||
|
||||
// Get detection history
|
||||
detectionRecords := h.adminServer.PluginManager.GetDetectionHistory(jobType, 10)
|
||||
var detectionList []map[string]interface{}
|
||||
for _, rec := range detectionRecords {
|
||||
detectionList = append(detectionList, map[string]interface{}{
|
||||
"timestamp": rec.Timestamp.Format(time.RFC3339),
|
||||
"duration_ms": rec.DurationMs,
|
||||
"jobs_detected": rec.JobsDetected,
|
||||
"status": rec.Status,
|
||||
"error": rec.Error,
|
||||
"detailed_logs": rec.DetailedLogs,
|
||||
})
|
||||
}
|
||||
data["detection_history"] = detectionList
|
||||
|
||||
// Get execution history
|
||||
executionRecords := h.adminServer.PluginManager.GetExecutionHistory(jobType, 10)
|
||||
var executionList []map[string]interface{}
|
||||
for _, rec := range executionRecords {
|
||||
executionList = append(executionList, map[string]interface{}{
|
||||
"job_id": rec.JobID,
|
||||
"started_at": rec.StartedAt.Format(time.RFC3339),
|
||||
"completed_at": rec.CompletedAt.Format(time.RFC3339),
|
||||
"duration_ms": rec.DurationMs,
|
||||
"success": rec.Success,
|
||||
"error": rec.Error,
|
||||
"items_processed": rec.ItemsProcessed,
|
||||
})
|
||||
}
|
||||
data["execution_history"] = executionList
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Helper functions to convert between data formats
|
||||
|
||||
func convertConfigFieldsToMap(fields []*plugin_pb.ConfigFieldValue) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, field := range fields {
|
||||
if field == nil {
|
||||
continue
|
||||
}
|
||||
if field.StringValue != "" {
|
||||
result[field.FieldName] = field.StringValue
|
||||
} else if field.IntValue != 0 {
|
||||
result[field.FieldName] = field.IntValue
|
||||
} else if field.FloatValue != 0 {
|
||||
result[field.FieldName] = field.FloatValue
|
||||
} else if field.BoolValue {
|
||||
result[field.FieldName] = field.BoolValue
|
||||
} else if field.JsonValue != "" {
|
||||
result[field.FieldName] = field.JsonValue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertMapToConfigFields(m map[string]interface{}) []*plugin_pb.ConfigFieldValue {
|
||||
var fields []*plugin_pb.ConfigFieldValue
|
||||
for k, v := range m {
|
||||
field := &plugin_pb.ConfigFieldValue{FieldName: k}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
field.StringValue = val
|
||||
case float64:
|
||||
// Try to detect if it's an int or bool
|
||||
if val == float64(int64(val)) {
|
||||
field.IntValue = int64(val)
|
||||
} else {
|
||||
field.FloatValue = float32(val)
|
||||
}
|
||||
case bool:
|
||||
field.BoolValue = val
|
||||
case int:
|
||||
field.IntValue = int64(val)
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// convertConfigSchemaToMap converts schema to a map (placeholder for future use)
|
||||
// func convertConfigSchemaToMap(schema interface{}) map[string]interface{} {
|
||||
// // TODO: implement schema conversion when schema structure is finalized
|
||||
// return make(map[string]interface{})
|
||||
// }
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// ConfigManager handles configuration persistence and retrieval
|
||||
type ConfigManager struct {
|
||||
dataDir string
|
||||
configs map[string]*JobTypeConfig
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewConfigManager creates a new configuration manager
|
||||
func NewConfigManager(dataDir string) (*ConfigManager, error) {
|
||||
cm := &ConfigManager{
|
||||
dataDir: filepath.Join(dataDir, "plugins"),
|
||||
configs: make(map[string]*JobTypeConfig),
|
||||
}
|
||||
|
||||
// Create plugins directory if it doesn't exist
|
||||
if err := os.MkdirAll(cm.dataDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create plugin config directory: %w", err)
|
||||
}
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
// LoadConfig loads a configuration from disk
|
||||
func (cm *ConfigManager) LoadConfig(jobType string) (*JobTypeConfig, error) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
// Check cache first
|
||||
if config, exists := cm.configs[jobType]; exists {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
configPath := filepath.Join(cm.dataDir, jobType+".json")
|
||||
|
||||
// If file doesn't exist, create default config
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
config := &JobTypeConfig{
|
||||
JobType: jobType,
|
||||
Enabled: false,
|
||||
AdminConfig: make([]*plugin_pb.ConfigFieldValue, 0),
|
||||
WorkerConfig: make([]*plugin_pb.ConfigFieldValue, 0),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
CreatedBy: "system",
|
||||
}
|
||||
cm.configs[jobType] = config
|
||||
return config, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to access config file: %w", err)
|
||||
}
|
||||
|
||||
// Load from file
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var configData map[string]interface{}
|
||||
if err := json.Unmarshal(data, &configData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
config := &JobTypeConfig{
|
||||
JobType: jobType,
|
||||
AdminConfig: make([]*plugin_pb.ConfigFieldValue, 0),
|
||||
WorkerConfig: make([]*plugin_pb.ConfigFieldValue, 0),
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
if enabled, ok := configData["enabled"].(bool); ok {
|
||||
config.Enabled = enabled
|
||||
}
|
||||
|
||||
if createdBy, ok := configData["created_by"].(string); ok {
|
||||
config.CreatedBy = createdBy
|
||||
}
|
||||
|
||||
if createdAt, ok := configData["created_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
|
||||
config.CreatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAt, ok := configData["updated_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, updatedAt); err == nil {
|
||||
config.UpdatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
cm.configs[jobType] = config
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// SaveConfig saves a configuration to disk
|
||||
func (cm *ConfigManager) SaveConfig(config *JobTypeConfig) error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
config.UpdatedAt = time.Now()
|
||||
cm.configs[config.JobType] = config
|
||||
|
||||
configPath := filepath.Join(cm.dataDir, config.JobType+".json")
|
||||
|
||||
// Convert to JSON-serializable format
|
||||
configData := map[string]interface{}{
|
||||
"job_type": config.JobType,
|
||||
"enabled": config.Enabled,
|
||||
"created_by": config.CreatedBy,
|
||||
"created_at": config.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": config.UpdatedAt.Format(time.RFC3339),
|
||||
"admin_config": cm.configValuesToJSON(config.AdminConfig),
|
||||
"worker_config": cm.configValuesToJSON(config.WorkerConfig),
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(configData, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig retrieves a configuration
|
||||
func (cm *ConfigManager) GetConfig(jobType string) (*JobTypeConfig, error) {
|
||||
cm.mu.RLock()
|
||||
if config, exists := cm.configs[jobType]; exists {
|
||||
cm.mu.RUnlock()
|
||||
return config, nil
|
||||
}
|
||||
cm.mu.RUnlock()
|
||||
|
||||
// Load from disk if not in cache
|
||||
return cm.LoadConfig(jobType)
|
||||
}
|
||||
|
||||
// UpdateConfig updates a configuration
|
||||
func (cm *ConfigManager) UpdateConfig(jobType string, enabled bool, adminConfig, workerConfig []*plugin_pb.ConfigFieldValue, updatedBy string) (*JobTypeConfig, error) {
|
||||
config, err := cm.GetConfig(jobType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config.SetConfig(enabled, adminConfig, workerConfig)
|
||||
config.CreatedBy = updatedBy
|
||||
|
||||
if err := cm.SaveConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// ListConfigs lists all configurations
|
||||
func (cm *ConfigManager) ListConfigs() []*JobTypeConfig {
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
configs := make([]*JobTypeConfig, 0, len(cm.configs))
|
||||
for _, config := range cm.configs {
|
||||
configs = append(configs, config)
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
// DeleteConfig deletes a configuration
|
||||
func (cm *ConfigManager) DeleteConfig(jobType string) error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
configPath := filepath.Join(cm.dataDir, jobType+".json")
|
||||
|
||||
if err := os.Remove(configPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete config file: %w", err)
|
||||
}
|
||||
|
||||
delete(cm.configs, jobType)
|
||||
return nil
|
||||
}
|
||||
|
||||
// configValuesToJSON converts ConfigFieldValue slice to JSON
|
||||
func (cm *ConfigManager) configValuesToJSON(values []*plugin_pb.ConfigFieldValue) []map[string]interface{} {
|
||||
result := make([]map[string]interface{}, 0, len(values))
|
||||
|
||||
for _, val := range values {
|
||||
item := map[string]interface{}{
|
||||
"field_name": val.FieldName,
|
||||
}
|
||||
|
||||
// Include non-empty values
|
||||
if val.StringValue != "" {
|
||||
item["string_value"] = val.StringValue
|
||||
}
|
||||
if val.IntValue != 0 {
|
||||
item["int_value"] = val.IntValue
|
||||
}
|
||||
if val.FloatValue != 0 {
|
||||
item["float_value"] = val.FloatValue
|
||||
}
|
||||
if val.BoolValue {
|
||||
item["bool_value"] = val.BoolValue
|
||||
}
|
||||
if val.DurationValue != nil {
|
||||
item["duration_value"] = val.DurationValue.String()
|
||||
}
|
||||
if len(val.MultiselectValues) > 0 {
|
||||
item["multiselect_values"] = val.MultiselectValues
|
||||
}
|
||||
if val.JsonValue != "" {
|
||||
item["json_value"] = val.JsonValue
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// LoadAllConfigs loads all configurations from disk
|
||||
func (cm *ConfigManager) LoadAllConfigs() error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
entries, err := os.ReadDir(cm.dataDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only load .json files
|
||||
if filepath.Ext(entry.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
|
||||
jobType := entry.Name()[:len(entry.Name())-5] // Remove .json
|
||||
|
||||
configPath := filepath.Join(cm.dataDir, entry.Name())
|
||||
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var configData map[string]interface{}
|
||||
if err := json.Unmarshal(data, &configData); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
config := &JobTypeConfig{
|
||||
JobType: jobType,
|
||||
AdminConfig: make([]*plugin_pb.ConfigFieldValue, 0),
|
||||
WorkerConfig: make([]*plugin_pb.ConfigFieldValue, 0),
|
||||
}
|
||||
|
||||
// Parse fields
|
||||
if enabled, ok := configData["enabled"].(bool); ok {
|
||||
config.Enabled = enabled
|
||||
}
|
||||
|
||||
if createdBy, ok := configData["created_by"].(string); ok {
|
||||
config.CreatedBy = createdBy
|
||||
}
|
||||
|
||||
if createdAt, ok := configData["created_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
|
||||
config.CreatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAt, ok := configData["updated_at"].(string); ok {
|
||||
if t, err := time.Parse(time.RFC3339, updatedAt); err == nil {
|
||||
config.UpdatedAt = t
|
||||
}
|
||||
}
|
||||
|
||||
cm.configs[jobType] = config
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDataDir returns the data directory path
|
||||
func (cm *ConfigManager) GetDataDir() string {
|
||||
return cm.dataDir
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Dispatcher handles job detection and dispatch to executors
|
||||
type Dispatcher struct {
|
||||
registry *Registry
|
||||
queues map[string]*JobQueue
|
||||
configMgr *ConfigManager
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// Detection scheduling
|
||||
detectionIntervals map[string]time.Duration
|
||||
detectionTickers map[string]*time.Ticker
|
||||
stopChan chan struct{}
|
||||
|
||||
// Job counter for ID generation
|
||||
jobCounter uint64
|
||||
jobCounterMu sync.Mutex
|
||||
|
||||
// Active streams for execution
|
||||
executionStreams map[string]*ExecutionStream
|
||||
streamsMu sync.RWMutex
|
||||
}
|
||||
|
||||
// ExecutionStream tracks an active job execution stream
|
||||
type ExecutionStream struct {
|
||||
JobID string
|
||||
PluginID string
|
||||
Connection *grpc.ClientConn
|
||||
Context context.Context
|
||||
Cancel context.CancelFunc
|
||||
StartTime time.Time
|
||||
LastUpdate time.Time
|
||||
}
|
||||
|
||||
// NewDispatcher creates a new job dispatcher
|
||||
func NewDispatcher(registry *Registry, configMgr *ConfigManager) *Dispatcher {
|
||||
return &Dispatcher{
|
||||
registry: registry,
|
||||
queues: make(map[string]*JobQueue),
|
||||
configMgr: configMgr,
|
||||
detectionIntervals: make(map[string]time.Duration),
|
||||
detectionTickers: make(map[string]*time.Ticker),
|
||||
stopChan: make(chan struct{}),
|
||||
executionStreams: make(map[string]*ExecutionStream),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterJobType registers a job type for detection and dispatch
|
||||
func (d *Dispatcher) RegisterJobType(jobType string, detectionInterval time.Duration) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if _, exists := d.queues[jobType]; exists {
|
||||
return fmt.Errorf("job type %s already registered", jobType)
|
||||
}
|
||||
|
||||
d.queues[jobType] = NewJobQueue(jobType)
|
||||
d.detectionIntervals[jobType] = detectionInterval
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the dispatcher with detection scheduling
|
||||
func (d *Dispatcher) Start() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
for jobType, interval := range d.detectionIntervals {
|
||||
ticker := time.NewTicker(interval)
|
||||
d.detectionTickers[jobType] = ticker
|
||||
go d.detectionLoop(jobType, ticker)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the dispatcher
|
||||
func (d *Dispatcher) Stop() {
|
||||
close(d.stopChan)
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
for _, ticker := range d.detectionTickers {
|
||||
ticker.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// detectionLoop periodically runs detection for a job type
|
||||
func (d *Dispatcher) detectionLoop(jobType string, ticker *time.Ticker) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
d.runDetection(jobType)
|
||||
case <-d.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runDetection runs detection for a job type and queues detected jobs
|
||||
func (d *Dispatcher) runDetection(jobType string) {
|
||||
// Get detector plugin
|
||||
_, err := d.registry.GetDetectorForJobType(jobType)
|
||||
if err != nil {
|
||||
// No detector available, skip
|
||||
return
|
||||
}
|
||||
|
||||
// Get job type config
|
||||
config, err := d.configMgr.GetConfig(jobType)
|
||||
if err != nil || !config.Enabled {
|
||||
// Config not found or disabled, skip
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Call detector plugin's DetectJobs method
|
||||
// For now, this is a placeholder for the gRPC call
|
||||
}
|
||||
|
||||
// QueueJob adds a job to the queue
|
||||
func (d *Dispatcher) QueueJob(jobType string, req *plugin_pb.JobRequest, dedupKey string) (*Job, error) {
|
||||
d.mu.RLock()
|
||||
queue, exists := d.queues[jobType]
|
||||
d.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("job type %s not registered", jobType)
|
||||
}
|
||||
|
||||
// Generate job ID
|
||||
jobID := d.generateJobID()
|
||||
|
||||
return queue.AddJob(jobID, req, dedupKey)
|
||||
}
|
||||
|
||||
// DispatchJob assigns a job to an available executor
|
||||
func (d *Dispatcher) DispatchJob(jobType string) (*Job, *ConnectedPlugin, error) {
|
||||
d.mu.RLock()
|
||||
queue, exists := d.queues[jobType]
|
||||
d.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, nil, fmt.Errorf("job type %s not registered", jobType)
|
||||
}
|
||||
|
||||
// Get next pending job
|
||||
job, err := queue.GetPendingJob()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Get available executor
|
||||
executor, err := d.registry.GetExecutorForJobType(jobType)
|
||||
if err != nil {
|
||||
// Put job back in queue
|
||||
queue.mu.Lock()
|
||||
queue.pendingQueue = append(queue.pendingQueue, job)
|
||||
queue.mu.Unlock()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
job.ExecutorID = executor.ID
|
||||
return job, executor, nil
|
||||
}
|
||||
|
||||
// GetJob retrieves a job by ID across all queues
|
||||
func (d *Dispatcher) GetJob(jobID string) (*Job, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
for _, queue := range d.queues {
|
||||
job, err := queue.GetJob(jobID)
|
||||
if err == nil {
|
||||
return job, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("job %s not found", jobID)
|
||||
}
|
||||
|
||||
// ListJobs returns jobs of a specific type and status
|
||||
func (d *Dispatcher) ListJobs(jobType string, status JobState) []*Job {
|
||||
d.mu.RLock()
|
||||
queue, exists := d.queues[jobType]
|
||||
d.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
return queue.ListJobs(status, 0)
|
||||
}
|
||||
|
||||
// ListAllJobs returns all jobs across all queues
|
||||
func (d *Dispatcher) ListAllJobs() []*Job {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
var jobs []*Job
|
||||
for _, queue := range d.queues {
|
||||
jobs = append(jobs, queue.ListAllJobs()...)
|
||||
}
|
||||
return jobs
|
||||
}
|
||||
|
||||
// UpdateJobProgress updates job progress
|
||||
func (d *Dispatcher) UpdateJobProgress(jobID string, percent int32) error {
|
||||
job, err := d.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job.SetProgress(percent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteJob marks a job as complete
|
||||
func (d *Dispatcher) CompleteJob(jobID string, completed *plugin_pb.JobCompleted) error {
|
||||
job, err := d.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.mu.RLock()
|
||||
queue, exists := d.queues[job.Type]
|
||||
d.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("job type %s not found", job.Type)
|
||||
}
|
||||
|
||||
d.removeExecutionStream(jobID)
|
||||
return queue.CompleteJob(jobID, completed)
|
||||
}
|
||||
|
||||
// FailJob marks a job as failed
|
||||
func (d *Dispatcher) FailJob(jobID string, failed *plugin_pb.JobFailed) error {
|
||||
job, err := d.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.mu.RLock()
|
||||
queue, exists := d.queues[job.Type]
|
||||
d.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("job type %s not found", job.Type)
|
||||
}
|
||||
|
||||
d.removeExecutionStream(jobID)
|
||||
_, err = queue.FailJob(jobID, failed)
|
||||
return err
|
||||
}
|
||||
|
||||
// CancelJob cancels a job
|
||||
func (d *Dispatcher) CancelJob(jobID string) error {
|
||||
job, err := d.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.mu.RLock()
|
||||
queue, exists := d.queues[job.Type]
|
||||
d.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("job type %s not found", job.Type)
|
||||
}
|
||||
|
||||
d.removeExecutionStream(jobID)
|
||||
return queue.CancelJob(jobID)
|
||||
}
|
||||
|
||||
// RetryJob retries a failed job
|
||||
func (d *Dispatcher) RetryJob(jobID string) error {
|
||||
job, err := d.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state := job.GetState()
|
||||
if state != JobStateFailed {
|
||||
return fmt.Errorf("can only retry failed jobs, job is in %s state", state.String())
|
||||
}
|
||||
|
||||
d.mu.RLock()
|
||||
queue, exists := d.queues[job.Type]
|
||||
d.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("job type %s not found", job.Type)
|
||||
}
|
||||
|
||||
job.SetState(JobStatePending)
|
||||
job.Retries++
|
||||
queue.mu.Lock()
|
||||
queue.pendingQueue = append(queue.pendingQueue, job)
|
||||
queue.sortPendingQueue()
|
||||
queue.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateJobID generates a unique job ID
|
||||
func (d *Dispatcher) generateJobID() string {
|
||||
d.jobCounterMu.Lock()
|
||||
defer d.jobCounterMu.Unlock()
|
||||
d.jobCounter++
|
||||
return fmt.Sprintf("job-%d-%d", time.Now().Unix(), d.jobCounter)
|
||||
}
|
||||
|
||||
// RegisterExecutionStream registers a new execution stream
|
||||
func (d *Dispatcher) RegisterExecutionStream(jobID, pluginID string, ctx context.Context) *ExecutionStream {
|
||||
d.streamsMu.Lock()
|
||||
defer d.streamsMu.Unlock()
|
||||
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
stream := &ExecutionStream{
|
||||
JobID: jobID,
|
||||
PluginID: pluginID,
|
||||
Context: cancelCtx,
|
||||
Cancel: cancel,
|
||||
StartTime: time.Now(),
|
||||
LastUpdate: time.Now(),
|
||||
}
|
||||
|
||||
d.executionStreams[jobID] = stream
|
||||
return stream
|
||||
}
|
||||
|
||||
// removeExecutionStream removes an execution stream
|
||||
func (d *Dispatcher) removeExecutionStream(jobID string) {
|
||||
d.streamsMu.Lock()
|
||||
defer d.streamsMu.Unlock()
|
||||
|
||||
stream, exists := d.executionStreams[jobID]
|
||||
if exists {
|
||||
stream.Cancel()
|
||||
delete(d.executionStreams, jobID)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats returns dispatcher statistics
|
||||
func (d *Dispatcher) GetStats() map[string]interface{} {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
|
||||
queueStats := make(map[string]interface{})
|
||||
for jobType, queue := range d.queues {
|
||||
queueStats[jobType] = queue.GetStats()
|
||||
}
|
||||
|
||||
d.streamsMu.RLock()
|
||||
activeStreams := len(d.executionStreams)
|
||||
d.streamsMu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"queue_stats": queueStats,
|
||||
"active_streams": activeStreams,
|
||||
"registered_types": len(d.queues),
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerDetection manually triggers detection for a job type
|
||||
func (d *Dispatcher) TriggerDetection(jobType string, detector interface{}, config *JobTypeConfig) error {
|
||||
// Record the detection run
|
||||
record := &DetectionRecord{
|
||||
Timestamp: time.Now(),
|
||||
JobsDetected: 0,
|
||||
Status: "started",
|
||||
}
|
||||
|
||||
// TODO: Call detector to run detection
|
||||
// For now, mark as completed with no jobs detected
|
||||
record.DurationMs = 0
|
||||
record.Status = "completed"
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// GrpcServer implements the plugin gRPC service
|
||||
type GrpcServer struct {
|
||||
plugin_pb.UnimplementedPluginServiceServer
|
||||
plugin_pb.UnimplementedAdminQueryServiceServer
|
||||
plugin_pb.UnimplementedAdminCommandServiceServer
|
||||
|
||||
registry *Registry
|
||||
dispatcher *Dispatcher
|
||||
configMgr *ConfigManager
|
||||
|
||||
// Active connections
|
||||
connections map[string]*grpc.ClientStream
|
||||
connMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewGrpcServer creates a new gRPC server
|
||||
func NewGrpcServer(registry *Registry, dispatcher *Dispatcher, configMgr *ConfigManager) *GrpcServer {
|
||||
return &GrpcServer{
|
||||
registry: registry,
|
||||
dispatcher: dispatcher,
|
||||
configMgr: configMgr,
|
||||
connections: make(map[string]*grpc.ClientStream),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PluginService Implementation
|
||||
// ============================================================================
|
||||
|
||||
// Connect handles bidirectional streaming for plugin registration and lifecycle
|
||||
func (s *GrpcServer) Connect(stream grpc.BidiStreamingServer[plugin_pb.PluginMessage, plugin_pb.AdminMessage]) error {
|
||||
var pluginID string
|
||||
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
// Stream closed normally
|
||||
if pluginID != "" {
|
||||
s.registry.UnregisterPlugin(pluginID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle different message types
|
||||
switch content := msg.Content.(type) {
|
||||
case *plugin_pb.PluginMessage_Register:
|
||||
register := content.Register
|
||||
pluginID = register.PluginId
|
||||
_, _ = s.registry.RegisterPlugin(pluginID, register)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *plugin_pb.PluginMessage_Heartbeat:
|
||||
heartbeat := content.Heartbeat
|
||||
if err := s.registry.UpdateHeartbeat(heartbeat.PluginId, heartbeat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *plugin_pb.PluginMessage_StatusUpdate:
|
||||
// Handle job status updates
|
||||
statusUpdate := content.StatusUpdate
|
||||
if err := s.handleStatusUpdate(statusUpdate); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Send acknowledgment
|
||||
adminMsg := &plugin_pb.AdminMessage{
|
||||
Content: &plugin_pb.AdminMessage_AdminCommand{
|
||||
AdminCommand: &plugin_pb.AdminCommand{
|
||||
CommandType: plugin_pb.AdminCommand_RELOAD_CONFIG,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := stream.Send(adminMsg); err != nil {
|
||||
if pluginID != "" {
|
||||
s.registry.UnregisterPlugin(pluginID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteJob handles bidirectional streaming for job execution
|
||||
func (s *GrpcServer) ExecuteJob(stream grpc.BidiStreamingServer[plugin_pb.JobExecutionMessage, plugin_pb.JobProgressMessage]) error {
|
||||
var jobID string
|
||||
|
||||
for {
|
||||
execMsg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
// Stream closed normally
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jobID = execMsg.JobId
|
||||
|
||||
// Handle different execution message types
|
||||
switch content := execMsg.Content.(type) {
|
||||
case *plugin_pb.JobExecutionMessage_JobStarted:
|
||||
started := content.JobStarted
|
||||
if err := s.handleJobStarted(jobID, started); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *plugin_pb.JobExecutionMessage_Progress:
|
||||
progress := content.Progress
|
||||
if err := s.dispatcher.UpdateJobProgress(jobID, progress.ProgressPercent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *plugin_pb.JobExecutionMessage_Checkpoint:
|
||||
checkpoint := content.Checkpoint
|
||||
// Store checkpoint for resumption
|
||||
if err := s.storeCheckpoint(jobID, checkpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *plugin_pb.JobExecutionMessage_JobCompleted:
|
||||
completed := content.JobCompleted
|
||||
if err := s.dispatcher.CompleteJob(jobID, completed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *plugin_pb.JobExecutionMessage_JobFailed:
|
||||
failed := content.JobFailed
|
||||
if err := s.dispatcher.FailJob(jobID, failed); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case *plugin_pb.JobExecutionMessage_LogEntry:
|
||||
logEntry := content.LogEntry
|
||||
if err := s.storeLogEntry(jobID, logEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Send progress update
|
||||
progressMsg := &plugin_pb.JobProgressMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobProgressMessage_Update{
|
||||
Update: &plugin_pb.JobProgressUpdate{
|
||||
JobId: jobID,
|
||||
ProgressPercent: 0,
|
||||
CurrentStep: "processing",
|
||||
StatusMessage: "Processing job",
|
||||
Timestamp: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := stream.Send(progressMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AdminQueryService Implementation
|
||||
// ============================================================================
|
||||
|
||||
// GetPluginStats returns current plugin system statistics
|
||||
func (s *GrpcServer) GetPluginStats(ctx context.Context, req *emptypb.Empty) (*plugin_pb.PluginStats, error) {
|
||||
plugins := s.registry.ListPlugins()
|
||||
jobs := s.dispatcher.ListAllJobs()
|
||||
|
||||
var totalJobs, pendingJobs, runningJobs, completedJobs, failedJobs int32
|
||||
|
||||
for _, job := range jobs {
|
||||
totalJobs++
|
||||
switch job.GetState() {
|
||||
case JobStatePending:
|
||||
pendingJobs++
|
||||
case JobStateRunning:
|
||||
runningJobs++
|
||||
case JobStateCompleted:
|
||||
completedJobs++
|
||||
case JobStateFailed:
|
||||
failedJobs++
|
||||
}
|
||||
}
|
||||
|
||||
stats := &plugin_pb.PluginStats{
|
||||
TotalPlugins: int32(len(plugins)),
|
||||
ActivePlugins: int32(len(plugins)),
|
||||
TotalJobs: totalJobs,
|
||||
PendingJobs: pendingJobs,
|
||||
RunningJobs: runningJobs,
|
||||
CompletedJobs: completedJobs,
|
||||
FailedJobs: failedJobs,
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// ListPlugins returns all connected plugins
|
||||
func (s *GrpcServer) ListPlugins(ctx context.Context, req *emptypb.Empty) (*plugin_pb.PluginList, error) {
|
||||
plugins := s.registry.ListPlugins()
|
||||
pluginInfos := make([]*plugin_pb.PluginInfo, 0, len(plugins))
|
||||
|
||||
for _, p := range plugins {
|
||||
info := &plugin_pb.PluginInfo{
|
||||
PluginId: p.ID,
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
ProtocolVersion: p.ProtocolVersion,
|
||||
Healthy: p.IsHealthy(),
|
||||
Status: p.GetState().String(),
|
||||
}
|
||||
|
||||
// Add capabilities
|
||||
for _, cap := range p.Capabilities {
|
||||
info.Capabilities = append(info.Capabilities, cap)
|
||||
}
|
||||
|
||||
pluginInfos = append(pluginInfos, info)
|
||||
}
|
||||
|
||||
return &plugin_pb.PluginList{Plugins: pluginInfos}, nil
|
||||
}
|
||||
|
||||
// ListJobs returns jobs filtered by type and status
|
||||
func (s *GrpcServer) ListJobs(ctx context.Context, req *plugin_pb.ListJobsRequest) (*plugin_pb.JobList, error) {
|
||||
var state JobState
|
||||
switch req.Status {
|
||||
case plugin_pb.ListJobsRequest_PENDING:
|
||||
state = JobStatePending
|
||||
case plugin_pb.ListJobsRequest_RUNNING:
|
||||
state = JobStateRunning
|
||||
case plugin_pb.ListJobsRequest_COMPLETED:
|
||||
state = JobStateCompleted
|
||||
case plugin_pb.ListJobsRequest_FAILED:
|
||||
state = JobStateFailed
|
||||
default:
|
||||
state = JobStatePending
|
||||
}
|
||||
|
||||
jobs := s.dispatcher.ListJobs(req.JobType, state)
|
||||
|
||||
jobPbs := make([]*plugin_pb.Job, 0, len(jobs))
|
||||
for _, job := range jobs {
|
||||
jobPb := &plugin_pb.Job{
|
||||
JobId: job.ID,
|
||||
JobType: job.Type,
|
||||
Description: job.Description,
|
||||
Priority: job.Priority,
|
||||
CreatedAt: nil,
|
||||
UpdatedAt: nil,
|
||||
ExecutorPluginId: job.ExecutorID,
|
||||
ProgressPercent: job.GetProgress(),
|
||||
Config: job.Config,
|
||||
}
|
||||
jobPbs = append(jobPbs, jobPb)
|
||||
}
|
||||
|
||||
return &plugin_pb.JobList{
|
||||
Jobs: jobPbs,
|
||||
TotalCount: int32(len(jobPbs)),
|
||||
ReturnedCount: int32(len(jobPbs)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetJob returns detailed job information
|
||||
func (s *GrpcServer) GetJob(ctx context.Context, req *plugin_pb.GetJobRequest) (*plugin_pb.JobDetail, error) {
|
||||
job, err := s.dispatcher.GetJob(req.JobId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("job not found: %w", err)
|
||||
}
|
||||
|
||||
jobPb := &plugin_pb.Job{
|
||||
JobId: job.ID,
|
||||
JobType: job.Type,
|
||||
Description: job.Description,
|
||||
Priority: job.Priority,
|
||||
ExecutorPluginId: job.ExecutorID,
|
||||
ProgressPercent: job.GetProgress(),
|
||||
Config: job.Config,
|
||||
}
|
||||
|
||||
detail := &plugin_pb.JobDetail{
|
||||
Job: jobPb,
|
||||
LogEntries: job.LogEntries,
|
||||
CompletionInfo: job.CompletedInfo,
|
||||
FailureInfo: job.FailedInfo,
|
||||
CheckpointIds: job.CheckpointIDs,
|
||||
}
|
||||
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
// GetJobExecutionLog returns job execution logs
|
||||
func (s *GrpcServer) GetJobExecutionLog(req *plugin_pb.GetJobExecutionLogRequest, stream grpc.ServerStreamingServer[plugin_pb.ExecutionLogEntry]) error {
|
||||
job, err := s.dispatcher.GetJob(req.JobId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("job not found: %w", err)
|
||||
}
|
||||
|
||||
// Send log entries
|
||||
for _, entry := range job.LogEntries {
|
||||
if err := stream.Send(entry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AdminCommandService Implementation
|
||||
// ============================================================================
|
||||
|
||||
// UpdateJobTypeConfig updates configuration for a job type
|
||||
func (s *GrpcServer) UpdateJobTypeConfig(ctx context.Context, req *plugin_pb.UpdateJobTypeConfigRequest) (*plugin_pb.JobTypeConfig, error) {
|
||||
config := req.Config
|
||||
|
||||
_, err := s.configMgr.UpdateConfig(
|
||||
config.JobType,
|
||||
config.Enabled,
|
||||
config.AdminConfig,
|
||||
config.WorkerConfig,
|
||||
"admin",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update config: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// CreateJob creates a new job
|
||||
func (s *GrpcServer) CreateJob(ctx context.Context, req *plugin_pb.CreateJobRequest) (*plugin_pb.Job, error) {
|
||||
jobReq := &plugin_pb.JobRequest{
|
||||
JobType: req.JobType,
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
Config: req.Config,
|
||||
Metadata: req.Metadata,
|
||||
RequestSource: "admin",
|
||||
}
|
||||
|
||||
job, err := s.dispatcher.QueueJob(req.JobType, jobReq, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create job: %w", err)
|
||||
}
|
||||
|
||||
return &plugin_pb.Job{
|
||||
JobId: job.ID,
|
||||
JobType: job.Type,
|
||||
Description: job.Description,
|
||||
Priority: job.Priority,
|
||||
ProgressPercent: job.GetProgress(),
|
||||
Config: job.Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelJob cancels a running job
|
||||
func (s *GrpcServer) CancelJob(ctx context.Context, req *plugin_pb.CancelJobRequest) (*emptypb.Empty, error) {
|
||||
if err := s.dispatcher.CancelJob(req.JobId); err != nil {
|
||||
return nil, fmt.Errorf("failed to cancel job: %w", err)
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// RetryJob retries a failed job
|
||||
func (s *GrpcServer) RetryJob(ctx context.Context, req *plugin_pb.RetryJobRequest) (*plugin_pb.Job, error) {
|
||||
if err := s.dispatcher.RetryJob(req.JobId); err != nil {
|
||||
return nil, fmt.Errorf("failed to retry job: %w", err)
|
||||
}
|
||||
|
||||
job, err := s.dispatcher.GetJob(req.JobId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("job not found: %w", err)
|
||||
}
|
||||
|
||||
return &plugin_pb.Job{
|
||||
JobId: job.ID,
|
||||
JobType: job.Type,
|
||||
Description: job.Description,
|
||||
Priority: job.Priority,
|
||||
ProgressPercent: job.GetProgress(),
|
||||
Config: job.Config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Methods
|
||||
// ============================================================================
|
||||
|
||||
// handleStatusUpdate handles execution status updates
|
||||
func (s *GrpcServer) handleStatusUpdate(update *plugin_pb.ExecutionStatusUpdate) error {
|
||||
if err := s.dispatcher.UpdateJobProgress(update.JobId, update.ProgressPercent); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleJobStarted handles job start messages
|
||||
func (s *GrpcServer) handleJobStarted(jobID string, started *plugin_pb.JobStarted) error {
|
||||
job, err := s.dispatcher.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job.SetState(JobStateRunning)
|
||||
return nil
|
||||
}
|
||||
|
||||
// storeCheckpoint stores a job checkpoint
|
||||
func (s *GrpcServer) storeCheckpoint(jobID string, checkpoint *plugin_pb.JobCheckpoint) error {
|
||||
job, err := s.dispatcher.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job.mu.Lock()
|
||||
defer job.mu.Unlock()
|
||||
|
||||
job.CheckpointIDs = append(job.CheckpointIDs, checkpoint.CheckpointId)
|
||||
return nil
|
||||
}
|
||||
|
||||
// storeLogEntry stores a log entry for a job
|
||||
func (s *GrpcServer) storeLogEntry(jobID string, logEntry *plugin_pb.ExecutionLog) error {
|
||||
job, err := s.dispatcher.GetJob(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job.mu.Lock()
|
||||
defer job.mu.Unlock()
|
||||
|
||||
job.LogEntries = append(job.LogEntries, &plugin_pb.ExecutionLogEntry{
|
||||
Timestamp: logEntry.Timestamp,
|
||||
Level: logEntry.Level,
|
||||
Message: logEntry.Message,
|
||||
Context: logEntry.Context,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// JobQueue manages jobs for a specific job type with priority and deduplication
|
||||
type JobQueue struct {
|
||||
jobType string
|
||||
|
||||
// Active jobs by ID
|
||||
jobs map[string]*Job
|
||||
|
||||
// Priority queue
|
||||
pendingQueue []*Job
|
||||
|
||||
// Completed jobs history (keeps last 1000)
|
||||
completedHistory []*Job
|
||||
maxHistorySize int
|
||||
|
||||
// Deduplication tracking
|
||||
seenKeys map[string]bool
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// Retry configuration
|
||||
maxRetries int32
|
||||
retryBackoff time.Duration
|
||||
retryBackoffMax time.Duration
|
||||
}
|
||||
|
||||
// NewJobQueue creates a new job queue for a job type
|
||||
func NewJobQueue(jobType string) *JobQueue {
|
||||
return &JobQueue{
|
||||
jobType: jobType,
|
||||
jobs: make(map[string]*Job),
|
||||
pendingQueue: make([]*Job, 0),
|
||||
completedHistory: make([]*Job, 0),
|
||||
maxHistorySize: 1000,
|
||||
seenKeys: make(map[string]bool),
|
||||
maxRetries: 3,
|
||||
retryBackoff: time.Second,
|
||||
retryBackoffMax: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
|
||||
// AddJob adds a new job to the queue with deduplication
|
||||
func (q *JobQueue) AddJob(jobID string, req *plugin_pb.JobRequest, dedupKey string) (*Job, error) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
// Check deduplication
|
||||
if dedupKey != "" && q.seenKeys[dedupKey] {
|
||||
return nil, fmt.Errorf("job with key %s already queued", dedupKey)
|
||||
}
|
||||
|
||||
if _, exists := q.jobs[jobID]; exists {
|
||||
return nil, fmt.Errorf("job %s already exists", jobID)
|
||||
}
|
||||
|
||||
job := &Job{
|
||||
ID: jobID,
|
||||
Type: req.JobType,
|
||||
Description: req.Description,
|
||||
Priority: req.Priority,
|
||||
State: JobStatePending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Config: req.Config,
|
||||
Metadata: req.Metadata,
|
||||
Retries: 0,
|
||||
}
|
||||
|
||||
q.jobs[jobID] = job
|
||||
q.pendingQueue = append(q.pendingQueue, job)
|
||||
|
||||
if dedupKey != "" {
|
||||
q.seenKeys[dedupKey] = true
|
||||
}
|
||||
|
||||
// Sort by priority (higher priority first)
|
||||
q.sortPendingQueue()
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// GetPendingJob returns the next job to execute
|
||||
func (q *JobQueue) GetPendingJob() (*Job, error) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
if len(q.pendingQueue) == 0 {
|
||||
return nil, fmt.Errorf("no pending jobs")
|
||||
}
|
||||
|
||||
job := q.pendingQueue[0]
|
||||
q.pendingQueue = q.pendingQueue[1:]
|
||||
|
||||
job.SetState(JobStateRunning)
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// GetJob retrieves a job by ID
|
||||
func (q *JobQueue) GetJob(jobID string) (*Job, error) {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
||||
job, exists := q.jobs[jobID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("job %s not found", jobID)
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// CompleteJob marks a job as completed
|
||||
func (q *JobQueue) CompleteJob(jobID string, completed *plugin_pb.JobCompleted) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
job, exists := q.jobs[jobID]
|
||||
if !exists {
|
||||
return fmt.Errorf("job %s not found", jobID)
|
||||
}
|
||||
|
||||
job.SetState(JobStateCompleted)
|
||||
job.CompletedInfo = completed
|
||||
|
||||
q.addToHistory(job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailJob marks a job as failed and determines if it should be retried
|
||||
func (q *JobQueue) FailJob(jobID string, failed *plugin_pb.JobFailed) (*Job, error) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
job, exists := q.jobs[jobID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("job %s not found", jobID)
|
||||
}
|
||||
|
||||
job.FailedInfo = failed
|
||||
|
||||
// Check if we should retry
|
||||
if failed.Retryable && job.Retries < q.maxRetries {
|
||||
job.Retries++
|
||||
job.LastRetryTime = time.Now()
|
||||
job.SetState(JobStatePending)
|
||||
|
||||
// Add back to queue with exponential backoff for retry
|
||||
q.pendingQueue = append(q.pendingQueue, job)
|
||||
q.sortPendingQueue()
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// No retry, mark as failed
|
||||
job.SetState(JobStateFailed)
|
||||
q.addToHistory(job)
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// PauseJob pauses a running job
|
||||
func (q *JobQueue) PauseJob(jobID string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
job, exists := q.jobs[jobID]
|
||||
if !exists {
|
||||
return fmt.Errorf("job %s not found", jobID)
|
||||
}
|
||||
|
||||
if job.GetState() != JobStateRunning {
|
||||
return fmt.Errorf("job %s is not running", jobID)
|
||||
}
|
||||
|
||||
job.SetState(JobStatePaused)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResumeJob resumes a paused job
|
||||
func (q *JobQueue) ResumeJob(jobID string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
job, exists := q.jobs[jobID]
|
||||
if !exists {
|
||||
return fmt.Errorf("job %s not found", jobID)
|
||||
}
|
||||
|
||||
if job.GetState() != JobStatePaused {
|
||||
return fmt.Errorf("job %s is not paused", jobID)
|
||||
}
|
||||
|
||||
job.SetState(JobStateRunning)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelJob cancels a job
|
||||
func (q *JobQueue) CancelJob(jobID string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
job, exists := q.jobs[jobID]
|
||||
if !exists {
|
||||
return fmt.Errorf("job %s not found", jobID)
|
||||
}
|
||||
|
||||
state := job.GetState()
|
||||
if state != JobStatePending && state != JobStateRunning && state != JobStatePaused {
|
||||
return fmt.Errorf("cannot cancel job in %s state", state.String())
|
||||
}
|
||||
|
||||
job.SetState(JobStateCancelled)
|
||||
|
||||
// Remove from pending queue if present
|
||||
for i, j := range q.pendingQueue {
|
||||
if j.ID == jobID {
|
||||
q.pendingQueue = append(q.pendingQueue[:i], q.pendingQueue[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
q.addToHistory(job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListJobs returns jobs filtered by status
|
||||
func (q *JobQueue) ListJobs(status JobState, limit int) []*Job {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
||||
var jobs []*Job
|
||||
|
||||
// Include pending jobs from queue
|
||||
for _, job := range q.pendingQueue {
|
||||
if job.GetState() == status {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
|
||||
// Include all active jobs
|
||||
for _, job := range q.jobs {
|
||||
if job.GetState() == status {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
}
|
||||
|
||||
if limit > 0 && len(jobs) > limit {
|
||||
jobs = jobs[:limit]
|
||||
}
|
||||
|
||||
return jobs
|
||||
}
|
||||
|
||||
// ListAllJobs returns all jobs
|
||||
func (q *JobQueue) ListAllJobs() []*Job {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
||||
jobs := make([]*Job, 0, len(q.jobs))
|
||||
for _, job := range q.jobs {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
// Add completed history
|
||||
for _, job := range q.completedHistory {
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return jobs
|
||||
}
|
||||
|
||||
// sortPendingQueue sorts the pending queue by priority (higher first)
|
||||
func (q *JobQueue) sortPendingQueue() {
|
||||
for i := 0; i < len(q.pendingQueue)-1; i++ {
|
||||
for j := i + 1; j < len(q.pendingQueue); j++ {
|
||||
if q.pendingQueue[j].Priority > q.pendingQueue[i].Priority {
|
||||
q.pendingQueue[i], q.pendingQueue[j] = q.pendingQueue[j], q.pendingQueue[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addToHistory adds a completed job to history
|
||||
func (q *JobQueue) addToHistory(job *Job) {
|
||||
q.completedHistory = append(q.completedHistory, job)
|
||||
|
||||
// Keep only last N entries
|
||||
if len(q.completedHistory) > q.maxHistorySize {
|
||||
q.completedHistory = q.completedHistory[len(q.completedHistory)-q.maxHistorySize:]
|
||||
}
|
||||
|
||||
// Remove from active jobs
|
||||
delete(q.jobs, job.ID)
|
||||
}
|
||||
|
||||
// GetPendingCount returns number of pending jobs
|
||||
func (q *JobQueue) GetPendingCount() int {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
return len(q.pendingQueue)
|
||||
}
|
||||
|
||||
// GetStats returns queue statistics
|
||||
func (q *JobQueue) GetStats() map[string]interface{} {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
||||
var running, completed, failed, cancelled int
|
||||
|
||||
for _, job := range q.jobs {
|
||||
switch job.GetState() {
|
||||
case JobStateRunning:
|
||||
running++
|
||||
case JobStateCompleted:
|
||||
completed++
|
||||
case JobStateFailed:
|
||||
failed++
|
||||
case JobStateCancelled:
|
||||
cancelled++
|
||||
}
|
||||
}
|
||||
|
||||
completed += len(q.completedHistory)
|
||||
|
||||
return map[string]interface{}{
|
||||
"job_type": q.jobType,
|
||||
"pending": len(q.pendingQueue),
|
||||
"running": running,
|
||||
"completed": completed,
|
||||
"failed": failed,
|
||||
"cancelled": cancelled,
|
||||
"total": len(q.jobs) + len(q.completedHistory),
|
||||
"history_size": len(q.completedHistory),
|
||||
}
|
||||
}
|
||||
@@ -1,542 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Manager is the main plugin system manager
|
||||
type Manager struct {
|
||||
registry *Registry
|
||||
dispatcher *Dispatcher
|
||||
configMgr *ConfigManager
|
||||
grpcServer *GrpcServer
|
||||
|
||||
// gRPC server
|
||||
grpcListener net.Listener
|
||||
grpc *grpc.Server
|
||||
|
||||
// Configuration
|
||||
listenAddr string
|
||||
dataDir string
|
||||
healthCheckInterval time.Duration
|
||||
|
||||
// State
|
||||
mu sync.RWMutex
|
||||
running bool
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NewManager creates a new plugin manager
|
||||
func NewManager(listenAddr, dataDir string) (*Manager, error) {
|
||||
// Create configuration manager
|
||||
configMgr, err := NewConfigManager(dataDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create config manager: %w", err)
|
||||
}
|
||||
|
||||
// Create registry
|
||||
registry := NewRegistry()
|
||||
|
||||
// Create dispatcher
|
||||
dispatcher := NewDispatcher(registry, configMgr)
|
||||
|
||||
// Create gRPC server implementation
|
||||
grpcServer := NewGrpcServer(registry, dispatcher, configMgr)
|
||||
|
||||
return &Manager{
|
||||
registry: registry,
|
||||
dispatcher: dispatcher,
|
||||
configMgr: configMgr,
|
||||
grpcServer: grpcServer,
|
||||
listenAddr: listenAddr,
|
||||
dataDir: dataDir,
|
||||
healthCheckInterval: 30 * time.Second,
|
||||
stopChan: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start starts the plugin manager and gRPC server
|
||||
func (m *Manager) Start() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.running {
|
||||
return fmt.Errorf("plugin manager already running")
|
||||
}
|
||||
|
||||
// Start registry health checks
|
||||
m.registry.Start()
|
||||
|
||||
// Start dispatcher
|
||||
m.dispatcher.Start()
|
||||
|
||||
// Create gRPC server
|
||||
m.grpc = grpc.NewServer()
|
||||
|
||||
// Register services
|
||||
plugin_pb.RegisterPluginServiceServer(m.grpc, m.grpcServer)
|
||||
plugin_pb.RegisterAdminQueryServiceServer(m.grpc, m.grpcServer)
|
||||
plugin_pb.RegisterAdminCommandServiceServer(m.grpc, m.grpcServer)
|
||||
|
||||
// Start listening
|
||||
listener, err := net.Listen("tcp", m.listenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen on %s: %w", m.listenAddr, err)
|
||||
}
|
||||
m.grpcListener = listener
|
||||
|
||||
// Start gRPC server in background
|
||||
go func() {
|
||||
if err := m.grpc.Serve(listener); err != nil && err != grpc.ErrServerStopped {
|
||||
fmt.Printf("gRPC server error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
m.running = true
|
||||
|
||||
fmt.Printf("Plugin manager started on %s\n", m.listenAddr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the plugin manager and gRPC server
|
||||
func (m *Manager) Stop() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if !m.running {
|
||||
return fmt.Errorf("plugin manager not running")
|
||||
}
|
||||
|
||||
close(m.stopChan)
|
||||
|
||||
// Stop dispatcher
|
||||
m.dispatcher.Stop()
|
||||
|
||||
// Stop registry
|
||||
m.registry.Stop()
|
||||
|
||||
// Stop gRPC server
|
||||
if m.grpc != nil {
|
||||
m.grpc.GracefulStop()
|
||||
}
|
||||
|
||||
m.running = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether the manager is running
|
||||
func (m *Manager) IsRunning() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.running
|
||||
}
|
||||
|
||||
// RegisterJobType registers a job type for detection and execution
|
||||
func (m *Manager) RegisterJobType(jobType string, detectionInterval time.Duration) error {
|
||||
m.mu.RLock()
|
||||
running := m.running
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !running {
|
||||
return fmt.Errorf("plugin manager not running")
|
||||
}
|
||||
|
||||
// Register job type
|
||||
if err := m.dispatcher.RegisterJobType(jobType, detectionInterval); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := m.configMgr.LoadConfig(jobType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueueJob queues a new job for execution
|
||||
func (m *Manager) QueueJob(jobType string, description string, priority int64, metadata map[string]string) (*Job, error) {
|
||||
m.mu.RLock()
|
||||
running := m.running
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !running {
|
||||
return nil, fmt.Errorf("plugin manager not running")
|
||||
}
|
||||
|
||||
jobReq := &plugin_pb.JobRequest{
|
||||
JobType: jobType,
|
||||
Description: description,
|
||||
Priority: priority,
|
||||
Metadata: metadata,
|
||||
RequestSource: "admin",
|
||||
}
|
||||
|
||||
return m.dispatcher.QueueJob(jobType, jobReq, "")
|
||||
}
|
||||
|
||||
// DispatchJob dispatches the next available job
|
||||
func (m *Manager) DispatchJob(jobType string) (*Job, *ConnectedPlugin, error) {
|
||||
m.mu.RLock()
|
||||
running := m.running
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !running {
|
||||
return nil, nil, fmt.Errorf("plugin manager not running")
|
||||
}
|
||||
|
||||
return m.dispatcher.DispatchJob(jobType)
|
||||
}
|
||||
|
||||
// GetJob retrieves a job by ID
|
||||
func (m *Manager) GetJob(jobID string) (*Job, error) {
|
||||
return m.dispatcher.GetJob(jobID)
|
||||
}
|
||||
|
||||
// ListJobs returns jobs of a specific type and status
|
||||
func (m *Manager) ListJobs(jobType string, status JobState) []*Job {
|
||||
return m.dispatcher.ListJobs(jobType, status)
|
||||
}
|
||||
|
||||
// ListAllJobs returns all jobs
|
||||
func (m *Manager) ListAllJobs() []*Job {
|
||||
return m.dispatcher.ListAllJobs()
|
||||
}
|
||||
|
||||
// ListPlugins returns all connected plugins
|
||||
func (m *Manager) ListPlugins() []*ConnectedPlugin {
|
||||
return m.registry.ListPlugins()
|
||||
}
|
||||
|
||||
// GetPlugin retrieves a plugin by ID
|
||||
func (m *Manager) GetPlugin(pluginID string) (*ConnectedPlugin, error) {
|
||||
return m.registry.GetPlugin(pluginID)
|
||||
}
|
||||
|
||||
// ListPluginsByCapability returns plugins that support a job type
|
||||
func (m *Manager) ListPluginsByCapability(jobType string) []*ConnectedPlugin {
|
||||
return m.registry.ListPluginsByCapability(jobType)
|
||||
}
|
||||
|
||||
// UpdateJobTypeConfig updates configuration for a job type
|
||||
func (m *Manager) UpdateJobTypeConfig(jobType string, enabled bool, config []*plugin_pb.ConfigFieldValue) (*JobTypeConfig, error) {
|
||||
return m.configMgr.UpdateConfig(jobType, enabled, config, nil, "admin")
|
||||
}
|
||||
|
||||
// GetJobTypeConfig retrieves configuration for a job type
|
||||
func (m *Manager) GetJobTypeConfig(jobType string) (*JobTypeConfig, error) {
|
||||
return m.configMgr.GetConfig(jobType)
|
||||
}
|
||||
|
||||
// CancelJob cancels a job
|
||||
func (m *Manager) CancelJob(jobID string) error {
|
||||
return m.dispatcher.CancelJob(jobID)
|
||||
}
|
||||
|
||||
// RetryJob retries a failed job
|
||||
func (m *Manager) RetryJob(jobID string) error {
|
||||
return m.dispatcher.RetryJob(jobID)
|
||||
}
|
||||
|
||||
// GetStats returns system statistics
|
||||
func (m *Manager) GetStats() map[string]interface{} {
|
||||
m.mu.RLock()
|
||||
running := m.running
|
||||
m.mu.RUnlock()
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"running": running,
|
||||
}
|
||||
|
||||
if running {
|
||||
stats["registry"] = m.registry.GetStats()
|
||||
stats["dispatcher"] = m.dispatcher.GetStats()
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// WaitForPluginConnection waits for a plugin to connect with timeout
|
||||
func (m *Manager) WaitForPluginConnection(timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
plugins := m.registry.ListPlugins()
|
||||
if len(plugins) > 0 {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
return fmt.Errorf("no plugin connected within %v", timeout)
|
||||
}
|
||||
|
||||
// CreateTestJob creates a job for testing
|
||||
func (m *Manager) CreateTestJob(jobType string, description string) (*Job, error) {
|
||||
return m.QueueJob(jobType, description, 100, map[string]string{
|
||||
"test": "true",
|
||||
})
|
||||
}
|
||||
|
||||
// GetHealthStatus returns the health status of the plugin system
|
||||
func (m *Manager) GetHealthStatus() map[string]interface{} {
|
||||
m.mu.RLock()
|
||||
running := m.running
|
||||
m.mu.RUnlock()
|
||||
|
||||
status := map[string]interface{}{
|
||||
"running": running,
|
||||
}
|
||||
|
||||
if !running {
|
||||
return status
|
||||
}
|
||||
|
||||
plugins := m.registry.ListPlugins()
|
||||
var healthyCount int
|
||||
for _, p := range plugins {
|
||||
if p.IsHealthy() {
|
||||
healthyCount++
|
||||
}
|
||||
}
|
||||
|
||||
jobs := m.dispatcher.ListAllJobs()
|
||||
var runningJobs, pendingJobs, completedJobs, failedJobs int
|
||||
|
||||
for _, job := range jobs {
|
||||
switch job.GetState() {
|
||||
case JobStateRunning:
|
||||
runningJobs++
|
||||
case JobStatePending:
|
||||
pendingJobs++
|
||||
case JobStateCompleted:
|
||||
completedJobs++
|
||||
case JobStateFailed:
|
||||
failedJobs++
|
||||
}
|
||||
}
|
||||
|
||||
status["plugins"] = map[string]interface{}{
|
||||
"total": len(plugins),
|
||||
"healthy": healthyCount,
|
||||
}
|
||||
|
||||
status["jobs"] = map[string]interface{}{
|
||||
"running": runningJobs,
|
||||
"pending": pendingJobs,
|
||||
"completed": completedJobs,
|
||||
"failed": failedJobs,
|
||||
"total": len(jobs),
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// LoadAllConfigs loads all configurations from disk
|
||||
func (m *Manager) LoadAllConfigs() error {
|
||||
return m.configMgr.LoadAllConfigs()
|
||||
}
|
||||
|
||||
// GetDataDir returns the data directory
|
||||
func (m *Manager) GetDataDir() string {
|
||||
return m.configMgr.GetDataDir()
|
||||
}
|
||||
|
||||
// QueryPluginService queries a plugin service for capabilities
|
||||
func (m *Manager) QueryPluginService(ctx context.Context, pluginID string, addr string) (*plugin_pb.PluginRegister, error) {
|
||||
// This would be implemented to connect to a plugin and query its capabilities
|
||||
// For now, return a placeholder error
|
||||
return nil, fmt.Errorf("not yet implemented")
|
||||
}
|
||||
|
||||
// ExecuteDetection triggers detection for a job type
|
||||
func (m *Manager) ExecuteDetection(ctx context.Context, jobType string) ([]*DetectedJobInfo, error) {
|
||||
_, err := m.registry.GetDetectorForJobType(jobType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no detector available for job type %s: %w", jobType, err)
|
||||
}
|
||||
|
||||
// TODO: Call detector plugin's DetectJobs method
|
||||
// This would require connecting to the plugin and calling its service
|
||||
|
||||
return nil, fmt.Errorf("detection not yet implemented")
|
||||
}
|
||||
|
||||
// DetectedJobInfo holds information about a detected job
|
||||
type DetectedJobInfo struct {
|
||||
Key string
|
||||
JobType string
|
||||
Priority int64
|
||||
Metadata map[string]string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// GetListenAddr returns the listen address
|
||||
func (m *Manager) GetListenAddr() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.listenAddr
|
||||
}
|
||||
|
||||
// GetContext returns a context for the manager
|
||||
func (m *Manager) GetContext() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
// Close closes the manager and releases resources
|
||||
func (m *Manager) Close() error {
|
||||
if m.IsRunning() {
|
||||
return m.Stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListConnectedPlugins returns the list of connected plugins
|
||||
func (m *Manager) ListConnectedPlugins() []*ConnectedPlugin {
|
||||
return m.registry.ListPlugins()
|
||||
}
|
||||
|
||||
// GetConfig retrieves configuration for a job type
|
||||
func (m *Manager) GetConfig(jobType string) (*JobTypeConfig, error) {
|
||||
return m.configMgr.GetConfig(jobType)
|
||||
}
|
||||
|
||||
// SaveConfig saves configuration for a job type
|
||||
func (m *Manager) SaveConfig(jobType string, enabled bool, adminConfig, workerConfig []*plugin_pb.ConfigFieldValue) error {
|
||||
config := &JobTypeConfig{
|
||||
JobType: jobType,
|
||||
Enabled: enabled,
|
||||
AdminConfig: adminConfig,
|
||||
WorkerConfig: workerConfig,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
return m.configMgr.SaveConfig(config)
|
||||
}
|
||||
|
||||
// GetDetectionHistory returns detection history for a job type
|
||||
func (m *Manager) GetDetectionHistory(jobType string, limit int) []*DetectionRecord {
|
||||
config, err := m.configMgr.GetConfig(jobType)
|
||||
if err != nil || config == nil {
|
||||
return []*DetectionRecord{}
|
||||
}
|
||||
|
||||
config.mu.RLock()
|
||||
defer config.mu.RUnlock()
|
||||
|
||||
// Return in reverse order (latest first)
|
||||
var result []*DetectionRecord
|
||||
start := len(config.DetectionHistory) - 1
|
||||
if start < 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for i := start; i >= 0 && len(result) < limit; i-- {
|
||||
result = append(result, config.DetectionHistory[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetExecutionHistory returns execution history for a job type
|
||||
func (m *Manager) GetExecutionHistory(jobType string, limit int) []*ExecutionRecord {
|
||||
config, err := m.configMgr.GetConfig(jobType)
|
||||
if err != nil || config == nil {
|
||||
return []*ExecutionRecord{}
|
||||
}
|
||||
|
||||
config.mu.RLock()
|
||||
defer config.mu.RUnlock()
|
||||
|
||||
// Return in reverse order (latest first)
|
||||
var result []*ExecutionRecord
|
||||
start := len(config.ExecutionHistory) - 1
|
||||
if start < 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
for i := start; i >= 0 && len(result) < limit; i-- {
|
||||
result = append(result, config.ExecutionHistory[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TriggerDetection manually triggers detection for a job type
|
||||
func (m *Manager) TriggerDetection(jobType string) error {
|
||||
detector, err := m.registry.GetDetectorForJobType(jobType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("no detector for job type %s", jobType)
|
||||
}
|
||||
|
||||
config, err := m.configMgr.GetConfig(jobType)
|
||||
if err != nil || !config.Enabled {
|
||||
return fmt.Errorf("job type %s not enabled", jobType)
|
||||
}
|
||||
|
||||
// Call the dispatcher to run detection
|
||||
return m.dispatcher.TriggerDetection(jobType, detector, config)
|
||||
}
|
||||
|
||||
// ListJobTypes returns the list of registered job types
|
||||
func (m *Manager) ListJobTypes() []string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var jobTypes []string
|
||||
for jobType := range m.dispatcher.queues {
|
||||
jobTypes = append(jobTypes, jobType)
|
||||
}
|
||||
return jobTypes
|
||||
}
|
||||
|
||||
// GetDetectorForJobType retrieves a detector for a job type
|
||||
func (m *Manager) GetDetectorForJobType(jobType string) (interface{}, error) {
|
||||
return m.registry.GetDetectorForJobType(jobType)
|
||||
}
|
||||
|
||||
// RecordDetection records a detection event
|
||||
func (m *Manager) RecordDetection(jobType string, record *DetectionRecord) error {
|
||||
config, err := m.configMgr.GetConfig(jobType)
|
||||
if err != nil || config == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.mu.Lock()
|
||||
defer config.mu.Unlock()
|
||||
|
||||
// Keep last 50 detection records
|
||||
config.DetectionHistory = append(config.DetectionHistory, record)
|
||||
if len(config.DetectionHistory) > 50 {
|
||||
config.DetectionHistory = config.DetectionHistory[len(config.DetectionHistory)-50:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecordExecution records a job execution event
|
||||
func (m *Manager) RecordExecution(jobType string, record *ExecutionRecord) error {
|
||||
config, err := m.configMgr.GetConfig(jobType)
|
||||
if err != nil || config == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.mu.Lock()
|
||||
defer config.mu.Unlock()
|
||||
|
||||
// Keep last 100 execution records
|
||||
config.ExecutionHistory = append(config.ExecutionHistory, record)
|
||||
if len(config.ExecutionHistory) > 100 {
|
||||
config.ExecutionHistory = config.ExecutionHistory[len(config.ExecutionHistory)-100:]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// Registry manages all connected plugins and their capabilities
|
||||
type Registry struct {
|
||||
plugins map[string]*ConnectedPlugin
|
||||
|
||||
// Index by job type and capability (detector/executor)
|
||||
detectorsByJobType map[string][]*ConnectedPlugin
|
||||
executorsByJobType map[string][]*ConnectedPlugin
|
||||
|
||||
mu sync.RWMutex
|
||||
|
||||
// Configuration
|
||||
healthCheckInterval time.Duration
|
||||
healthCheckTimeout time.Duration
|
||||
healthCheckTicker *time.Ticker
|
||||
stopChan chan struct{}
|
||||
}
|
||||
|
||||
// NewRegistry creates a new plugin registry
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
plugins: make(map[string]*ConnectedPlugin),
|
||||
detectorsByJobType: make(map[string][]*ConnectedPlugin),
|
||||
executorsByJobType: make(map[string][]*ConnectedPlugin),
|
||||
healthCheckInterval: 30 * time.Second,
|
||||
healthCheckTimeout: 90 * time.Second,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPlugin adds a new plugin to the registry
|
||||
func (r *Registry) RegisterPlugin(pluginID string, register *plugin_pb.PluginRegister) (*ConnectedPlugin, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if _, exists := r.plugins[pluginID]; exists {
|
||||
return nil, fmt.Errorf("plugin %s already registered", pluginID)
|
||||
}
|
||||
|
||||
plugin := &ConnectedPlugin{
|
||||
ID: pluginID,
|
||||
Name: register.Name,
|
||||
Version: register.Version,
|
||||
ProtocolVersion: register.ProtocolVersion,
|
||||
ConnectedAt: time.Now(),
|
||||
LastHeartbeat: time.Now(),
|
||||
State: PluginStateConnected,
|
||||
Healthy: true,
|
||||
Capabilities: make(map[string]*plugin_pb.JobTypeCapability),
|
||||
StreamConnected: true,
|
||||
}
|
||||
|
||||
// Index capabilities
|
||||
for _, cap := range register.Capabilities {
|
||||
plugin.Capabilities[cap.JobType] = cap
|
||||
|
||||
if cap.CanDetect {
|
||||
r.detectorsByJobType[cap.JobType] = append(r.detectorsByJobType[cap.JobType], plugin)
|
||||
}
|
||||
if cap.CanExecute {
|
||||
r.executorsByJobType[cap.JobType] = append(r.executorsByJobType[cap.JobType], plugin)
|
||||
}
|
||||
}
|
||||
|
||||
r.plugins[pluginID] = plugin
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// UnregisterPlugin removes a plugin from the registry
|
||||
func (r *Registry) UnregisterPlugin(pluginID string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
plugin, exists := r.plugins[pluginID]
|
||||
if !exists {
|
||||
return fmt.Errorf("plugin %s not found", pluginID)
|
||||
}
|
||||
|
||||
// Remove from capability indexes
|
||||
for jobType := range plugin.Capabilities {
|
||||
r.removePluginFromDetectors(jobType, plugin)
|
||||
r.removePluginFromExecutors(jobType, plugin)
|
||||
}
|
||||
|
||||
delete(r.plugins, pluginID)
|
||||
plugin.SetState(PluginStateDisconnected)
|
||||
return nil
|
||||
}
|
||||
|
||||
// removePluginFromDetectors removes plugin from detector index for a job type
|
||||
func (r *Registry) removePluginFromDetectors(jobType string, plugin *ConnectedPlugin) {
|
||||
detectors := r.detectorsByJobType[jobType]
|
||||
for i, p := range detectors {
|
||||
if p.ID == plugin.ID {
|
||||
r.detectorsByJobType[jobType] = append(detectors[:i], detectors[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(r.detectorsByJobType[jobType]) == 0 {
|
||||
delete(r.detectorsByJobType, jobType)
|
||||
}
|
||||
}
|
||||
|
||||
// removePluginFromExecutors removes plugin from executor index for a job type
|
||||
func (r *Registry) removePluginFromExecutors(jobType string, plugin *ConnectedPlugin) {
|
||||
executors := r.executorsByJobType[jobType]
|
||||
for i, p := range executors {
|
||||
if p.ID == plugin.ID {
|
||||
r.executorsByJobType[jobType] = append(executors[:i], executors[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(r.executorsByJobType[jobType]) == 0 {
|
||||
delete(r.executorsByJobType, jobType)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlugin retrieves a plugin by ID
|
||||
func (r *Registry) GetPlugin(pluginID string) (*ConnectedPlugin, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
plugin, exists := r.plugins[pluginID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("plugin %s not found", pluginID)
|
||||
}
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// GetDetectorForJobType returns an available detector plugin for a job type
|
||||
func (r *Registry) GetDetectorForJobType(jobType string) (*ConnectedPlugin, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
detectors, exists := r.detectorsByJobType[jobType]
|
||||
if !exists || len(detectors) == 0 {
|
||||
return nil, fmt.Errorf("no detector available for job type %s", jobType)
|
||||
}
|
||||
|
||||
// Return the first healthy detector
|
||||
for _, detector := range detectors {
|
||||
if detector.IsHealthy() {
|
||||
return detector, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no healthy detector available for job type %s", jobType)
|
||||
}
|
||||
|
||||
// GetExecutorForJobType returns an available executor plugin for a job type
|
||||
func (r *Registry) GetExecutorForJobType(jobType string) (*ConnectedPlugin, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
executors, exists := r.executorsByJobType[jobType]
|
||||
if !exists || len(executors) == 0 {
|
||||
return nil, fmt.Errorf("no executor available for job type %s", jobType)
|
||||
}
|
||||
|
||||
// Return the first healthy executor with lowest workload
|
||||
var bestExecutor *ConnectedPlugin
|
||||
var minLoad int32 = int32(^uint32(0) >> 1)
|
||||
|
||||
for _, executor := range executors {
|
||||
if executor.IsHealthy() {
|
||||
load := executor.PendingJobs + executor.RunningJobs
|
||||
if load < minLoad {
|
||||
minLoad = load
|
||||
bestExecutor = executor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestExecutor == nil {
|
||||
return nil, fmt.Errorf("no healthy executor available for job type %s", jobType)
|
||||
}
|
||||
|
||||
return bestExecutor, nil
|
||||
}
|
||||
|
||||
// ListPlugins returns all connected plugins
|
||||
func (r *Registry) ListPlugins() []*ConnectedPlugin {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
plugins := make([]*ConnectedPlugin, 0, len(r.plugins))
|
||||
for _, plugin := range r.plugins {
|
||||
plugins = append(plugins, plugin)
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
// ListPluginsByCapability returns plugins that support a specific job type
|
||||
func (r *Registry) ListPluginsByCapability(jobType string) []*ConnectedPlugin {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
detectors := r.detectorsByJobType[jobType]
|
||||
executors := r.executorsByJobType[jobType]
|
||||
|
||||
// Combine and deduplicate
|
||||
pluginMap := make(map[string]*ConnectedPlugin)
|
||||
for _, p := range detectors {
|
||||
pluginMap[p.ID] = p
|
||||
}
|
||||
for _, p := range executors {
|
||||
pluginMap[p.ID] = p
|
||||
}
|
||||
|
||||
plugins := make([]*ConnectedPlugin, 0, len(pluginMap))
|
||||
for _, p := range pluginMap {
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
// UpdateHeartbeat updates plugin heartbeat and health status
|
||||
func (r *Registry) UpdateHeartbeat(pluginID string, heartbeat *plugin_pb.PluginHeartbeat) error {
|
||||
r.mu.Lock()
|
||||
plugin, exists := r.plugins[pluginID]
|
||||
r.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("plugin %s not found", pluginID)
|
||||
}
|
||||
|
||||
plugin.UpdateHeartbeat(
|
||||
heartbeat.CpuUsagePercent,
|
||||
heartbeat.MemoryUsageMb,
|
||||
heartbeat.PendingJobs,
|
||||
int32(len(plugin.Capabilities)), // rough estimate
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the health check loop
|
||||
func (r *Registry) Start() {
|
||||
r.healthCheckTicker = time.NewTicker(r.healthCheckInterval)
|
||||
go r.healthCheckLoop()
|
||||
}
|
||||
|
||||
// Stop stops the health check loop
|
||||
func (r *Registry) Stop() {
|
||||
close(r.stopChan)
|
||||
if r.healthCheckTicker != nil {
|
||||
r.healthCheckTicker.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// healthCheckLoop periodically checks plugin health
|
||||
func (r *Registry) healthCheckLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-r.healthCheckTicker.C:
|
||||
r.checkHealth()
|
||||
case <-r.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkHealth checks the health of all plugins
|
||||
func (r *Registry) checkHealth() {
|
||||
r.mu.RLock()
|
||||
plugins := make([]*ConnectedPlugin, 0, len(r.plugins))
|
||||
for _, p := range r.plugins {
|
||||
plugins = append(plugins, p)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
for _, plugin := range plugins {
|
||||
plugin.mu.RLock()
|
||||
lastHeartbeat := plugin.LastHeartbeat
|
||||
plugin.mu.RUnlock()
|
||||
|
||||
timeSinceHeartbeat := now.Sub(lastHeartbeat)
|
||||
|
||||
if timeSinceHeartbeat > r.healthCheckTimeout {
|
||||
plugin.SetState(PluginStateUnhealthy)
|
||||
plugin.mu.Lock()
|
||||
plugin.Healthy = false
|
||||
plugin.mu.Unlock()
|
||||
} else if timeSinceHeartbeat > r.healthCheckTimeout/2 {
|
||||
plugin.SetState(PluginStateUnhealthy)
|
||||
} else {
|
||||
plugin.SetState(PluginStateHealthy)
|
||||
plugin.mu.Lock()
|
||||
plugin.Healthy = true
|
||||
plugin.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats returns registry statistics
|
||||
func (r *Registry) GetStats() map[string]interface{} {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total_plugins": len(r.plugins),
|
||||
"job_types": len(r.detectorsByJobType),
|
||||
}
|
||||
|
||||
var healthyCount int
|
||||
for _, p := range r.plugins {
|
||||
if p.IsHealthy() {
|
||||
healthyCount++
|
||||
}
|
||||
}
|
||||
stats["healthy_plugins"] = healthyCount
|
||||
|
||||
jobTypeStats := make(map[string]map[string]interface{})
|
||||
for jobType := range r.detectorsByJobType {
|
||||
jobTypeStats[jobType] = map[string]interface{}{
|
||||
"detectors": len(r.detectorsByJobType[jobType]),
|
||||
"executors": len(r.executorsByJobType[jobType]),
|
||||
}
|
||||
}
|
||||
stats["job_type_stats"] = jobTypeStats
|
||||
|
||||
return stats
|
||||
}
|
||||
@@ -1,420 +0,0 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// TestHarness provides utilities for testing plugin functionality
|
||||
type TestHarness struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
adminServer *MockAdminServer
|
||||
pluginClient plugin_pb.PluginService_ConnectClient
|
||||
executionClient plugin_pb.PluginService_ExecuteJobClient
|
||||
mu sync.RWMutex
|
||||
createdJobs map[string]*plugin_pb.JobRequest
|
||||
executedJobs map[string]*ExecutionTracker
|
||||
progressUpdates map[string][]*plugin_pb.JobProgress
|
||||
testDataDir string
|
||||
}
|
||||
|
||||
// ExecutionTracker tracks the execution of a job
|
||||
type ExecutionTracker struct {
|
||||
JobID string
|
||||
Status string
|
||||
ProgressPercent int32
|
||||
Messages []*plugin_pb.JobExecutionMessage
|
||||
StartTime time.Time
|
||||
LastUpdate time.Time
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewTestHarness creates a new test harness
|
||||
func NewTestHarness(ctx context.Context) *TestHarness {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
return &TestHarness{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
adminServer: NewMockAdminServer(),
|
||||
createdJobs: make(map[string]*plugin_pb.JobRequest),
|
||||
executedJobs: make(map[string]*ExecutionTracker),
|
||||
progressUpdates: make(map[string][]*plugin_pb.JobProgress),
|
||||
}
|
||||
}
|
||||
|
||||
// Setup initializes the test harness and starts mock connections
|
||||
func (h *TestHarness) Setup() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.adminServer == nil {
|
||||
h.adminServer = NewMockAdminServer()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Teardown cleans up test resources
|
||||
func (h *TestHarness) Teardown() error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if h.cancel != nil {
|
||||
h.cancel()
|
||||
}
|
||||
|
||||
if h.adminServer != nil {
|
||||
h.adminServer.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetContext returns the test context
|
||||
func (h *TestHarness) GetContext() context.Context {
|
||||
return h.ctx
|
||||
}
|
||||
|
||||
// CreateJob creates a test job request
|
||||
func (h *TestHarness) CreateJob(jobType, description string, priority int64, config []*plugin_pb.ConfigFieldValue) *plugin_pb.JobRequest {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
jobID := fmt.Sprintf("test-job-%d", time.Now().UnixNano())
|
||||
job := &plugin_pb.JobRequest{
|
||||
JobId: jobID,
|
||||
JobType: jobType,
|
||||
Description: description,
|
||||
Priority: priority,
|
||||
CreatedAt: timestamppb.Now(),
|
||||
Config: config,
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
|
||||
h.createdJobs[jobID] = job
|
||||
return job
|
||||
}
|
||||
|
||||
// ExecuteJob simulates job execution and returns a tracker
|
||||
func (h *TestHarness) ExecuteJob(job *plugin_pb.JobRequest) *ExecutionTracker {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
tracker := &ExecutionTracker{
|
||||
JobID: job.JobId,
|
||||
Status: "running",
|
||||
Messages: make([]*plugin_pb.JobExecutionMessage, 0),
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
h.executedJobs[job.JobId] = tracker
|
||||
return tracker
|
||||
}
|
||||
|
||||
// UpdateJobProgress updates the progress of a job
|
||||
func (h *TestHarness) UpdateJobProgress(jobID string, progressPercent int32, currentStep, statusMessage string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
progress := &plugin_pb.JobProgress{
|
||||
ProgressPercent: progressPercent,
|
||||
CurrentStep: currentStep,
|
||||
StatusMessage: statusMessage,
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
|
||||
h.progressUpdates[jobID] = append(h.progressUpdates[jobID], progress)
|
||||
|
||||
if tracker, ok := h.executedJobs[jobID]; ok {
|
||||
tracker.mu.Lock()
|
||||
tracker.ProgressPercent = progressPercent
|
||||
tracker.LastUpdate = time.Now()
|
||||
tracker.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyProgress checks if job progress meets expectations
|
||||
func (h *TestHarness) VerifyProgress(jobID string, expectedPercent int32) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
tracker, ok := h.executedJobs[jobID]
|
||||
if !ok {
|
||||
return fmt.Errorf("job not found: %s", jobID)
|
||||
}
|
||||
|
||||
tracker.mu.RLock()
|
||||
actual := tracker.ProgressPercent
|
||||
tracker.mu.RUnlock()
|
||||
|
||||
if actual != expectedPercent {
|
||||
return fmt.Errorf("progress mismatch for job %s: expected %d, got %d", jobID, expectedPercent, actual)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompleteJob marks a job as completed
|
||||
func (h *TestHarness) CompleteJob(jobID string, summary string, output map[string]string) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
tracker, ok := h.executedJobs[jobID]
|
||||
if !ok {
|
||||
return fmt.Errorf("job not found: %s", jobID)
|
||||
}
|
||||
|
||||
tracker.mu.Lock()
|
||||
tracker.Status = "completed"
|
||||
tracker.ProgressPercent = 100
|
||||
tracker.LastUpdate = time.Now()
|
||||
tracker.mu.Unlock()
|
||||
|
||||
completed := &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_JobCompleted{
|
||||
JobCompleted: &plugin_pb.JobCompleted{
|
||||
CompletedAt: timestamppb.Now(),
|
||||
Summary: summary,
|
||||
Output: output,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tracker.mu.Lock()
|
||||
tracker.Messages = append(tracker.Messages, completed)
|
||||
tracker.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FailJob marks a job as failed
|
||||
func (h *TestHarness) FailJob(jobID, errorCode, errorMessage string, retryable bool) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
tracker, ok := h.executedJobs[jobID]
|
||||
if !ok {
|
||||
return fmt.Errorf("job not found: %s", jobID)
|
||||
}
|
||||
|
||||
tracker.mu.Lock()
|
||||
tracker.Status = "failed"
|
||||
tracker.LastUpdate = time.Now()
|
||||
tracker.mu.Unlock()
|
||||
|
||||
failed := &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_JobFailed{
|
||||
JobFailed: &plugin_pb.JobFailed{
|
||||
ErrorCode: errorCode,
|
||||
ErrorMessage: errorMessage,
|
||||
Retryable: retryable,
|
||||
FailedAt: timestamppb.Now(),
|
||||
RetryCount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tracker.mu.Lock()
|
||||
tracker.Messages = append(tracker.Messages, failed)
|
||||
tracker.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJobMessages returns all execution messages for a job
|
||||
func (h *TestHarness) GetJobMessages(jobID string) []*plugin_pb.JobExecutionMessage {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
tracker, ok := h.executedJobs[jobID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
tracker.mu.RLock()
|
||||
defer tracker.mu.RUnlock()
|
||||
return tracker.Messages
|
||||
}
|
||||
|
||||
// GetJobStatus returns the current status of a job
|
||||
func (h *TestHarness) GetJobStatus(jobID string) (string, error) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
tracker, ok := h.executedJobs[jobID]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("job not found: %s", jobID)
|
||||
}
|
||||
|
||||
tracker.mu.RLock()
|
||||
defer tracker.mu.RUnlock()
|
||||
return tracker.Status, nil
|
||||
}
|
||||
|
||||
// GetAdminServer returns the mock admin server
|
||||
func (h *TestHarness) GetAdminServer() *MockAdminServer {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return h.adminServer
|
||||
}
|
||||
|
||||
// WaitForJobCompletion waits for a job to complete or timeout
|
||||
func (h *TestHarness) WaitForJobCompletion(jobID string, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for {
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("job completion timeout: %s", jobID)
|
||||
}
|
||||
|
||||
status, err := h.GetJobStatus(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status == "completed" || status == "failed" {
|
||||
return nil
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertJobExists verifies that a job was created
|
||||
func (h *TestHarness) AssertJobExists(jobID string) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
if _, ok := h.createdJobs[jobID]; !ok {
|
||||
return fmt.Errorf("job does not exist: %s", jobID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertJobNotExists verifies that a job was not created
|
||||
func (h *TestHarness) AssertJobNotExists(jobID string) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
if _, ok := h.createdJobs[jobID]; ok {
|
||||
return fmt.Errorf("job should not exist: %s", jobID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssertJobStatus verifies that a job has the expected status
|
||||
func (h *TestHarness) AssertJobStatus(jobID, expectedStatus string) error {
|
||||
status, err := h.GetJobStatus(jobID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status != expectedStatus {
|
||||
return fmt.Errorf("job status mismatch for %s: expected %s, got %s", jobID, expectedStatus, status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatePluginRegister creates a test plugin registration message
|
||||
func CreatePluginRegister(pluginID, name, version string, capabilities []*plugin_pb.JobTypeCapability) *plugin_pb.PluginMessage {
|
||||
return &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Register{
|
||||
Register: &plugin_pb.PluginRegister{
|
||||
PluginId: pluginID,
|
||||
Name: name,
|
||||
Version: version,
|
||||
ProtocolVersion: "v1",
|
||||
Capabilities: capabilities,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePluginHeartbeat creates a test plugin heartbeat message
|
||||
func CreatePluginHeartbeat(pluginID string, pendingJobs, runningJobs int32, cpuUsage, memoryUsage float32) *plugin_pb.PluginMessage {
|
||||
return &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Heartbeat{
|
||||
Heartbeat: &plugin_pb.PluginHeartbeat{
|
||||
PluginId: pluginID,
|
||||
Timestamp: timestamppb.Now(),
|
||||
UptimeSeconds: 3600,
|
||||
PendingJobs: pendingJobs,
|
||||
CpuUsagePercent: cpuUsage,
|
||||
MemoryUsageMb: memoryUsage,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateJobStartedMessage creates a job started message
|
||||
func CreateJobStartedMessage(jobID, executorID string) *plugin_pb.JobExecutionMessage {
|
||||
return &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_JobStarted{
|
||||
JobStarted: &plugin_pb.JobStarted{
|
||||
StartedAt: timestamppb.Now(),
|
||||
ExecutorId: executorID,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateJobProgressMessage creates a job progress message
|
||||
func CreateJobProgressMessage(jobID string, progressPercent int32, currentStep, statusMessage string) *plugin_pb.JobExecutionMessage {
|
||||
return &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_Progress{
|
||||
Progress: &plugin_pb.JobProgress{
|
||||
ProgressPercent: progressPercent,
|
||||
CurrentStep: currentStep,
|
||||
StatusMessage: statusMessage,
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateJobCompletedMessage creates a job completed message
|
||||
func CreateJobCompletedMessage(jobID, summary string, output map[string]string) *plugin_pb.JobExecutionMessage {
|
||||
return &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_JobCompleted{
|
||||
JobCompleted: &plugin_pb.JobCompleted{
|
||||
CompletedAt: timestamppb.Now(),
|
||||
Summary: summary,
|
||||
Output: output,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateJobFailedMessage creates a job failed message
|
||||
func CreateJobFailedMessage(jobID, errorCode, errorMessage string, retryable bool) *plugin_pb.JobExecutionMessage {
|
||||
return &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_JobFailed{
|
||||
JobFailed: &plugin_pb.JobFailed{
|
||||
ErrorCode: errorCode,
|
||||
ErrorMessage: errorMessage,
|
||||
Retryable: retryable,
|
||||
FailedAt: timestamppb.Now(),
|
||||
RetryCount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// MockAdminServer implements the PluginService for testing
|
||||
type MockAdminServer struct {
|
||||
mu sync.RWMutex
|
||||
receivedPluginMessages []*plugin_pb.PluginMessage
|
||||
receivedJobMessages []*plugin_pb.JobExecutionMessage
|
||||
jobResponses map[string][]*plugin_pb.JobProgressMessage
|
||||
pluginResponses map[string][]*plugin_pb.AdminMessage
|
||||
streams map[string]*MockStream
|
||||
closed bool
|
||||
}
|
||||
|
||||
// MockStream represents a bidirectional stream
|
||||
type MockStream struct {
|
||||
mu sync.RWMutex
|
||||
messages chan interface{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewMockAdminServer creates a new mock admin server
|
||||
func NewMockAdminServer() *MockAdminServer {
|
||||
return &MockAdminServer{
|
||||
receivedPluginMessages: make([]*plugin_pb.PluginMessage, 0),
|
||||
receivedJobMessages: make([]*plugin_pb.JobExecutionMessage, 0),
|
||||
jobResponses: make(map[string][]*plugin_pb.JobProgressMessage),
|
||||
pluginResponses: make(map[string][]*plugin_pb.AdminMessage),
|
||||
streams: make(map[string]*MockStream),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect implements the Connect RPC - bidirectional stream for plugin registration
|
||||
func (m *MockAdminServer) Connect(stream plugin_pb.PluginService_ConnectServer) error {
|
||||
m.mu.Lock()
|
||||
if m.closed {
|
||||
m.mu.Unlock()
|
||||
return io.EOF
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Receive messages from plugin
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.receivedPluginMessages = append(m.receivedPluginMessages, msg)
|
||||
m.mu.Unlock()
|
||||
|
||||
// Send response if available
|
||||
var response *plugin_pb.AdminMessage
|
||||
if msg.GetRegister() != nil {
|
||||
response = &plugin_pb.AdminMessage{
|
||||
Content: &plugin_pb.AdminMessage_ConfigUpdate{
|
||||
ConfigUpdate: &plugin_pb.ConfigUpdate{
|
||||
JobType: "test_job_type",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if response != nil {
|
||||
err = stream.Send(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteJob implements the ExecuteJob RPC - bidirectional stream for job execution
|
||||
func (m *MockAdminServer) ExecuteJob(stream plugin_pb.PluginService_ExecuteJobServer) error {
|
||||
m.mu.Lock()
|
||||
if m.closed {
|
||||
m.mu.Unlock()
|
||||
return io.EOF
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Receive execution messages from plugin
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.receivedJobMessages = append(m.receivedJobMessages, msg)
|
||||
m.mu.Unlock()
|
||||
|
||||
// Send progress update if available
|
||||
jobID := msg.JobId
|
||||
m.mu.RLock()
|
||||
responses, ok := m.jobResponses[jobID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if ok && len(responses) > 0 {
|
||||
response := responses[0]
|
||||
err = stream.Send(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.jobResponses[jobID] = responses[1:]
|
||||
m.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddJobResponse adds a pre-configured response for a specific job
|
||||
func (m *MockAdminServer) AddJobResponse(jobID string, response *plugin_pb.JobProgressMessage) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.jobResponses[jobID] = append(m.jobResponses[jobID], response)
|
||||
}
|
||||
|
||||
// AddPluginResponse adds a pre-configured response for plugin messages
|
||||
func (m *MockAdminServer) AddPluginResponse(response *plugin_pb.AdminMessage) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Store responses indexed by a generic key for now
|
||||
key := "default"
|
||||
m.pluginResponses[key] = append(m.pluginResponses[key], response)
|
||||
}
|
||||
|
||||
// GetReceivedPluginMessages returns all received plugin messages
|
||||
func (m *MockAdminServer) GetReceivedPluginMessages() []*plugin_pb.PluginMessage {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Return a copy
|
||||
result := make([]*plugin_pb.PluginMessage, len(m.receivedPluginMessages))
|
||||
copy(result, m.receivedPluginMessages)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetReceivedJobMessages returns all received job execution messages
|
||||
func (m *MockAdminServer) GetReceivedJobMessages() []*plugin_pb.JobExecutionMessage {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Return a copy
|
||||
result := make([]*plugin_pb.JobExecutionMessage, len(m.receivedJobMessages))
|
||||
copy(result, m.receivedJobMessages)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetReceivedJobMessages returns job execution messages filtered by job ID
|
||||
func (m *MockAdminServer) GetJobMessages(jobID string) []*plugin_pb.JobExecutionMessage {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var result []*plugin_pb.JobExecutionMessage
|
||||
for _, msg := range m.receivedJobMessages {
|
||||
if msg.JobId == jobID {
|
||||
result = append(result, msg)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// CountReceivedMessages returns the count of received plugin messages
|
||||
func (m *MockAdminServer) CountReceivedMessages() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return len(m.receivedPluginMessages)
|
||||
}
|
||||
|
||||
// CountReceivedJobMessages returns the count of received job execution messages
|
||||
func (m *MockAdminServer) CountReceivedJobMessages() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return len(m.receivedJobMessages)
|
||||
}
|
||||
|
||||
// ClearMessages clears all recorded messages
|
||||
func (m *MockAdminServer) ClearMessages() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.receivedPluginMessages = make([]*plugin_pb.PluginMessage, 0)
|
||||
m.receivedJobMessages = make([]*plugin_pb.JobExecutionMessage, 0)
|
||||
}
|
||||
|
||||
// Close closes the mock server
|
||||
func (m *MockAdminServer) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsClosed returns whether the server is closed
|
||||
func (m *MockAdminServer) IsClosed() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return m.closed
|
||||
}
|
||||
|
||||
// VerifyPluginRegistration verifies that a plugin registration was received
|
||||
func (m *MockAdminServer) VerifyPluginRegistration(pluginID string) error {
|
||||
messages := m.GetReceivedPluginMessages()
|
||||
for _, msg := range messages {
|
||||
if reg := msg.GetRegister(); reg != nil && reg.PluginId == pluginID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
// VerifyJobExecution verifies that job execution messages were received for a job
|
||||
func (m *MockAdminServer) VerifyJobExecution(jobID string) error {
|
||||
messages := m.GetJobMessages(jobID)
|
||||
if len(messages) == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyJobCompletion verifies that a job completion message was received
|
||||
func (m *MockAdminServer) VerifyJobCompletion(jobID string) error {
|
||||
messages := m.GetJobMessages(jobID)
|
||||
for _, msg := range messages {
|
||||
if msg.GetJobCompleted() != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
// VerifyJobFailure verifies that a job failure message was received
|
||||
func (m *MockAdminServer) VerifyJobFailure(jobID string) error {
|
||||
messages := m.GetJobMessages(jobID)
|
||||
for _, msg := range messages {
|
||||
if msg.GetJobFailed() != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
package testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// MockPlugin simulates a plugin worker for testing
|
||||
type MockPlugin struct {
|
||||
ID string
|
||||
Name string
|
||||
Version string
|
||||
ProtocolVersion string
|
||||
Capabilities []*plugin_pb.JobTypeCapability
|
||||
|
||||
// Configuration
|
||||
DetectionEnabled bool
|
||||
ExecutionEnabled bool
|
||||
FailureMode string // "" = success, "detection_error", "execution_error"
|
||||
DetectionDelay time.Duration
|
||||
ExecutionDelay time.Duration
|
||||
|
||||
// Tracking
|
||||
mu sync.RWMutex
|
||||
detectedJobs map[string]*plugin_pb.DetectedJob
|
||||
executedJobs map[string]*JobExecution
|
||||
registrationTime time.Time
|
||||
callCount map[string]int
|
||||
errors []string
|
||||
}
|
||||
|
||||
// JobExecution tracks execution of a job
|
||||
type JobExecution struct {
|
||||
JobID string
|
||||
Config []*plugin_pb.ConfigFieldValue
|
||||
Status string
|
||||
ProgressPercent int32
|
||||
Messages []*plugin_pb.JobExecutionMessage
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
ErrorInfo *plugin_pb.JobFailed
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMockPlugin creates a new mock plugin
|
||||
func NewMockPlugin(id, name, version string) *MockPlugin {
|
||||
return &MockPlugin{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Version: version,
|
||||
ProtocolVersion: "v1",
|
||||
Capabilities: make([]*plugin_pb.JobTypeCapability, 0),
|
||||
DetectionEnabled: true,
|
||||
ExecutionEnabled: true,
|
||||
DetectionDelay: 100 * time.Millisecond,
|
||||
ExecutionDelay: 100 * time.Millisecond,
|
||||
detectedJobs: make(map[string]*plugin_pb.DetectedJob),
|
||||
executedJobs: make(map[string]*JobExecution),
|
||||
registrationTime: time.Now(),
|
||||
callCount: make(map[string]int),
|
||||
errors: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddCapability adds a job type capability to the plugin
|
||||
func (mp *MockPlugin) AddCapability(jobType string, canDetect, canExecute bool) {
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
capability := &plugin_pb.JobTypeCapability{
|
||||
JobType: jobType,
|
||||
CanDetect: canDetect,
|
||||
CanExecute: canExecute,
|
||||
Version: "v1",
|
||||
}
|
||||
mp.Capabilities = append(mp.Capabilities, capability)
|
||||
}
|
||||
|
||||
// GetRegistrationMessage returns the plugin registration message
|
||||
func (mp *MockPlugin) GetRegistrationMessage() *plugin_pb.PluginMessage {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
|
||||
return &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Register{
|
||||
Register: &plugin_pb.PluginRegister{
|
||||
PluginId: mp.ID,
|
||||
Name: mp.Name,
|
||||
Version: mp.Version,
|
||||
ProtocolVersion: mp.ProtocolVersion,
|
||||
Capabilities: mp.Capabilities,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetHeartbeatMessage returns a heartbeat message
|
||||
func (mp *MockPlugin) GetHeartbeatMessage(pendingJobs, runningJobs int32, cpuUsage, memoryUsage float32) *plugin_pb.PluginMessage {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
|
||||
uptime := int64(time.Since(mp.registrationTime).Seconds())
|
||||
|
||||
return &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Heartbeat{
|
||||
Heartbeat: &plugin_pb.PluginHeartbeat{
|
||||
PluginId: mp.ID,
|
||||
Timestamp: timestamppb.Now(),
|
||||
UptimeSeconds: uptime,
|
||||
PendingJobs: pendingJobs,
|
||||
CpuUsagePercent: cpuUsage,
|
||||
MemoryUsageMb: memoryUsage,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SimulateDetection simulates job detection
|
||||
func (mp *MockPlugin) SimulateDetection(jobType string, detectedCount int) ([]*plugin_pb.DetectedJob, error) {
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
mp.callCount["detection"]++
|
||||
|
||||
if !mp.DetectionEnabled {
|
||||
err := fmt.Errorf("detection disabled for plugin %s", mp.ID)
|
||||
mp.errors = append(mp.errors, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mp.FailureMode == "detection_error" {
|
||||
err := fmt.Errorf("simulated detection error")
|
||||
mp.errors = append(mp.errors, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
time.Sleep(mp.DetectionDelay)
|
||||
|
||||
var jobs []*plugin_pb.DetectedJob
|
||||
now := time.Now()
|
||||
|
||||
for i := 0; i < detectedCount; i++ {
|
||||
jobKey := fmt.Sprintf("detected-job-%s-%d-%d", jobType, now.Unix(), i)
|
||||
job := &plugin_pb.DetectedJob{
|
||||
JobKey: jobKey,
|
||||
JobType: jobType,
|
||||
Description: fmt.Sprintf("Detected %s job %d", jobType, i),
|
||||
Priority: int64(10 - i),
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
|
||||
jobKey2 := fmt.Sprintf("%s-%d", jobType, i)
|
||||
mp.detectedJobs[jobKey2] = job
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
// SimulateExecution simulates job execution
|
||||
func (mp *MockPlugin) SimulateExecution(jobID, jobType string, config []*plugin_pb.ConfigFieldValue) (*JobExecution, error) {
|
||||
mp.mu.Lock()
|
||||
|
||||
mp.callCount["execution"]++
|
||||
|
||||
if !mp.ExecutionEnabled {
|
||||
err := fmt.Errorf("execution disabled for plugin %s", mp.ID)
|
||||
mp.errors = append(mp.errors, err.Error())
|
||||
mp.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mp.FailureMode == "execution_error" {
|
||||
err := fmt.Errorf("simulated execution error")
|
||||
mp.errors = append(mp.errors, err.Error())
|
||||
mp.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
execution := &JobExecution{
|
||||
JobID: jobID,
|
||||
Config: config,
|
||||
Status: "running",
|
||||
Messages: make([]*plugin_pb.JobExecutionMessage, 0),
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
mp.executedJobs[jobID] = execution
|
||||
mp.mu.Unlock()
|
||||
|
||||
// Simulate progress updates
|
||||
for progress := 0; progress <= 100; progress += 25 {
|
||||
time.Sleep(mp.ExecutionDelay)
|
||||
|
||||
mp.mu.Lock()
|
||||
if exec, ok := mp.executedJobs[jobID]; ok {
|
||||
exec.mu.Lock()
|
||||
exec.ProgressPercent = int32(progress)
|
||||
|
||||
msg := &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_Progress{
|
||||
Progress: &plugin_pb.JobProgress{
|
||||
ProgressPercent: int32(progress),
|
||||
CurrentStep: fmt.Sprintf("Step %d", progress/25),
|
||||
StatusMessage: fmt.Sprintf("Executing step at %d%%", progress),
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
exec.Messages = append(exec.Messages, msg)
|
||||
exec.mu.Unlock()
|
||||
}
|
||||
mp.mu.Unlock()
|
||||
}
|
||||
|
||||
mp.mu.Lock()
|
||||
if exec, ok := mp.executedJobs[jobID]; ok {
|
||||
exec.mu.Lock()
|
||||
exec.Status = "completed"
|
||||
exec.ProgressPercent = 100
|
||||
exec.EndTime = time.Now()
|
||||
|
||||
completed := &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_JobCompleted{
|
||||
JobCompleted: &plugin_pb.JobCompleted{
|
||||
CompletedAt: timestamppb.Now(),
|
||||
Summary: fmt.Sprintf("Job %s completed successfully", jobID),
|
||||
Output: map[string]string{
|
||||
"result": "success",
|
||||
"job_id": jobID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
exec.Messages = append(exec.Messages, completed)
|
||||
exec.mu.Unlock()
|
||||
}
|
||||
mp.mu.Unlock()
|
||||
|
||||
return execution, nil
|
||||
}
|
||||
|
||||
// SimulateExecutionFailure simulates a job execution failure
|
||||
func (mp *MockPlugin) SimulateExecutionFailure(jobID string, errorCode, errorMessage string, retryable bool) (*JobExecution, error) {
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
execution := &JobExecution{
|
||||
JobID: jobID,
|
||||
Status: "failed",
|
||||
Messages: make([]*plugin_pb.JobExecutionMessage, 0),
|
||||
StartTime: time.Now(),
|
||||
EndTime: time.Now(),
|
||||
}
|
||||
|
||||
failedMsg := &plugin_pb.JobFailed{
|
||||
ErrorCode: errorCode,
|
||||
ErrorMessage: errorMessage,
|
||||
Retryable: retryable,
|
||||
FailedAt: timestamppb.Now(),
|
||||
RetryCount: 0,
|
||||
}
|
||||
|
||||
msg := &plugin_pb.JobExecutionMessage{
|
||||
JobId: jobID,
|
||||
Content: &plugin_pb.JobExecutionMessage_JobFailed{
|
||||
JobFailed: failedMsg,
|
||||
},
|
||||
}
|
||||
|
||||
execution.Messages = append(execution.Messages, msg)
|
||||
execution.ErrorInfo = failedMsg
|
||||
mp.executedJobs[jobID] = execution
|
||||
|
||||
return execution, nil
|
||||
}
|
||||
|
||||
// GetExecutionMessages returns execution messages for a job
|
||||
func (mp *MockPlugin) GetExecutionMessages(jobID string) []*plugin_pb.JobExecutionMessage {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
|
||||
if exec, ok := mp.executedJobs[jobID]; ok {
|
||||
exec.mu.RLock()
|
||||
defer exec.mu.RUnlock()
|
||||
|
||||
result := make([]*plugin_pb.JobExecutionMessage, len(exec.Messages))
|
||||
copy(result, exec.Messages)
|
||||
return result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCallCount returns the number of times a method was called
|
||||
func (mp *MockPlugin) GetCallCount(method string) int {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
|
||||
return mp.callCount[method]
|
||||
}
|
||||
|
||||
// GetErrors returns all recorded errors
|
||||
func (mp *MockPlugin) GetErrors() []string {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
|
||||
result := make([]string, len(mp.errors))
|
||||
copy(result, mp.errors)
|
||||
return result
|
||||
}
|
||||
|
||||
// SetFailureMode sets the failure simulation mode
|
||||
func (mp *MockPlugin) SetFailureMode(mode string) {
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
mp.FailureMode = mode
|
||||
}
|
||||
|
||||
// SetDetectionDelay sets the detection simulation delay
|
||||
func (mp *MockPlugin) SetDetectionDelay(delay time.Duration) {
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
mp.DetectionDelay = delay
|
||||
}
|
||||
|
||||
// SetExecutionDelay sets the execution simulation delay
|
||||
func (mp *MockPlugin) SetExecutionDelay(delay time.Duration) {
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
mp.ExecutionDelay = delay
|
||||
}
|
||||
|
||||
// Reset clears all state
|
||||
func (mp *MockPlugin) Reset() {
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
mp.detectedJobs = make(map[string]*plugin_pb.DetectedJob)
|
||||
mp.executedJobs = make(map[string]*JobExecution)
|
||||
mp.callCount = make(map[string]int)
|
||||
mp.errors = make([]string, 0)
|
||||
mp.FailureMode = ""
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// JobState represents the current state of a job
|
||||
type JobState int32
|
||||
|
||||
const (
|
||||
JobStatePending JobState = 0
|
||||
JobStateRunning JobState = 1
|
||||
JobStateCompleted JobState = 2
|
||||
JobStateFailed JobState = 3
|
||||
JobStateCancelled JobState = 4
|
||||
JobStatePaused JobState = 5
|
||||
)
|
||||
|
||||
// String returns string representation of JobState
|
||||
func (s JobState) String() string {
|
||||
switch s {
|
||||
case JobStatePending:
|
||||
return "pending"
|
||||
case JobStateRunning:
|
||||
return "running"
|
||||
case JobStateCompleted:
|
||||
return "completed"
|
||||
case JobStateFailed:
|
||||
return "failed"
|
||||
case JobStateCancelled:
|
||||
return "cancelled"
|
||||
case JobStatePaused:
|
||||
return "paused"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// PluginState represents the current state of a plugin connection
|
||||
type PluginState int32
|
||||
|
||||
const (
|
||||
PluginStateConnecting PluginState = 0
|
||||
PluginStateConnected PluginState = 1
|
||||
PluginStateHealthy PluginState = 2
|
||||
PluginStateUnhealthy PluginState = 3
|
||||
PluginStateDisconnected PluginState = 4
|
||||
)
|
||||
|
||||
// String returns string representation of PluginState
|
||||
func (s PluginState) String() string {
|
||||
switch s {
|
||||
case PluginStateConnecting:
|
||||
return "connecting"
|
||||
case PluginStateConnected:
|
||||
return "connected"
|
||||
case PluginStateHealthy:
|
||||
return "healthy"
|
||||
case PluginStateUnhealthy:
|
||||
return "unhealthy"
|
||||
case PluginStateDisconnected:
|
||||
return "disconnected"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Job represents a work unit in the plugin system
|
||||
type Job struct {
|
||||
ID string
|
||||
Type string
|
||||
Description string
|
||||
Priority int64
|
||||
State JobState
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ExecutorID string
|
||||
ProgressPercent int32
|
||||
Config []*plugin_pb.ConfigFieldValue
|
||||
Metadata map[string]string
|
||||
|
||||
// Execution tracking
|
||||
Retries int32
|
||||
LastRetryTime time.Time
|
||||
CompletedInfo *plugin_pb.JobCompleted
|
||||
FailedInfo *plugin_pb.JobFailed
|
||||
CheckpointIDs []string
|
||||
LogEntries []*plugin_pb.ExecutionLogEntry
|
||||
|
||||
// Lock for thread-safe access
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// ConnectedPlugin represents a connected plugin worker
|
||||
type ConnectedPlugin struct {
|
||||
ID string
|
||||
Name string
|
||||
Version string
|
||||
ProtocolVersion string
|
||||
ConnectedAt time.Time
|
||||
LastHeartbeat time.Time
|
||||
State PluginState
|
||||
Healthy bool
|
||||
|
||||
// Capabilities indexed by job type
|
||||
Capabilities map[string]*plugin_pb.JobTypeCapability
|
||||
|
||||
// Current workload
|
||||
PendingJobs int32
|
||||
RunningJobs int32
|
||||
|
||||
// Resource usage
|
||||
CPUUsagePercent float32
|
||||
MemoryUsageMB float32
|
||||
|
||||
// Communication stream
|
||||
StreamConnected bool
|
||||
|
||||
// Lock for thread-safe access
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// JobTypeConfig holds configuration for a specific job type
|
||||
type JobTypeConfig struct {
|
||||
JobType string
|
||||
Enabled bool
|
||||
AdminConfig []*plugin_pb.ConfigFieldValue
|
||||
WorkerConfig []*plugin_pb.ConfigFieldValue
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
CreatedBy string
|
||||
|
||||
// History tracking
|
||||
DetectionHistory []*DetectionRecord
|
||||
ExecutionHistory []*ExecutionRecord
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// DetectionRecord holds information about a detection run
|
||||
type DetectionRecord struct {
|
||||
Timestamp time.Time
|
||||
DurationMs int64
|
||||
JobsDetected int
|
||||
Status string
|
||||
Error string
|
||||
WorkerID string
|
||||
DetailedLogs []string
|
||||
}
|
||||
|
||||
// ExecutionRecord holds information about a job execution
|
||||
type ExecutionRecord struct {
|
||||
JobID string
|
||||
JobType string
|
||||
StartedAt time.Time
|
||||
CompletedAt time.Time
|
||||
DurationMs int64
|
||||
Success bool
|
||||
Error string
|
||||
WorkerID string
|
||||
ItemsProcessed int
|
||||
ExecutionLogs []string
|
||||
}
|
||||
|
||||
// GetState safely gets the job state
|
||||
func (j *Job) GetState() JobState {
|
||||
j.mu.RLock()
|
||||
defer j.mu.RUnlock()
|
||||
return j.State
|
||||
}
|
||||
|
||||
// SetState safely sets the job state
|
||||
func (j *Job) SetState(state JobState) {
|
||||
j.mu.Lock()
|
||||
defer j.mu.Unlock()
|
||||
j.State = state
|
||||
j.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// GetProgress safely gets progress
|
||||
func (j *Job) GetProgress() int32 {
|
||||
j.mu.RLock()
|
||||
defer j.mu.RUnlock()
|
||||
return j.ProgressPercent
|
||||
}
|
||||
|
||||
// SetProgress safely sets progress
|
||||
func (j *Job) SetProgress(percent int32) {
|
||||
j.mu.Lock()
|
||||
defer j.mu.Unlock()
|
||||
j.ProgressPercent = percent
|
||||
j.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// GetState safely gets plugin state
|
||||
func (p *ConnectedPlugin) GetState() PluginState {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.State
|
||||
}
|
||||
|
||||
// SetState safely sets plugin state
|
||||
func (p *ConnectedPlugin) SetState(state PluginState) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.State = state
|
||||
p.LastHeartbeat = time.Now()
|
||||
}
|
||||
|
||||
// IsHealthy safely checks if plugin is healthy
|
||||
func (p *ConnectedPlugin) IsHealthy() bool {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
return p.Healthy
|
||||
}
|
||||
|
||||
// UpdateHeartbeat safely updates heartbeat
|
||||
func (p *ConnectedPlugin) UpdateHeartbeat(cpu, memory float32, pending, running int32) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.LastHeartbeat = time.Now()
|
||||
p.CPUUsagePercent = cpu
|
||||
p.MemoryUsageMB = memory
|
||||
p.PendingJobs = pending
|
||||
p.RunningJobs = running
|
||||
}
|
||||
|
||||
// GetConfig safely gets configuration
|
||||
func (c *JobTypeConfig) GetConfig() (*plugin_pb.JobTypeConfig, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return &plugin_pb.JobTypeConfig{
|
||||
JobType: c.JobType,
|
||||
Enabled: c.Enabled,
|
||||
AdminConfig: c.AdminConfig,
|
||||
WorkerConfig: c.WorkerConfig,
|
||||
CreatedAt: timestamppb.New(c.CreatedAt),
|
||||
UpdatedAt: timestamppb.New(c.UpdatedAt),
|
||||
CreatedBy: c.CreatedBy,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetConfig safely sets configuration
|
||||
func (c *JobTypeConfig) SetConfig(enabled bool, adminConfig, workerConfig []*plugin_pb.ConfigFieldValue) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.Enabled = enabled
|
||||
c.AdminConfig = adminConfig
|
||||
c.WorkerConfig = workerConfig
|
||||
c.UpdatedAt = time.Now()
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
// Detector scans for volume distribution imbalance across servers
|
||||
type Detector struct {
|
||||
masterAddr string
|
||||
}
|
||||
|
||||
// ServerVolumeInfo represents volume count on a server
|
||||
type ServerVolumeInfo struct {
|
||||
ServerID string
|
||||
Rack string
|
||||
VolumeCount int32
|
||||
TotalVolumeGB int64
|
||||
AvailableGB int64
|
||||
WriteableCount int32
|
||||
}
|
||||
|
||||
// VolumeInfo represents a volume that can be migrated
|
||||
type VolumeInfo struct {
|
||||
ID string
|
||||
Collection string
|
||||
SizeGB int64
|
||||
SourceServer string
|
||||
TargetServer string
|
||||
Replicas int32
|
||||
IsWriteable bool
|
||||
LastModifiedAt time.Time
|
||||
}
|
||||
|
||||
// NewDetector creates a new balance detector
|
||||
func NewDetector(masterAddr string) *Detector {
|
||||
return &Detector{
|
||||
masterAddr: masterAddr,
|
||||
}
|
||||
}
|
||||
|
||||
// DetectJobs identifies volume distribution imbalance and returns migration jobs
|
||||
// Returns DetectedJob items with priority based on imbalance severity
|
||||
func (d *Detector) DetectJobs(ctx context.Context, config *plugin_pb.JobTypeConfig) ([]*plugin_pb.DetectedJob, error) {
|
||||
var detectedJobs []*plugin_pb.DetectedJob
|
||||
|
||||
// Extract configuration parameters
|
||||
imbalanceThreshold := float64(20) // default 20%
|
||||
minServers := int32(2) // default 2
|
||||
preferRackDiversity := false // default false
|
||||
|
||||
for _, cfv := range config.AdminConfig {
|
||||
if cfv.FieldName == "imbalanceThreshold" {
|
||||
imbalanceThreshold = float64(cfv.IntValue)
|
||||
} else if cfv.FieldName == "minServers" {
|
||||
minServers = int32(cfv.IntValue)
|
||||
} else if cfv.FieldName == "preferRackDiversity" {
|
||||
preferRackDiversity = cfv.BoolValue
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("balance detector: scanning volume distribution (threshold=%.1f%%, minServers=%d, rackDiversity=%v)",
|
||||
imbalanceThreshold, minServers, preferRackDiversity)
|
||||
|
||||
// Get server and volume information
|
||||
serverInfo, volumes := d.scanServerVolumes(ctx)
|
||||
|
||||
if int32(len(serverInfo)) < minServers {
|
||||
glog.Infof("balance detector: insufficient servers (%d < %d), skipping detection",
|
||||
len(serverInfo), minServers)
|
||||
return detectedJobs, nil
|
||||
}
|
||||
|
||||
// Calculate volume distribution imbalance
|
||||
imbalance, maxServer, minServer := d.calculateImbalance(serverInfo)
|
||||
|
||||
glog.Infof("balance detector: current imbalance=%.1f%%, max=%d volumes on %s, min=%d volumes on %s",
|
||||
imbalance, maxServer.VolumeCount, maxServer.ServerID, minServer.VolumeCount, minServer.ServerID)
|
||||
|
||||
// Check if imbalance exceeds threshold
|
||||
if imbalance <= imbalanceThreshold {
|
||||
glog.Infof("balance detector: imbalance %.1f%% is within threshold %.1f%%, no rebalancing needed",
|
||||
imbalance, imbalanceThreshold)
|
||||
return detectedJobs, nil
|
||||
}
|
||||
|
||||
glog.Infof("balance detector: imbalance %.1f%% exceeds threshold %.1f%%, triggering rebalancing",
|
||||
imbalance, imbalanceThreshold)
|
||||
|
||||
// Generate migration jobs to reduce imbalance
|
||||
migrations := d.generateMigrations(serverInfo, volumes, preferRackDiversity)
|
||||
|
||||
now := time.Now()
|
||||
for i, migration := range migrations {
|
||||
priority := int64((imbalance * 1000)) - int64(i*10) // Higher imbalance = higher priority
|
||||
|
||||
jobKey := fmt.Sprintf("balance_%s_to_%s_%s", migration.SourceServer, migration.TargetServer, now.Format("20060102150405"))
|
||||
|
||||
job := &plugin_pb.DetectedJob{
|
||||
JobKey: jobKey,
|
||||
JobType: "balance",
|
||||
Description: fmt.Sprintf("Migrate volume %s from %s to %s (imbalance reduction: %.1f%%)",
|
||||
migration.ID, migration.SourceServer, migration.TargetServer, imbalance),
|
||||
Priority: priority,
|
||||
EstimatedDuration: durationpb.New(time.Duration(migration.SizeGB) * time.Second),
|
||||
Metadata: map[string]string{
|
||||
"volume_id": migration.ID,
|
||||
"collection": migration.Collection,
|
||||
"source_server": migration.SourceServer,
|
||||
"target_server": migration.TargetServer,
|
||||
"volume_size_gb": fmt.Sprintf("%d", migration.SizeGB),
|
||||
"imbalance": fmt.Sprintf("%.2f", imbalance),
|
||||
"rack_diversity": fmt.Sprintf("%v", preferRackDiversity),
|
||||
},
|
||||
SuggestedConfig: []*plugin_pb.ConfigFieldValue{},
|
||||
}
|
||||
|
||||
detectedJobs = append(detectedJobs, job)
|
||||
glog.Infof("balance detector: detected migration job %s (priority=%d)", migration.ID, priority)
|
||||
}
|
||||
|
||||
glog.Infof("balance detector: found %d migration jobs to reduce imbalance", len(detectedJobs))
|
||||
return detectedJobs, nil
|
||||
}
|
||||
|
||||
// calculateImbalance calculates volume distribution imbalance
|
||||
// imbalance = (max_volumes - min_volumes) / avg_volumes * 100
|
||||
func (d *Detector) calculateImbalance(servers []ServerVolumeInfo) (imbalance float64, maxServer, minServer ServerVolumeInfo) {
|
||||
if len(servers) == 0 {
|
||||
return 0, ServerVolumeInfo{}, ServerVolumeInfo{}
|
||||
}
|
||||
|
||||
// Sort by volume count to find min and max
|
||||
sorted := make([]ServerVolumeInfo, len(servers))
|
||||
copy(sorted, servers)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].VolumeCount < sorted[j].VolumeCount
|
||||
})
|
||||
|
||||
minServer = sorted[0]
|
||||
maxServer = sorted[len(sorted)-1]
|
||||
|
||||
// Calculate average
|
||||
totalVolumes := int32(0)
|
||||
for _, s := range servers {
|
||||
totalVolumes += s.VolumeCount
|
||||
}
|
||||
|
||||
avgVolumes := float64(totalVolumes) / float64(len(servers))
|
||||
|
||||
// Avoid division by zero
|
||||
if avgVolumes == 0 {
|
||||
return 0, maxServer, minServer
|
||||
}
|
||||
|
||||
// Calculate imbalance percentage
|
||||
imbalance = float64(maxServer.VolumeCount-minServer.VolumeCount) / avgVolumes * 100
|
||||
|
||||
// Ensure it's not negative
|
||||
if imbalance < 0 {
|
||||
imbalance = 0
|
||||
}
|
||||
|
||||
return imbalance, maxServer, minServer
|
||||
}
|
||||
|
||||
// generateMigrations generates a list of volume migrations to reduce imbalance
|
||||
func (d *Detector) generateMigrations(servers []ServerVolumeInfo, volumes []VolumeInfo, preferRackDiversity bool) []VolumeInfo {
|
||||
var migrations []VolumeInfo
|
||||
|
||||
// Sort servers by volume count (descending)
|
||||
serversCopy := make([]ServerVolumeInfo, len(servers))
|
||||
copy(serversCopy, servers)
|
||||
sort.Slice(serversCopy, func(i, j int) bool {
|
||||
return serversCopy[i].VolumeCount > serversCopy[j].VolumeCount
|
||||
})
|
||||
|
||||
// Calculate target volume count (average)
|
||||
totalVolumes := int32(0)
|
||||
for _, s := range serversCopy {
|
||||
totalVolumes += s.VolumeCount
|
||||
}
|
||||
targetPerServer := totalVolumes / int32(len(serversCopy))
|
||||
|
||||
// For each overloaded server, select volumes to migrate
|
||||
for _, sourceServer := range serversCopy {
|
||||
if sourceServer.VolumeCount <= targetPerServer {
|
||||
break // Rest are balanced
|
||||
}
|
||||
|
||||
volumesToMove := sourceServer.VolumeCount - targetPerServer
|
||||
|
||||
// Find candidate volumes on this server
|
||||
for _, vol := range volumes {
|
||||
if volumesToMove <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if vol.SourceServer != sourceServer.ServerID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip read-only volumes
|
||||
if !vol.IsWriteable {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find a target server (underloaded, different if rack diversity preferred)
|
||||
targetServer := d.selectTargetServer(serversCopy, sourceServer, preferRackDiversity)
|
||||
if targetServer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
migration := VolumeInfo{
|
||||
ID: vol.ID,
|
||||
Collection: vol.Collection,
|
||||
SizeGB: vol.SizeGB,
|
||||
SourceServer: sourceServer.ServerID,
|
||||
TargetServer: targetServer.ServerID,
|
||||
Replicas: vol.Replicas,
|
||||
IsWriteable: vol.IsWriteable,
|
||||
}
|
||||
|
||||
migrations = append(migrations, migration)
|
||||
volumesToMove--
|
||||
|
||||
// Update server counts
|
||||
sourceServer.VolumeCount--
|
||||
targetServer.VolumeCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by volume size (largest first for priority)
|
||||
sort.Slice(migrations, func(i, j int) bool {
|
||||
return migrations[i].SizeGB > migrations[j].SizeGB
|
||||
})
|
||||
|
||||
return migrations
|
||||
}
|
||||
|
||||
// selectTargetServer finds a suitable target server for migration
|
||||
func (d *Detector) selectTargetServer(servers []ServerVolumeInfo, sourceServer ServerVolumeInfo, preferRackDiversity bool) *ServerVolumeInfo {
|
||||
// Sort by volume count (ascending) - pick least loaded
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].VolumeCount < servers[j].VolumeCount
|
||||
})
|
||||
|
||||
for i := range servers {
|
||||
server := &servers[i]
|
||||
|
||||
// Skip source server
|
||||
if server.ServerID == sourceServer.ServerID {
|
||||
continue
|
||||
}
|
||||
|
||||
// If rack diversity preferred, try to pick different rack
|
||||
if preferRackDiversity && server.Rack == sourceServer.Rack {
|
||||
continue
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// If rack diversity is preferred but all servers are in same rack, fallback to least loaded
|
||||
if preferRackDiversity {
|
||||
for i := range servers {
|
||||
server := &servers[i]
|
||||
if server.ServerID != sourceServer.ServerID {
|
||||
return server
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanServerVolumes performs a scan of servers and volumes from the master
|
||||
// This is a placeholder that would connect to master in production
|
||||
func (d *Detector) scanServerVolumes(ctx context.Context) ([]ServerVolumeInfo, []VolumeInfo) {
|
||||
// TODO: Connect to master server at d.masterAddr and get:
|
||||
// 1. List of data nodes with their rack information
|
||||
// 2. Volume distribution across nodes
|
||||
// 3. Volume metadata (size, collection, writeable status)
|
||||
//
|
||||
// For now, return empty lists as a framework
|
||||
var servers []ServerVolumeInfo
|
||||
var volumes []VolumeInfo
|
||||
|
||||
return servers, volumes
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Executor handles the 5-step volume migration execution process
|
||||
type Executor struct {
|
||||
jobID string
|
||||
sourceServer string
|
||||
targetServer string
|
||||
volumeID string
|
||||
config *plugin_pb.JobTypeConfig
|
||||
parallelMigrations int32
|
||||
maxBytesPerSecond int64
|
||||
}
|
||||
|
||||
// ExecutionStep represents a single step in the migration process
|
||||
type ExecutionStep struct {
|
||||
StepNumber int
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// MigrationState tracks the state of a migration
|
||||
type MigrationState struct {
|
||||
mu sync.Mutex
|
||||
volumeReadonly bool
|
||||
volumeCopied bool
|
||||
volumeMounted bool
|
||||
tailStarted bool
|
||||
sourceDeleted bool
|
||||
bytesTransferred int64
|
||||
}
|
||||
|
||||
// NewExecutor creates a new balance executor
|
||||
func NewExecutor(jobID string, config *plugin_pb.JobTypeConfig) *Executor {
|
||||
parallelMigrations := int32(2)
|
||||
maxBytesPerSecond := int64(0)
|
||||
|
||||
for _, cfv := range config.WorkerConfig {
|
||||
if cfv.FieldName == "parallelMigrations" {
|
||||
parallelMigrations = int32(cfv.IntValue)
|
||||
} else if cfv.FieldName == "maxBytesPerSecond" {
|
||||
maxBytesPerSecond = cfv.IntValue
|
||||
}
|
||||
}
|
||||
|
||||
return &Executor{
|
||||
jobID: jobID,
|
||||
config: config,
|
||||
parallelMigrations: parallelMigrations,
|
||||
maxBytesPerSecond: maxBytesPerSecond,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs the 5-step volume migration process
|
||||
// Step 1: markVolumeReadonly - Make source volume read-only
|
||||
// Step 2: copyVolume - Copy to target server
|
||||
// Step 3: mountVolume - Mount on target
|
||||
// Step 4: tailUpdates - Tail live updates from source
|
||||
// Step 5: deleteSourceVolume - Delete from source if balanced
|
||||
// Returns progress updates via the progressChan
|
||||
func (e *Executor) Execute(ctx context.Context, metadata map[string]string, progressChan chan<- *plugin_pb.JobProgress) error {
|
||||
e.volumeID = metadata["volume_id"]
|
||||
e.sourceServer = metadata["source_server"]
|
||||
e.targetServer = metadata["target_server"]
|
||||
|
||||
glog.Infof("balance executor job=%s: starting migration (volume=%s, %s -> %s)",
|
||||
e.jobID, e.volumeID, e.sourceServer, e.targetServer)
|
||||
|
||||
state := &MigrationState{}
|
||||
|
||||
steps := []ExecutionStep{
|
||||
{StepNumber: 1, Name: "markVolumeReadonly", Description: "Make source volume read-only"},
|
||||
{StepNumber: 2, Name: "copyVolume", Description: "Copy volume data to target server"},
|
||||
{StepNumber: 3, Name: "mountVolume", Description: "Mount volume on target server"},
|
||||
{StepNumber: 4, Name: "tailUpdates", Description: "Tail live updates from source"},
|
||||
{StepNumber: 5, Name: "deleteSourceVolume", Description: "Delete volume from source server"},
|
||||
}
|
||||
|
||||
totalSteps := len(steps)
|
||||
|
||||
for i, step := range steps {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
glog.Infof("balance executor job=%s: executing step %d: %s", e.jobID, step.StepNumber, step.Name)
|
||||
|
||||
// Send progress update before executing step
|
||||
progress := int32((i * 100) / totalSteps)
|
||||
progressChan <- &plugin_pb.JobProgress{
|
||||
ProgressPercent: progress,
|
||||
CurrentStep: step.Name,
|
||||
StatusMessage: step.Description,
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
|
||||
var err error
|
||||
switch step.StepNumber {
|
||||
case 1:
|
||||
err = e.markVolumeReadonly(ctx, state)
|
||||
case 2:
|
||||
err = e.copyVolume(ctx, state, progressChan)
|
||||
case 3:
|
||||
err = e.mountVolume(ctx, state)
|
||||
case 4:
|
||||
err = e.tailUpdates(ctx, state, progressChan)
|
||||
case 5:
|
||||
err = e.deleteSourceVolume(ctx, state)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf("balance executor job=%s: step %d failed: %v", e.jobID, step.StepNumber, err)
|
||||
// Attempt to rollback on error
|
||||
e.rollbackMigration(ctx, state)
|
||||
return fmt.Errorf("step %d (%s) failed: %w", step.StepNumber, step.Name, err)
|
||||
}
|
||||
|
||||
glog.Infof("balance executor job=%s: step %d completed", e.jobID, step.StepNumber)
|
||||
}
|
||||
|
||||
// Send final progress update
|
||||
progressChan <- &plugin_pb.JobProgress{
|
||||
ProgressPercent: 100,
|
||||
CurrentStep: "complete",
|
||||
StatusMessage: "Volume migration completed successfully",
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
|
||||
glog.Infof("balance executor job=%s: migration completed (volume=%s, transferred=%dB)",
|
||||
e.jobID, e.volumeID, state.bytesTransferred)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 1: markVolumeReadonly makes the source volume read-only to prepare for migration
|
||||
func (e *Executor) markVolumeReadonly(ctx context.Context, state *MigrationState) error {
|
||||
glog.Infof("balance executor job=%s: marking volume %s as read-only on %s",
|
||||
e.jobID, e.volumeID, e.sourceServer)
|
||||
|
||||
// TODO: Connect to source server and mark volume as read-only
|
||||
// 1. Get volume server connection
|
||||
// 2. Send MarkVolumeReadonly RPC
|
||||
// 3. Wait for confirmation
|
||||
// 4. Verify volume is read-only
|
||||
|
||||
state.mu.Lock()
|
||||
state.volumeReadonly = true
|
||||
state.mu.Unlock()
|
||||
|
||||
glog.Infof("balance executor job=%s: volume %s is now read-only", e.jobID, e.volumeID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 2: copyVolume copies volume data from source to target server
|
||||
func (e *Executor) copyVolume(ctx context.Context, state *MigrationState, progressChan chan<- *plugin_pb.JobProgress) error {
|
||||
glog.Infof("balance executor job=%s: starting volume copy from %s to %s (bandwidth=%dB/s)",
|
||||
e.jobID, e.sourceServer, e.targetServer, e.maxBytesPerSecond)
|
||||
|
||||
// TODO: Connect to both source and target servers and perform copy
|
||||
// 1. Get volume metadata from source (size, collection, replicas)
|
||||
// 2. Create volume on target server
|
||||
// 3. Stream volume data from source to target
|
||||
// 4. Respect bandwidth limit if maxBytesPerSecond > 0
|
||||
// 5. Periodically send progress updates via progressChan
|
||||
// 6. Verify checksum after transfer
|
||||
|
||||
// Simulate progress updates
|
||||
startTime := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(time.Second):
|
||||
elapsed := time.Since(startTime)
|
||||
// Simulate byte transfer (would be actual in production)
|
||||
state.mu.Lock()
|
||||
state.bytesTransferred = int64(elapsed.Seconds()) * 1024 * 1024 // 1MB/s simulation
|
||||
state.mu.Unlock()
|
||||
|
||||
if elapsed > 5*time.Second {
|
||||
// Copy complete after 5 seconds in simulation
|
||||
state.mu.Lock()
|
||||
state.volumeCopied = true
|
||||
state.mu.Unlock()
|
||||
glog.Infof("balance executor job=%s: volume copy completed (%dB transferred)",
|
||||
e.jobID, state.bytesTransferred)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: mountVolume mounts the copied volume on the target server
|
||||
func (e *Executor) mountVolume(ctx context.Context, state *MigrationState) error {
|
||||
glog.Infof("balance executor job=%s: mounting volume %s on target server %s",
|
||||
e.jobID, e.volumeID, e.targetServer)
|
||||
|
||||
state.mu.Lock()
|
||||
if !state.volumeCopied {
|
||||
state.mu.Unlock()
|
||||
return fmt.Errorf("volume not copied yet")
|
||||
}
|
||||
state.mu.Unlock()
|
||||
|
||||
// TODO: Connect to target server and mount the volume
|
||||
// 1. Send MountVolume RPC with volume ID and collection
|
||||
// 2. Wait for mount to complete
|
||||
// 3. Verify volume is accessible and readable
|
||||
|
||||
state.mu.Lock()
|
||||
state.volumeMounted = true
|
||||
state.mu.Unlock()
|
||||
|
||||
glog.Infof("balance executor job=%s: volume %s mounted on %s", e.jobID, e.volumeID, e.targetServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 4: tailUpdates tails live updates from source to target to keep them in sync
|
||||
func (e *Executor) tailUpdates(ctx context.Context, state *MigrationState, progressChan chan<- *plugin_pb.JobProgress) error {
|
||||
glog.Infof("balance executor job=%s: starting to tail updates from %s to %s",
|
||||
e.jobID, e.sourceServer, e.targetServer)
|
||||
|
||||
state.mu.Lock()
|
||||
if !state.volumeMounted {
|
||||
state.mu.Unlock()
|
||||
return fmt.Errorf("volume not mounted on target")
|
||||
}
|
||||
state.mu.Unlock()
|
||||
|
||||
// TODO: Stream updates from source to target
|
||||
// 1. Get update stream from source volume
|
||||
// 2. Apply updates to target volume
|
||||
// 3. Handle concurrent writes
|
||||
// 4. Report tail progress
|
||||
|
||||
// Simulate tail for a short duration
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(time.Second):
|
||||
state.mu.Lock()
|
||||
state.tailStarted = true
|
||||
state.mu.Unlock()
|
||||
|
||||
progressChan <- &plugin_pb.JobProgress{
|
||||
ProgressPercent: int32(70 + i*5),
|
||||
CurrentStep: "tailUpdates",
|
||||
StatusMessage: fmt.Sprintf("Applying updates... (%d updates processed)", (i+1)*100),
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("balance executor job=%s: tail updates completed", e.jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 5: deleteSourceVolume deletes the volume from the source server after verification
|
||||
func (e *Executor) deleteSourceVolume(ctx context.Context, state *MigrationState) error {
|
||||
glog.Infof("balance executor job=%s: deleting volume %s from source server %s",
|
||||
e.jobID, e.volumeID, e.sourceServer)
|
||||
|
||||
state.mu.Lock()
|
||||
if !state.tailStarted {
|
||||
state.mu.Unlock()
|
||||
return fmt.Errorf("tail updates not completed")
|
||||
}
|
||||
state.mu.Unlock()
|
||||
|
||||
// TODO: Delete volume from source server
|
||||
// 1. Verify target volume is healthy and in sync
|
||||
// 2. Verify no new writes can occur to source volume
|
||||
// 3. Send DeleteVolume RPC to source server
|
||||
// 4. Wait for deletion to complete
|
||||
// 5. Verify source volume is removed
|
||||
|
||||
state.mu.Lock()
|
||||
state.sourceDeleted = true
|
||||
state.mu.Unlock()
|
||||
|
||||
glog.Infof("balance executor job=%s: volume %s deleted from source %s", e.jobID, e.volumeID, e.sourceServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
// rollbackMigration attempts to rollback the migration if an error occurs
|
||||
func (e *Executor) rollbackMigration(ctx context.Context, state *MigrationState) {
|
||||
glog.Warningf("balance executor job=%s: attempting rollback (readonly=%v, copied=%v, mounted=%v, deleted=%v)",
|
||||
e.jobID, state.volumeReadonly, state.volumeCopied, state.volumeMounted, state.sourceDeleted)
|
||||
|
||||
// Only rollback if we've started but not completed
|
||||
state.mu.Lock()
|
||||
defer state.mu.Unlock()
|
||||
|
||||
// If volume was deleted, we can't fully rollback
|
||||
if state.sourceDeleted {
|
||||
glog.Errorf("balance executor job=%s: cannot rollback - volume already deleted from source", e.jobID)
|
||||
return
|
||||
}
|
||||
|
||||
// If target was mounted, unmount it
|
||||
if state.volumeMounted {
|
||||
glog.Infof("balance executor job=%s: unmounting target volume", e.jobID)
|
||||
// TODO: Send UnmountVolume RPC to target server
|
||||
}
|
||||
|
||||
// If source was made read-only, mark it as writable again
|
||||
if state.volumeReadonly {
|
||||
glog.Infof("balance executor job=%s: restoring source volume to writable", e.jobID)
|
||||
// TODO: Send MarkVolumeWritable RPC to source server
|
||||
}
|
||||
|
||||
glog.Infof("balance executor job=%s: rollback completed", e.jobID)
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// GetConfigurationSchema returns the configuration schema for the balance job type
|
||||
func GetConfigurationSchema() *plugin_pb.JobTypeConfigSchema {
|
||||
return &plugin_pb.JobTypeConfigSchema{
|
||||
JobType: "balance",
|
||||
Version: "v1",
|
||||
Description: "Automatically balance volume distribution across servers to optimize storage and performance",
|
||||
AdminFields: []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "imbalanceThreshold",
|
||||
Label: "Imbalance Threshold (%)",
|
||||
Description: "Trigger rebalancing when imbalance exceeds this percentage (1-100)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "20",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "1",
|
||||
ErrorMessage: "Imbalance threshold must be at least 1",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "100",
|
||||
ErrorMessage: "Imbalance threshold must be at most 100",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 1,
|
||||
MaxValue: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "minServers",
|
||||
Label: "Minimum Servers",
|
||||
Description: "Only rebalance when cluster has at least this many servers (2-10)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: false,
|
||||
DefaultValue: "2",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "2",
|
||||
ErrorMessage: "Minimum servers must be at least 2",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "10",
|
||||
ErrorMessage: "Minimum servers must be at most 10",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 2,
|
||||
MaxValue: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "preferRackDiversity",
|
||||
Label: "Prefer Rack Diversity",
|
||||
Description: "Consider rack locations when balancing to improve fault tolerance",
|
||||
FieldType: plugin_pb.ConfigField_BOOL,
|
||||
Required: false,
|
||||
DefaultValue: "false",
|
||||
},
|
||||
},
|
||||
WorkerFields: []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "parallelMigrations",
|
||||
Label: "Parallel Migrations",
|
||||
Description: "Number of concurrent volume migrations to perform (1-10)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: false,
|
||||
DefaultValue: "2",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "1",
|
||||
ErrorMessage: "Parallel migrations must be at least 1",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "10",
|
||||
ErrorMessage: "Parallel migrations must be at most 10",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 1,
|
||||
MaxValue: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "maxBytesPerSecond",
|
||||
Label: "Max Bytes Per Second",
|
||||
Description: "Limit migration bandwidth (0 = unlimited, in bytes/sec)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: false,
|
||||
DefaultValue: "0",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "0",
|
||||
ErrorMessage: "Max bytes per second must be non-negative",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
FieldGroups: []*plugin_pb.ConfigGroup{
|
||||
{
|
||||
Name: "Balance Settings",
|
||||
Label: "Balance Settings",
|
||||
Description: "Configuration for volume rebalancing",
|
||||
FieldNames: []string{
|
||||
"imbalanceThreshold",
|
||||
"minServers",
|
||||
"preferRackDiversity",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "advanced",
|
||||
Label: "Advanced",
|
||||
Description: "Advanced migration parameters",
|
||||
FieldNames: []string{
|
||||
"parallelMigrations",
|
||||
"maxBytesPerSecond",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
package balance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Worker is the main balance plugin worker
|
||||
type Worker struct {
|
||||
id string
|
||||
name string
|
||||
version string
|
||||
masterAddr string
|
||||
httpPort int
|
||||
|
||||
// gRPC connection
|
||||
conn *grpc.ClientConn
|
||||
client plugin_pb.PluginServiceClient
|
||||
|
||||
// Detector and executor
|
||||
detector *Detector
|
||||
|
||||
// Context and coordination
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Running jobs
|
||||
jobsMu sync.RWMutex
|
||||
runningJobs map[string]context.CancelFunc
|
||||
}
|
||||
|
||||
// NewWorker creates a new balance worker
|
||||
func NewWorker(id, masterAddr string, httpPort int) *Worker {
|
||||
return &Worker{
|
||||
id: id,
|
||||
name: "balance_worker",
|
||||
version: "v1",
|
||||
masterAddr: masterAddr,
|
||||
httpPort: httpPort,
|
||||
runningJobs: make(map[string]context.CancelFunc),
|
||||
detector: NewDetector(masterAddr),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the worker and connects to the admin server
|
||||
func (w *Worker) Start(ctx context.Context) error {
|
||||
glog.Infof("balance worker: starting (id=%s, master=%s, httpPort=%d)", w.id, w.masterAddr, w.httpPort)
|
||||
|
||||
w.ctx, w.cancel = context.WithCancel(ctx)
|
||||
|
||||
// Connect to admin server via gRPC
|
||||
// Admin server runs on httpPort + 10000
|
||||
adminPort := w.httpPort + 10000
|
||||
adminAddr := fmt.Sprintf("localhost:%d", adminPort)
|
||||
|
||||
glog.Infof("balance worker: connecting to admin at %s", adminAddr)
|
||||
|
||||
conn, err := grpc.Dial(adminAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to admin server: %w", err)
|
||||
}
|
||||
|
||||
w.conn = conn
|
||||
w.client = plugin_pb.NewPluginServiceClient(conn)
|
||||
|
||||
// Start connection handler in goroutine
|
||||
w.wg.Add(1)
|
||||
go w.handleConnection()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the worker and closes all connections
|
||||
func (w *Worker) Stop() error {
|
||||
glog.Infof("balance worker: stopping (id=%s)", w.id)
|
||||
|
||||
// Cancel context to signal all goroutines
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
|
||||
// Wait for goroutines with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
w.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
glog.Warningf("balance worker: graceful shutdown timeout")
|
||||
}
|
||||
|
||||
// Close gRPC connection
|
||||
if w.conn != nil {
|
||||
if err := w.conn.Close(); err != nil {
|
||||
glog.Errorf("balance worker: error closing gRPC connection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("balance worker: stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleConnection handles the bidirectional gRPC connection with the admin server
|
||||
func (w *Worker) handleConnection() {
|
||||
defer w.wg.Done()
|
||||
|
||||
glog.Infof("balance worker: establishing bidirectional stream")
|
||||
|
||||
// Create bidirectional stream
|
||||
stream, err := w.client.Connect(w.ctx)
|
||||
if err != nil {
|
||||
glog.Errorf("balance worker: failed to create stream: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send registration message
|
||||
if err := w.sendRegistration(stream); err != nil {
|
||||
glog.Errorf("balance worker: failed to send registration: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start heartbeat goroutine
|
||||
w.wg.Add(1)
|
||||
go w.sendHeartbeats(stream)
|
||||
|
||||
// Start job executor goroutine
|
||||
w.wg.Add(1)
|
||||
go w.executeJobs(stream)
|
||||
|
||||
// Listen for messages from admin
|
||||
w.listenForMessages(stream)
|
||||
|
||||
glog.Infof("balance worker: connection closed")
|
||||
}
|
||||
|
||||
// sendRegistration sends the initial registration message
|
||||
func (w *Worker) sendRegistration(stream plugin_pb.PluginService_ConnectClient) error {
|
||||
capabilities := []*plugin_pb.JobTypeCapability{
|
||||
{
|
||||
JobType: "balance",
|
||||
CanDetect: true,
|
||||
CanExecute: true,
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
|
||||
register := &plugin_pb.PluginRegister{
|
||||
PluginId: w.id,
|
||||
Name: w.name,
|
||||
Version: w.version,
|
||||
ProtocolVersion: "v1",
|
||||
Capabilities: capabilities,
|
||||
}
|
||||
|
||||
msg := &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Register{
|
||||
Register: register,
|
||||
},
|
||||
}
|
||||
|
||||
return stream.Send(msg)
|
||||
}
|
||||
|
||||
// sendHeartbeats sends periodic heartbeat messages
|
||||
func (w *Worker) sendHeartbeats(stream plugin_pb.PluginService_ConnectClient) {
|
||||
defer w.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.jobsMu.RLock()
|
||||
pendingJobs := int32(len(w.runningJobs))
|
||||
w.jobsMu.RUnlock()
|
||||
|
||||
heartbeat := &plugin_pb.PluginHeartbeat{
|
||||
PluginId: w.id,
|
||||
Timestamp: timestamppb.Now(),
|
||||
UptimeSeconds: int64(time.Since(time.Now()).Seconds()),
|
||||
PendingJobs: pendingJobs,
|
||||
CpuUsagePercent: 0, // TODO: Get actual CPU usage
|
||||
MemoryUsageMb: 0, // TODO: Get actual memory usage
|
||||
}
|
||||
|
||||
msg := &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Heartbeat{
|
||||
Heartbeat: heartbeat,
|
||||
},
|
||||
}
|
||||
|
||||
if err := stream.Send(msg); err != nil {
|
||||
glog.Errorf("balance worker: failed to send heartbeat: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeJobs periodically detects and executes jobs
|
||||
func (w *Worker) executeJobs(stream plugin_pb.PluginService_ConnectClient) {
|
||||
defer w.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// TODO: Implement job detection and execution
|
||||
// Query admin for jobs or use detector to find jobs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listenForMessages listens for messages from the admin server
|
||||
func (w *Worker) listenForMessages(stream plugin_pb.PluginService_ConnectClient) {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
glog.Infof("balance worker: admin closed connection")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
glog.Errorf("balance worker: failed to receive message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Content == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch content := msg.Content.(type) {
|
||||
case *plugin_pb.AdminMessage_JobRequest:
|
||||
w.handleJobRequest(content.JobRequest)
|
||||
case *plugin_pb.AdminMessage_ConfigUpdate:
|
||||
glog.Infof("balance worker: received config update: %s", content.ConfigUpdate.JobType)
|
||||
case *plugin_pb.AdminMessage_AdminCommand:
|
||||
w.handleAdminCommand(content.AdminCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleJobRequest processes a job request from the admin
|
||||
func (w *Worker) handleJobRequest(jobReq *plugin_pb.JobRequest) {
|
||||
glog.Infof("balance worker: received job request (id=%s, type=%s)", jobReq.JobId, jobReq.JobType)
|
||||
|
||||
// Create job context
|
||||
jobCtx, cancel := context.WithCancel(w.ctx)
|
||||
|
||||
w.jobsMu.Lock()
|
||||
w.runningJobs[jobReq.JobId] = cancel
|
||||
w.jobsMu.Unlock()
|
||||
|
||||
// Execute job in goroutine
|
||||
w.wg.Add(1)
|
||||
go func() {
|
||||
defer w.wg.Done()
|
||||
defer func() {
|
||||
w.jobsMu.Lock()
|
||||
delete(w.runningJobs, jobReq.JobId)
|
||||
w.jobsMu.Unlock()
|
||||
}()
|
||||
|
||||
w.executeJobRequest(jobCtx, jobReq)
|
||||
}()
|
||||
}
|
||||
|
||||
// executeJobRequest executes a single job request
|
||||
func (w *Worker) executeJobRequest(ctx context.Context, jobReq *plugin_pb.JobRequest) {
|
||||
// Build job config from request
|
||||
config := &plugin_pb.JobTypeConfig{
|
||||
JobType: jobReq.JobType,
|
||||
AdminConfig: nil,
|
||||
WorkerConfig: jobReq.Config,
|
||||
}
|
||||
|
||||
// Create executor
|
||||
executor := NewExecutor(jobReq.JobId, config)
|
||||
|
||||
// Progress channel
|
||||
progressChan := make(chan *plugin_pb.JobProgress, 10)
|
||||
|
||||
// Execute job
|
||||
go func() {
|
||||
if err := executor.Execute(ctx, jobReq.Metadata, progressChan); err != nil {
|
||||
glog.Errorf("balance worker: job execution failed (id=%s): %v", jobReq.JobId, err)
|
||||
}
|
||||
close(progressChan)
|
||||
}()
|
||||
|
||||
// TODO: Send progress updates back to admin via ExecuteJob RPC
|
||||
for progress := range progressChan {
|
||||
_ = progress
|
||||
glog.Infof("balance worker: job %s progress: %d%% (%s)", jobReq.JobId, progress.ProgressPercent, progress.CurrentStep)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdminCommand processes admin commands
|
||||
func (w *Worker) handleAdminCommand(cmd *plugin_pb.AdminCommand) {
|
||||
glog.Infof("balance worker: received admin command: %v", cmd.CommandType)
|
||||
|
||||
switch cmd.CommandType {
|
||||
case plugin_pb.AdminCommand_RELOAD_CONFIG:
|
||||
glog.Infof("balance worker: reloading configuration")
|
||||
case plugin_pb.AdminCommand_ENABLE_JOB_TYPE:
|
||||
glog.Infof("balance worker: enabling balance job type")
|
||||
case plugin_pb.AdminCommand_DISABLE_JOB_TYPE:
|
||||
glog.Infof("balance worker: disabling balance job type")
|
||||
case plugin_pb.AdminCommand_SHUTDOWN:
|
||||
glog.Infof("balance worker: received shutdown command")
|
||||
w.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// GetCapabilities returns the worker's capabilities
|
||||
func (w *Worker) GetCapabilities() []*plugin_pb.JobTypeCapability {
|
||||
return []*plugin_pb.JobTypeCapability{
|
||||
{
|
||||
JobType: "balance",
|
||||
CanDetect: true,
|
||||
CanExecute: true,
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigurationSchema returns the configuration schema
|
||||
func (w *Worker) GetConfigurationSchema() *plugin_pb.JobTypeConfigSchema {
|
||||
return GetConfigurationSchema()
|
||||
}
|
||||
|
||||
// Run is a convenience method that starts the worker and waits for context cancellation
|
||||
func (w *Worker) Run(ctx context.Context) error {
|
||||
if err := w.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartWorkerServer starts the balance worker with the given configuration
|
||||
func StartWorkerServer(masterAddr string, httpPort int) error {
|
||||
pluginID := fmt.Sprintf("balance_worker_%d", time.Now().Unix())
|
||||
worker := NewWorker(pluginID, masterAddr, httpPort)
|
||||
|
||||
ctx := context.Background()
|
||||
return worker.Run(ctx)
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
// Detector scans for volumes that are candidates for erasure coding
|
||||
type Detector struct {
|
||||
masterAddr string
|
||||
}
|
||||
|
||||
// NewDetector creates a new erasure coding detector
|
||||
func NewDetector(masterAddr string) *Detector {
|
||||
return &Detector{
|
||||
masterAddr: masterAddr,
|
||||
}
|
||||
}
|
||||
|
||||
// DetectJobs scans the master for volumes meeting EC criteria
|
||||
// Returns a list of DetectedJob items sorted by priority (volume size)
|
||||
func (d *Detector) DetectJobs(ctx context.Context, config *plugin_pb.JobTypeConfig) ([]*plugin_pb.DetectedJob, error) {
|
||||
var detectedJobs []*plugin_pb.DetectedJob
|
||||
|
||||
// Get destination nodes from config
|
||||
var destinationNodes []string
|
||||
var ecM, ecN, stripeSize int64
|
||||
|
||||
for _, cfv := range config.AdminConfig {
|
||||
if cfv.FieldName == "destination_data_nodes" {
|
||||
if cfv.StringValue != "" {
|
||||
// Parse comma-separated nodes
|
||||
destinationNodes = append(destinationNodes, cfv.StringValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, cfv := range config.WorkerConfig {
|
||||
if cfv.FieldName == "ec_m" {
|
||||
ecM = cfv.IntValue
|
||||
} else if cfv.FieldName == "ec_n" {
|
||||
ecN = cfv.IntValue
|
||||
} else if cfv.FieldName == "stripe_size" {
|
||||
stripeSize = cfv.IntValue
|
||||
}
|
||||
}
|
||||
|
||||
if len(destinationNodes) == 0 {
|
||||
glog.Warningf("erasure_coding detector: no destination nodes configured")
|
||||
return detectedJobs, nil
|
||||
}
|
||||
|
||||
if ecM == 0 {
|
||||
ecM = 10
|
||||
}
|
||||
if ecN == 0 {
|
||||
ecN = 4
|
||||
}
|
||||
if stripeSize == 0 {
|
||||
stripeSize = 65536
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding detector: scanning for volumes (ecM=%d, ecN=%d)", ecM, ecN)
|
||||
|
||||
// Scan master for volumes
|
||||
// This would typically connect to the master server to get volume information
|
||||
// For now, we create a framework that can be extended with actual master connectivity
|
||||
volumes := d.scanVolumes(ctx)
|
||||
|
||||
for _, vol := range volumes {
|
||||
// Check if volume is a candidate for EC
|
||||
// Criteria: less than 90% full, not already encoded
|
||||
if vol.fullnessPercent < 90 && !vol.isEncoded {
|
||||
jobKey := fmt.Sprintf("ec_%s_%s", vol.id, time.Now().Format("20060102"))
|
||||
|
||||
// Priority is based on volume size (larger volumes get higher priority)
|
||||
priority := int64(vol.sizeGB)
|
||||
|
||||
job := &plugin_pb.DetectedJob{
|
||||
JobKey: jobKey,
|
||||
JobType: "erasure_coding",
|
||||
Description: fmt.Sprintf("Encode volume %s (%.1f%% full, %dGB)", vol.id, vol.fullnessPercent, vol.sizeGB),
|
||||
Priority: priority,
|
||||
EstimatedDuration: durationpb.New(time.Duration(vol.sizeGB) * time.Hour), // Rough estimate
|
||||
Metadata: map[string]string{
|
||||
"volume_id": vol.id,
|
||||
"collection": vol.collection,
|
||||
"fullness_pct": fmt.Sprintf("%.1f", vol.fullnessPercent),
|
||||
"size_gb": fmt.Sprintf("%d", vol.sizeGB),
|
||||
"data_nodes": fmt.Sprintf("%d", len(destinationNodes)),
|
||||
},
|
||||
SuggestedConfig: []*plugin_pb.ConfigFieldValue{
|
||||
{
|
||||
FieldName: "ec_m",
|
||||
IntValue: ecM,
|
||||
},
|
||||
{
|
||||
FieldName: "ec_n",
|
||||
IntValue: ecN,
|
||||
},
|
||||
{
|
||||
FieldName: "stripe_size",
|
||||
IntValue: stripeSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
detectedJobs = append(detectedJobs, job)
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding detector: found %d candidate volumes", len(detectedJobs))
|
||||
return detectedJobs, nil
|
||||
}
|
||||
|
||||
// Volume represents a volume on the cluster
|
||||
type volume struct {
|
||||
id string
|
||||
collection string
|
||||
sizeGB int64
|
||||
usedGB int64
|
||||
fullnessPercent float64
|
||||
isEncoded bool
|
||||
replicaPlacement int32
|
||||
dataNodes []string
|
||||
}
|
||||
|
||||
// scanVolumes performs a scan of available volumes
|
||||
// This is a placeholder implementation that would connect to master in production
|
||||
func (d *Detector) scanVolumes(ctx context.Context) []volume {
|
||||
// TODO: Connect to master server at d.masterAddr and get volume list
|
||||
// For now, return empty list as a framework
|
||||
var volumes []volume
|
||||
return volumes
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdtesting "testing"
|
||||
"time"
|
||||
|
||||
plugintesting "github.com/seaweedfs/seaweedfs/weed/admin/plugin/testing"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// TestECSchemaGeneration tests that the EC plugin generates the correct configuration schema
|
||||
func TestECSchemaGeneration(t *stdtesting.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
harness := plugintesting.NewTestHarness(ctx)
|
||||
if err := harness.Setup(); err != nil {
|
||||
t.Fatalf("Setup failed: %v", err)
|
||||
}
|
||||
defer harness.Teardown()
|
||||
|
||||
// Verify schema fields for EC job type
|
||||
expectedFields := map[string]bool{
|
||||
"data_shards": true,
|
||||
"parity_shards": true,
|
||||
"block_size": true,
|
||||
"algorithm": true,
|
||||
}
|
||||
|
||||
configFields := []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "data_shards",
|
||||
Label: "Data Shards",
|
||||
Description: "Number of data shards",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "4",
|
||||
},
|
||||
{
|
||||
Name: "parity_shards",
|
||||
Label: "Parity Shards",
|
||||
Description: "Number of parity shards",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "2",
|
||||
},
|
||||
{
|
||||
Name: "block_size",
|
||||
Label: "Block Size",
|
||||
Description: "Block size in bytes",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: false,
|
||||
DefaultValue: "65536",
|
||||
},
|
||||
{
|
||||
Name: "algorithm",
|
||||
Label: "Algorithm",
|
||||
Description: "Erasure coding algorithm",
|
||||
FieldType: plugin_pb.ConfigField_SELECT,
|
||||
Required: true,
|
||||
DefaultValue: "reed_solomon",
|
||||
},
|
||||
}
|
||||
|
||||
// Verify all expected fields are present
|
||||
for _, field := range configFields {
|
||||
if _, ok := expectedFields[field.Name]; ok {
|
||||
if field.FieldType == plugin_pb.ConfigField_INT ||
|
||||
field.FieldType == plugin_pb.ConfigField_SELECT {
|
||||
delete(expectedFields, field.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(expectedFields) > 0 {
|
||||
t.Errorf("Missing expected schema fields: %v", expectedFields)
|
||||
}
|
||||
}
|
||||
|
||||
// TestECDetection tests that the EC plugin can detect jobs
|
||||
func TestECDetection(t *stdtesting.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
harness := plugintesting.NewTestHarness(ctx)
|
||||
if err := harness.Setup(); err != nil {
|
||||
t.Fatalf("Setup failed: %v", err)
|
||||
}
|
||||
defer harness.Teardown()
|
||||
|
||||
// Create mock plugin
|
||||
plugin := plugintesting.NewMockPlugin("ec-plugin-1", "Erasure Coding", "1.0.0")
|
||||
plugin.AddCapability("erasure_coding", true, true)
|
||||
|
||||
// Simulate detection
|
||||
detectedJobs, err := plugin.SimulateDetection("erasure_coding", 3)
|
||||
if err != nil {
|
||||
t.Fatalf("Detection failed: %v", err)
|
||||
}
|
||||
|
||||
if len(detectedJobs) != 3 {
|
||||
t.Errorf("Expected 3 detected jobs, got %d", len(detectedJobs))
|
||||
}
|
||||
|
||||
for i, job := range detectedJobs {
|
||||
if job.JobType != "erasure_coding" {
|
||||
t.Errorf("Job %d has wrong type: expected erasure_coding, got %s", i, job.JobType)
|
||||
}
|
||||
if job.JobKey == "" {
|
||||
t.Errorf("Job %d missing job key", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestECExecution tests that the EC plugin can execute jobs
|
||||
func TestECExecution(t *stdtesting.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
harness := plugintesting.NewTestHarness(ctx)
|
||||
if err := harness.Setup(); err != nil {
|
||||
t.Fatalf("Setup failed: %v", err)
|
||||
}
|
||||
defer harness.Teardown()
|
||||
|
||||
// Create mock plugin
|
||||
plugin := plugintesting.NewMockPlugin("ec-plugin-2", "Erasure Coding", "1.0.0")
|
||||
plugin.AddCapability("erasure_coding", true, true)
|
||||
|
||||
// Create a test job
|
||||
config := []*plugin_pb.ConfigFieldValue{
|
||||
{
|
||||
FieldName: "data_shards",
|
||||
IntValue: 4,
|
||||
},
|
||||
{
|
||||
FieldName: "parity_shards",
|
||||
IntValue: 2,
|
||||
},
|
||||
}
|
||||
|
||||
jobID := "ec-job-test-001"
|
||||
execution, err := plugin.SimulateExecution(jobID, "erasure_coding", config)
|
||||
if err != nil {
|
||||
t.Fatalf("Execution failed: %v", err)
|
||||
}
|
||||
|
||||
if execution.Status != "completed" {
|
||||
t.Errorf("Expected status 'completed', got %s", execution.Status)
|
||||
}
|
||||
|
||||
if execution.ProgressPercent != 100 {
|
||||
t.Errorf("Expected progress 100, got %d", execution.ProgressPercent)
|
||||
}
|
||||
|
||||
messages := plugin.GetExecutionMessages(jobID)
|
||||
if len(messages) == 0 {
|
||||
t.Fatal("Expected execution messages")
|
||||
}
|
||||
|
||||
// Check for completion message
|
||||
hasCompletion := false
|
||||
for _, msg := range messages {
|
||||
if msg.GetJobCompleted() != nil {
|
||||
hasCompletion = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCompletion {
|
||||
t.Error("Missing job completion message")
|
||||
}
|
||||
}
|
||||
|
||||
// TestECErrorHandling tests error scenarios in EC plugin
|
||||
func TestECErrorHandling(t *stdtesting.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
harness := plugintesting.NewTestHarness(ctx)
|
||||
if err := harness.Setup(); err != nil {
|
||||
t.Fatalf("Setup failed: %v", err)
|
||||
}
|
||||
defer harness.Teardown()
|
||||
|
||||
// Test 1: Detection disabled
|
||||
plugin := plugintesting.NewMockPlugin("ec-plugin-3", "Erasure Coding", "1.0.0")
|
||||
plugin.AddCapability("erasure_coding", true, true)
|
||||
plugin.DetectionEnabled = false
|
||||
|
||||
_, err := plugin.SimulateDetection("erasure_coding", 1)
|
||||
if err == nil {
|
||||
t.Error("Expected error when detection is disabled")
|
||||
}
|
||||
|
||||
// Test 2: Execution failure scenario
|
||||
plugin.Reset()
|
||||
plugin.DetectionEnabled = true
|
||||
plugin.ExecutionEnabled = true
|
||||
|
||||
jobID := "ec-job-fail-001"
|
||||
execution, err := plugin.SimulateExecutionFailure(jobID, "INVALID_CONFIG", "Data shards must be > 0", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Execution failure simulation failed: %v", err)
|
||||
}
|
||||
|
||||
if execution.Status != "failed" {
|
||||
t.Errorf("Expected status 'failed', got %s", execution.Status)
|
||||
}
|
||||
|
||||
if execution.ErrorInfo.ErrorCode != "INVALID_CONFIG" {
|
||||
t.Errorf("Expected error code 'INVALID_CONFIG', got %s", execution.ErrorInfo.ErrorCode)
|
||||
}
|
||||
|
||||
if !execution.ErrorInfo.Retryable {
|
||||
t.Error("Expected error to be retryable")
|
||||
}
|
||||
|
||||
// Test 3: Simulated detection error
|
||||
plugin.SetFailureMode("detection_error")
|
||||
_, err = plugin.SimulateDetection("erasure_coding", 1)
|
||||
if err == nil {
|
||||
t.Error("Expected error in detection with failure mode set")
|
||||
}
|
||||
|
||||
errors := plugin.GetErrors()
|
||||
if len(errors) == 0 {
|
||||
t.Error("Expected recorded errors")
|
||||
}
|
||||
}
|
||||
|
||||
// TestECIntegration tests the full EC plugin workflow
|
||||
func TestECIntegration(t *stdtesting.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
harness := plugintesting.NewTestHarness(ctx)
|
||||
if err := harness.Setup(); err != nil {
|
||||
t.Fatalf("Setup failed: %v", err)
|
||||
}
|
||||
defer harness.Teardown()
|
||||
|
||||
// Create mock admin server
|
||||
adminServer := harness.GetAdminServer()
|
||||
_ = adminServer // Keep reference to prevent GC
|
||||
|
||||
// Create mock plugin
|
||||
plugin := plugintesting.NewMockPlugin("ec-plugin-integration", "Erasure Coding", "1.0.0")
|
||||
plugin.AddCapability("erasure_coding", true, true)
|
||||
|
||||
// Step 1: Plugin registration
|
||||
regMsg := plugin.GetRegistrationMessage()
|
||||
_ = regMsg // Keep reference
|
||||
|
||||
// Step 2: Detect jobs
|
||||
detectedJobs, err := plugin.SimulateDetection("erasure_coding", 2)
|
||||
if err != nil {
|
||||
t.Fatalf("Detection failed: %v", err)
|
||||
}
|
||||
|
||||
if len(detectedJobs) != 2 {
|
||||
t.Errorf("Expected 2 detected jobs, got %d", len(detectedJobs))
|
||||
}
|
||||
|
||||
// Step 3: Create jobs from detection
|
||||
for i, detectedJob := range detectedJobs {
|
||||
job := harness.CreateJob(
|
||||
"erasure_coding",
|
||||
detectedJob.Description,
|
||||
detectedJob.Priority,
|
||||
detectedJob.SuggestedConfig,
|
||||
)
|
||||
|
||||
// Step 4: Execute job with plugin simulation
|
||||
_, err := plugin.SimulateExecution(job.JobId, "erasure_coding", job.Config)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to execute job %d: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Track in harness
|
||||
_ = harness.ExecuteJob(job)
|
||||
|
||||
// Complete the job
|
||||
if err := harness.CompleteJob(job.JobId, "Erasure coding completed", map[string]string{
|
||||
"blocks_encoded": "1000",
|
||||
}); err != nil {
|
||||
t.Errorf("Failed to complete job %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Verify job status
|
||||
if err := harness.AssertJobStatus(job.JobId, "completed"); err != nil {
|
||||
t.Errorf("Job %d status verification failed: %v", i, err)
|
||||
}
|
||||
|
||||
// Verify progress
|
||||
if err := harness.VerifyProgress(job.JobId, 100); err != nil {
|
||||
t.Errorf("Job %d progress verification failed: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify call counts
|
||||
detectionCalls := plugin.GetCallCount("detection")
|
||||
if detectionCalls == 0 {
|
||||
t.Error("Expected at least one detection call")
|
||||
}
|
||||
|
||||
executionCalls := plugin.GetCallCount("execution")
|
||||
if executionCalls != 2 {
|
||||
t.Errorf("Expected 2 execution calls, got %d", executionCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestECConfigurationValidation tests configuration field validation
|
||||
func TestECConfigurationValidation(t *stdtesting.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *plugin_pb.ConfigFieldValue
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid data shards",
|
||||
config: &plugin_pb.ConfigFieldValue{
|
||||
FieldName: "data_shards",
|
||||
IntValue: 4,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid data shards - zero",
|
||||
config: &plugin_pb.ConfigFieldValue{
|
||||
FieldName: "data_shards",
|
||||
IntValue: 0,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid parity shards",
|
||||
config: &plugin_pb.ConfigFieldValue{
|
||||
FieldName: "parity_shards",
|
||||
IntValue: 2,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "negative parity shards",
|
||||
config: &plugin_pb.ConfigFieldValue{
|
||||
FieldName: "parity_shards",
|
||||
IntValue: -1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *stdtesting.T) {
|
||||
// Validate configuration
|
||||
var hasError bool
|
||||
|
||||
// Simple validation: data_shards > 0, parity_shards >= 0
|
||||
if tt.config.FieldName == "data_shards" && tt.config.IntValue <= 0 {
|
||||
hasError = true
|
||||
}
|
||||
if tt.config.FieldName == "parity_shards" && tt.config.IntValue < 0 {
|
||||
hasError = true
|
||||
}
|
||||
|
||||
if hasError != tt.wantErr {
|
||||
t.Errorf("validation error: expected %v, got %v", tt.wantErr, hasError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkECDetection benchmarks the detection performance
|
||||
func BenchmarkECDetection(b *stdtesting.B) {
|
||||
plugin := plugintesting.NewMockPlugin("ec-plugin-bench", "Erasure Coding", "1.0.0")
|
||||
plugin.AddCapability("erasure_coding", true, true)
|
||||
plugin.SetDetectionDelay(0) // No delay for benchmark
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = plugin.SimulateDetection("erasure_coding", 10)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkECExecution benchmarks the execution performance
|
||||
func BenchmarkECExecution(b *stdtesting.B) {
|
||||
plugin := plugintesting.NewMockPlugin("ec-plugin-bench-exec", "Erasure Coding", "1.0.0")
|
||||
plugin.AddCapability("erasure_coding", true, true)
|
||||
plugin.SetExecutionDelay(0) // No delay for benchmark
|
||||
|
||||
config := []*plugin_pb.ConfigFieldValue{
|
||||
{
|
||||
FieldName: "data_shards",
|
||||
IntValue: 4,
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
jobID := "bench-job-" + string(rune(i))
|
||||
_, _ = plugin.SimulateExecution(jobID, "erasure_coding", config)
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Executor handles the 6-step erasure coding execution process
|
||||
type Executor struct {
|
||||
jobID string
|
||||
volumeID string
|
||||
config *plugin_pb.JobTypeConfig
|
||||
}
|
||||
|
||||
// NewExecutor creates a new erasure coding executor
|
||||
func NewExecutor(jobID string, config *plugin_pb.JobTypeConfig) *Executor {
|
||||
return &Executor{
|
||||
jobID: jobID,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecutionStep represents a single step in the EC process
|
||||
type ExecutionStep struct {
|
||||
StepNumber int
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// Execute runs the 6-step erasure coding process
|
||||
// Returns progress updates via the progressChan
|
||||
func (e *Executor) Execute(ctx context.Context, metadata map[string]string, progressChan chan<- *plugin_pb.JobProgress) error {
|
||||
volumeID := metadata["volume_id"]
|
||||
e.volumeID = volumeID
|
||||
|
||||
steps := []ExecutionStep{
|
||||
{StepNumber: 1, Name: "markVolumeReadonly", Description: "Mark volume as read-only"},
|
||||
{StepNumber: 2, Name: "copyVolumeFilesToWorker", Description: "Copy volume files to worker"},
|
||||
{StepNumber: 3, Name: "generateEcShards", Description: "Generate erasure coding shards"},
|
||||
{StepNumber: 4, Name: "distributeEcShards", Description: "Distribute shards to data nodes"},
|
||||
{StepNumber: 5, Name: "mountEcShards", Description: "Mount shards on target nodes"},
|
||||
{StepNumber: 6, Name: "deleteOriginalVolume", Description: "Delete original volume"},
|
||||
}
|
||||
|
||||
totalSteps := len(steps)
|
||||
for i, step := range steps {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding executor job=%s: executing step %d: %s", e.jobID, step.StepNumber, step.Name)
|
||||
|
||||
// Send progress update before executing step
|
||||
progress := int32((i * 100) / totalSteps)
|
||||
progressChan <- &plugin_pb.JobProgress{
|
||||
ProgressPercent: progress,
|
||||
CurrentStep: step.Name,
|
||||
StatusMessage: step.Description,
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
|
||||
var err error
|
||||
switch step.StepNumber {
|
||||
case 1:
|
||||
err = e.markVolumeReadonly(ctx)
|
||||
case 2:
|
||||
err = e.copyVolumeFilesToWorker(ctx)
|
||||
case 3:
|
||||
err = e.generateEcShards(ctx)
|
||||
case 4:
|
||||
err = e.distributeEcShards(ctx)
|
||||
case 5:
|
||||
err = e.mountEcShards(ctx)
|
||||
case 6:
|
||||
err = e.deleteOriginalVolume(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf("erasure_coding executor job=%s: step %d failed: %v", e.jobID, step.StepNumber, err)
|
||||
return fmt.Errorf("step %d (%s) failed: %w", step.StepNumber, step.Name, err)
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding executor job=%s: step %d completed", e.jobID, step.StepNumber)
|
||||
}
|
||||
|
||||
// Send final progress update
|
||||
progressChan <- &plugin_pb.JobProgress{
|
||||
ProgressPercent: 100,
|
||||
CurrentStep: "complete",
|
||||
StatusMessage: "Erasure coding completed successfully",
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding executor job=%s: all steps completed", e.jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 1: markVolumeReadonly marks the volume as read-only
|
||||
func (e *Executor) markVolumeReadonly(ctx context.Context) error {
|
||||
glog.Infof("erasure_coding executor job=%s: marking volume %s as read-only", e.jobID, e.volumeID)
|
||||
|
||||
// TODO: Connect to master and mark volume as read-only
|
||||
// This prevents new writes during encoding
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 2: copyVolumeFilesToWorker copies volume data to the worker
|
||||
func (e *Executor) copyVolumeFilesToWorker(ctx context.Context) error {
|
||||
glog.Infof("erasure_coding executor job=%s: copying volume files for %s", e.jobID, e.volumeID)
|
||||
|
||||
// TODO: Transfer volume files from storage nodes to worker
|
||||
// This includes the .idx and .dat files
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 3: generateEcShards generates erasure coding shards
|
||||
func (e *Executor) generateEcShards(ctx context.Context) error {
|
||||
glog.Infof("erasure_coding executor job=%s: generating EC shards for volume %s", e.jobID, e.volumeID)
|
||||
|
||||
// Extract config values
|
||||
var ecM, ecN, stripeSize int64
|
||||
for _, cfv := range e.config.WorkerConfig {
|
||||
if cfv.FieldName == "ec_m" {
|
||||
ecM = cfv.IntValue
|
||||
} else if cfv.FieldName == "ec_n" {
|
||||
ecN = cfv.IntValue
|
||||
} else if cfv.FieldName == "stripe_size" {
|
||||
stripeSize = cfv.IntValue
|
||||
}
|
||||
}
|
||||
|
||||
if ecM == 0 {
|
||||
ecM = 10
|
||||
}
|
||||
if ecN == 0 {
|
||||
ecN = 4
|
||||
}
|
||||
if stripeSize == 0 {
|
||||
stripeSize = 65536
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding executor job=%s: encoding with M=%d, N=%d, stripe_size=%d", e.jobID, ecM, ecN, stripeSize)
|
||||
|
||||
// TODO: Use libReedSolomon or similar library to generate EC shards
|
||||
// This creates M data shards and N parity shards
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 4: distributeEcShards distributes shards to data nodes
|
||||
func (e *Executor) distributeEcShards(ctx context.Context) error {
|
||||
glog.Infof("erasure_coding executor job=%s: distributing EC shards to destination nodes", e.jobID)
|
||||
|
||||
// Extract destination nodes from config
|
||||
var destinationNodes []string
|
||||
for _, cfv := range e.config.AdminConfig {
|
||||
if cfv.FieldName == "destination_data_nodes" {
|
||||
if cfv.StringValue != "" {
|
||||
destinationNodes = append(destinationNodes, cfv.StringValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding executor job=%s: distributing to %d destination nodes", e.jobID, len(destinationNodes))
|
||||
|
||||
// TODO: Send shards to each destination data node
|
||||
// Balance shards across available nodes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 5: mountEcShards mounts the shards on their target nodes
|
||||
func (e *Executor) mountEcShards(ctx context.Context) error {
|
||||
glog.Infof("erasure_coding executor job=%s: mounting EC shards", e.jobID)
|
||||
|
||||
// TODO: Notify data nodes to mount the EC shards
|
||||
// Register shards with master server
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 6: deleteOriginalVolume deletes the original volume if configured
|
||||
func (e *Executor) deleteOriginalVolume(ctx context.Context) error {
|
||||
// Check if delete_source is enabled
|
||||
var deleteSource bool
|
||||
for _, cfv := range e.config.AdminConfig {
|
||||
if cfv.FieldName == "delete_source" {
|
||||
deleteSource = cfv.BoolValue
|
||||
}
|
||||
}
|
||||
|
||||
if !deleteSource {
|
||||
glog.Infof("erasure_coding executor job=%s: skipping deletion of original volume (delete_source=false)", e.jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding executor job=%s: deleting original volume %s", e.jobID, e.volumeID)
|
||||
|
||||
// TODO: Delete the original volume from the master
|
||||
// This frees up space on the storage nodes
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// GetConfigurationSchema returns the configuration schema for the erasure coding job type
|
||||
func GetConfigurationSchema() *plugin_pb.JobTypeConfigSchema {
|
||||
return &plugin_pb.JobTypeConfigSchema{
|
||||
JobType: "erasure_coding",
|
||||
Version: "v1",
|
||||
Description: "Automatically encode volumes using erasure coding for improved storage efficiency",
|
||||
AdminFields: []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "destination_data_nodes",
|
||||
Label: "Destination Data Nodes",
|
||||
Description: "List of data nodes where EC shards should be distributed",
|
||||
FieldType: plugin_pb.ConfigField_STRING,
|
||||
Required: true,
|
||||
DefaultValue: "",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_LENGTH,
|
||||
Value: "1",
|
||||
ErrorMessage: "At least one destination node must be specified",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
Placeholder: "node1,node2,node3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "threshold",
|
||||
Label: "Failure Threshold",
|
||||
Description: "Number of data shards that can be lost before data is unrecoverable (1-99)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "2",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "1",
|
||||
ErrorMessage: "Threshold must be at least 1",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "99",
|
||||
ErrorMessage: "Threshold must be at most 99",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 1,
|
||||
MaxValue: 99,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete_source",
|
||||
Label: "Delete Source After Encoding",
|
||||
Description: "Whether to delete the original volume after successful EC encoding",
|
||||
FieldType: plugin_pb.ConfigField_BOOL,
|
||||
Required: false,
|
||||
DefaultValue: "true",
|
||||
},
|
||||
},
|
||||
WorkerFields: []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "ec_m",
|
||||
Label: "Data Shards (M)",
|
||||
Description: "Number of data shards for erasure coding",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "10",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "1",
|
||||
ErrorMessage: "Data shards must be at least 1",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "32",
|
||||
ErrorMessage: "Data shards must be at most 32",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 1,
|
||||
MaxValue: 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ec_n",
|
||||
Label: "Parity Shards (N)",
|
||||
Description: "Number of parity shards for erasure coding",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "4",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "1",
|
||||
ErrorMessage: "Parity shards must be at least 1",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "32",
|
||||
ErrorMessage: "Parity shards must be at most 32",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 1,
|
||||
MaxValue: 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stripe_size",
|
||||
Label: "Stripe Size (bytes)",
|
||||
Description: "Size of data chunks used in erasure coding (must be power of 2)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "65536",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "4096",
|
||||
ErrorMessage: "Stripe size must be at least 4096 bytes",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "1048576",
|
||||
ErrorMessage: "Stripe size must be at most 1MB",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 4096,
|
||||
MaxValue: 1048576,
|
||||
},
|
||||
},
|
||||
},
|
||||
FieldGroups: []*plugin_pb.ConfigGroup{
|
||||
{
|
||||
Name: "general",
|
||||
Label: "General",
|
||||
Description: "Basic configuration for erasure coding",
|
||||
FieldNames: []string{
|
||||
"destination_data_nodes",
|
||||
"threshold",
|
||||
"delete_source",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "advanced",
|
||||
Label: "Advanced",
|
||||
Description: "Advanced erasure coding parameters",
|
||||
FieldNames: []string{
|
||||
"ec_m",
|
||||
"ec_n",
|
||||
"stripe_size",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
package erasure_coding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Worker is the main erasure coding plugin worker
|
||||
type Worker struct {
|
||||
id string
|
||||
name string
|
||||
version string
|
||||
masterAddr string
|
||||
httpPort int
|
||||
|
||||
// gRPC connection
|
||||
conn *grpc.ClientConn
|
||||
client plugin_pb.PluginServiceClient
|
||||
|
||||
// Detector and executor
|
||||
detector *Detector
|
||||
|
||||
// Context and coordination
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Running jobs
|
||||
jobsMu sync.RWMutex
|
||||
runningJobs map[string]context.CancelFunc
|
||||
}
|
||||
|
||||
// NewWorker creates a new erasure coding worker
|
||||
func NewWorker(id, masterAddr string, httpPort int) *Worker {
|
||||
return &Worker{
|
||||
id: id,
|
||||
name: "erasure_coding_worker",
|
||||
version: "v1",
|
||||
masterAddr: masterAddr,
|
||||
httpPort: httpPort,
|
||||
runningJobs: make(map[string]context.CancelFunc),
|
||||
detector: NewDetector(masterAddr),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the worker and connects to the admin server
|
||||
func (w *Worker) Start(ctx context.Context) error {
|
||||
glog.Infof("erasure_coding worker: starting (id=%s, master=%s, httpPort=%d)", w.id, w.masterAddr, w.httpPort)
|
||||
|
||||
w.ctx, w.cancel = context.WithCancel(ctx)
|
||||
|
||||
// Connect to admin server via gRPC
|
||||
// Admin server runs on httpPort + 10000
|
||||
adminPort := w.httpPort + 10000
|
||||
adminAddr := fmt.Sprintf("localhost:%d", adminPort)
|
||||
|
||||
glog.Infof("erasure_coding worker: connecting to admin at %s", adminAddr)
|
||||
|
||||
conn, err := grpc.Dial(adminAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to admin server: %w", err)
|
||||
}
|
||||
|
||||
w.conn = conn
|
||||
w.client = plugin_pb.NewPluginServiceClient(conn)
|
||||
|
||||
// Start connection handler in goroutine
|
||||
w.wg.Add(1)
|
||||
go w.handleConnection()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the worker and closes all connections
|
||||
func (w *Worker) Stop() error {
|
||||
glog.Infof("erasure_coding worker: stopping (id=%s)", w.id)
|
||||
|
||||
// Cancel context to signal all goroutines
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
|
||||
// Wait for goroutines with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
w.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
glog.Warningf("erasure_coding worker: graceful shutdown timeout")
|
||||
}
|
||||
|
||||
// Close gRPC connection
|
||||
if w.conn != nil {
|
||||
if err := w.conn.Close(); err != nil {
|
||||
glog.Errorf("erasure_coding worker: error closing gRPC connection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("erasure_coding worker: stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleConnection handles the bidirectional gRPC connection with the admin server
|
||||
func (w *Worker) handleConnection() {
|
||||
defer w.wg.Done()
|
||||
|
||||
glog.Infof("erasure_coding worker: establishing bidirectional stream")
|
||||
|
||||
// Create bidirectional stream
|
||||
stream, err := w.client.Connect(w.ctx)
|
||||
if err != nil {
|
||||
glog.Errorf("erasure_coding worker: failed to create stream: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send registration message
|
||||
if err := w.sendRegistration(stream); err != nil {
|
||||
glog.Errorf("erasure_coding worker: failed to send registration: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start heartbeat goroutine
|
||||
w.wg.Add(1)
|
||||
go w.sendHeartbeats(stream)
|
||||
|
||||
// Start job executor goroutine
|
||||
w.wg.Add(1)
|
||||
go w.executeJobs(stream)
|
||||
|
||||
// Listen for messages from admin
|
||||
w.listenForMessages(stream)
|
||||
|
||||
glog.Infof("erasure_coding worker: connection closed")
|
||||
}
|
||||
|
||||
// sendRegistration sends the initial registration message
|
||||
func (w *Worker) sendRegistration(stream plugin_pb.PluginService_ConnectClient) error {
|
||||
capabilities := []*plugin_pb.JobTypeCapability{
|
||||
{
|
||||
JobType: "erasure_coding",
|
||||
CanDetect: true,
|
||||
CanExecute: true,
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
|
||||
register := &plugin_pb.PluginRegister{
|
||||
PluginId: w.id,
|
||||
Name: w.name,
|
||||
Version: w.version,
|
||||
ProtocolVersion: "v1",
|
||||
Capabilities: capabilities,
|
||||
}
|
||||
|
||||
msg := &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Register{
|
||||
Register: register,
|
||||
},
|
||||
}
|
||||
|
||||
return stream.Send(msg)
|
||||
}
|
||||
|
||||
// sendHeartbeats sends periodic heartbeat messages
|
||||
func (w *Worker) sendHeartbeats(stream plugin_pb.PluginService_ConnectClient) {
|
||||
defer w.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.jobsMu.RLock()
|
||||
pendingJobs := int32(len(w.runningJobs))
|
||||
w.jobsMu.RUnlock()
|
||||
|
||||
heartbeat := &plugin_pb.PluginHeartbeat{
|
||||
PluginId: w.id,
|
||||
Timestamp: timestamppb.Now(),
|
||||
UptimeSeconds: int64(time.Since(time.Now()).Seconds()),
|
||||
PendingJobs: pendingJobs,
|
||||
CpuUsagePercent: 0, // TODO: Get actual CPU usage
|
||||
MemoryUsageMb: 0, // TODO: Get actual memory usage
|
||||
}
|
||||
|
||||
msg := &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Heartbeat{
|
||||
Heartbeat: heartbeat,
|
||||
},
|
||||
}
|
||||
|
||||
if err := stream.Send(msg); err != nil {
|
||||
glog.Errorf("erasure_coding worker: failed to send heartbeat: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeJobs periodically detects and executes jobs
|
||||
func (w *Worker) executeJobs(stream plugin_pb.PluginService_ConnectClient) {
|
||||
defer w.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// TODO: Implement job detection and execution
|
||||
// Query admin for jobs or use detector to find jobs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listenForMessages listens for messages from the admin server
|
||||
func (w *Worker) listenForMessages(stream plugin_pb.PluginService_ConnectClient) {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
glog.Infof("erasure_coding worker: admin closed connection")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
glog.Errorf("erasure_coding worker: failed to receive message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Content == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch content := msg.Content.(type) {
|
||||
case *plugin_pb.AdminMessage_JobRequest:
|
||||
w.handleJobRequest(content.JobRequest)
|
||||
case *plugin_pb.AdminMessage_ConfigUpdate:
|
||||
glog.Infof("erasure_coding worker: received config update: %s", content.ConfigUpdate.JobType)
|
||||
case *plugin_pb.AdminMessage_AdminCommand:
|
||||
w.handleAdminCommand(content.AdminCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleJobRequest processes a job request from the admin
|
||||
func (w *Worker) handleJobRequest(jobReq *plugin_pb.JobRequest) {
|
||||
glog.Infof("erasure_coding worker: received job request (id=%s, type=%s)", jobReq.JobId, jobReq.JobType)
|
||||
|
||||
// Create job context
|
||||
jobCtx, cancel := context.WithCancel(w.ctx)
|
||||
|
||||
w.jobsMu.Lock()
|
||||
w.runningJobs[jobReq.JobId] = cancel
|
||||
w.jobsMu.Unlock()
|
||||
|
||||
// Execute job in goroutine
|
||||
w.wg.Add(1)
|
||||
go func() {
|
||||
defer w.wg.Done()
|
||||
defer func() {
|
||||
w.jobsMu.Lock()
|
||||
delete(w.runningJobs, jobReq.JobId)
|
||||
w.jobsMu.Unlock()
|
||||
}()
|
||||
|
||||
w.executeJobRequest(jobCtx, jobReq)
|
||||
}()
|
||||
}
|
||||
|
||||
// executeJobRequest executes a single job request
|
||||
func (w *Worker) executeJobRequest(ctx context.Context, jobReq *plugin_pb.JobRequest) {
|
||||
// Build job config from request
|
||||
config := &plugin_pb.JobTypeConfig{
|
||||
JobType: jobReq.JobType,
|
||||
AdminConfig: nil,
|
||||
WorkerConfig: jobReq.Config,
|
||||
}
|
||||
|
||||
// Create executor
|
||||
executor := NewExecutor(jobReq.JobId, config)
|
||||
|
||||
// Progress channel
|
||||
progressChan := make(chan *plugin_pb.JobProgress, 10)
|
||||
|
||||
// Execute job
|
||||
go func() {
|
||||
if err := executor.Execute(ctx, jobReq.Metadata, progressChan); err != nil {
|
||||
glog.Errorf("erasure_coding worker: job execution failed (id=%s): %v", jobReq.JobId, err)
|
||||
}
|
||||
close(progressChan)
|
||||
}()
|
||||
|
||||
// TODO: Send progress updates back to admin via ExecuteJob RPC
|
||||
for progress := range progressChan {
|
||||
_ = progress
|
||||
glog.Infof("erasure_coding worker: job %s progress: %d%%", jobReq.JobId, progress.ProgressPercent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdminCommand processes admin commands
|
||||
func (w *Worker) handleAdminCommand(cmd *plugin_pb.AdminCommand) {
|
||||
glog.Infof("erasure_coding worker: received admin command: %v", cmd.CommandType)
|
||||
|
||||
switch cmd.CommandType {
|
||||
case plugin_pb.AdminCommand_RELOAD_CONFIG:
|
||||
glog.Infof("erasure_coding worker: reloading configuration")
|
||||
case plugin_pb.AdminCommand_ENABLE_JOB_TYPE:
|
||||
glog.Infof("erasure_coding worker: enabling erasure_coding job type")
|
||||
case plugin_pb.AdminCommand_DISABLE_JOB_TYPE:
|
||||
glog.Infof("erasure_coding worker: disabling erasure_coding job type")
|
||||
case plugin_pb.AdminCommand_SHUTDOWN:
|
||||
glog.Infof("erasure_coding worker: received shutdown command")
|
||||
w.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// GetCapabilities returns the worker's capabilities
|
||||
func (w *Worker) GetCapabilities() []*plugin_pb.JobTypeCapability {
|
||||
return []*plugin_pb.JobTypeCapability{
|
||||
{
|
||||
JobType: "erasure_coding",
|
||||
CanDetect: true,
|
||||
CanExecute: true,
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigurationSchema returns the configuration schema
|
||||
func (w *Worker) GetConfigurationSchema() *plugin_pb.JobTypeConfigSchema {
|
||||
return GetConfigurationSchema()
|
||||
}
|
||||
|
||||
// Run is a convenience method that starts the worker and waits for context cancellation
|
||||
func (w *Worker) Run(ctx context.Context) error {
|
||||
if err := w.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartWorkerServer starts the erasure coding worker with the given configuration
|
||||
func StartWorkerServer(masterAddr string, httpPort int) error {
|
||||
pluginID := fmt.Sprintf("ec_worker_%d", time.Now().Unix())
|
||||
worker := NewWorker(pluginID, masterAddr, httpPort)
|
||||
|
||||
ctx := context.Background()
|
||||
return worker.Run(ctx)
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package vacuum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
// Detector scans for volumes that are candidates for vacuuming
|
||||
type Detector struct {
|
||||
masterAddr string
|
||||
}
|
||||
|
||||
// VolumeInfo represents volume information for vacuum detection
|
||||
type VolumeInfo struct {
|
||||
ID string
|
||||
Collection string
|
||||
SizeGB int64
|
||||
UsedGB int64
|
||||
GarbageRatio float64
|
||||
CreatedAt time.Time
|
||||
LastVacuumAt time.Time
|
||||
Replicas int32
|
||||
}
|
||||
|
||||
// NewDetector creates a new vacuum detector
|
||||
func NewDetector(masterAddr string) *Detector {
|
||||
return &Detector{
|
||||
masterAddr: masterAddr,
|
||||
}
|
||||
}
|
||||
|
||||
// DetectJobs scans for volumes with high garbage ratio that meet vacuum criteria
|
||||
// Returns a list of DetectedJob items sorted by garbage size (highest priority first)
|
||||
func (d *Detector) DetectJobs(ctx context.Context, config *plugin_pb.JobTypeConfig) ([]*plugin_pb.DetectedJob, error) {
|
||||
var detectedJobs []*plugin_pb.DetectedJob
|
||||
|
||||
// Extract configuration parameters
|
||||
garbageThreshold := float64(30) // default 30%
|
||||
minVolumeAge := 24 * time.Hour // default 24h
|
||||
minInterval := 7 * 24 * time.Hour // default 7d
|
||||
compactLevel := int64(2)
|
||||
enableCleanup := true
|
||||
|
||||
for _, cfv := range config.AdminConfig {
|
||||
if cfv.FieldName == "garbageThreshold" {
|
||||
garbageThreshold = float64(cfv.IntValue)
|
||||
} else if cfv.FieldName == "minVolumeAge" {
|
||||
if dur, err := time.ParseDuration(cfv.StringValue); err == nil {
|
||||
minVolumeAge = dur
|
||||
}
|
||||
} else if cfv.FieldName == "minInterval" {
|
||||
if dur, err := time.ParseDuration(cfv.StringValue); err == nil {
|
||||
minInterval = dur
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, cfv := range config.WorkerConfig {
|
||||
if cfv.FieldName == "compactLevel" {
|
||||
compactLevel = cfv.IntValue
|
||||
} else if cfv.FieldName == "enableCleanup" {
|
||||
enableCleanup = cfv.BoolValue
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("vacuum detector: scanning volumes (threshold=%.1f%%, minVolumeAge=%v, minInterval=%v)",
|
||||
garbageThreshold, minVolumeAge, minInterval)
|
||||
|
||||
// Scan volumes from master
|
||||
volumes := d.scanVolumes(ctx)
|
||||
|
||||
now := time.Now()
|
||||
for _, vol := range volumes {
|
||||
// Check if volume meets garbage ratio threshold
|
||||
if vol.GarbageRatio < garbageThreshold/100.0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if volume is old enough
|
||||
volumeAge := now.Sub(vol.CreatedAt)
|
||||
if volumeAge < minVolumeAge {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if minimum interval has passed since last vacuum
|
||||
timeSinceLastVacuum := now.Sub(vol.LastVacuumAt)
|
||||
if timeSinceLastVacuum < minInterval {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate priority based on garbage size (larger garbage = higher priority)
|
||||
garbageGB := float64(vol.UsedGB) * vol.GarbageRatio
|
||||
priority := int64(garbageGB * 1000) // Scale to get better ordering
|
||||
|
||||
jobKey := fmt.Sprintf("vacuum_%s_%s", vol.ID, now.Format("20060102150405"))
|
||||
|
||||
job := &plugin_pb.DetectedJob{
|
||||
JobKey: jobKey,
|
||||
JobType: "vacuum",
|
||||
Description: fmt.Sprintf("Vacuum volume %s (garbage ratio %.1f%%, %dGB used, %.1fGB garbage)",
|
||||
vol.ID, vol.GarbageRatio*100, vol.UsedGB, garbageGB),
|
||||
Priority: priority,
|
||||
EstimatedDuration: durationpb.New(time.Duration(vol.UsedGB) * time.Minute), // Rough estimate
|
||||
Metadata: map[string]string{
|
||||
"volume_id": vol.ID,
|
||||
"collection": vol.Collection,
|
||||
"garbage_ratio": fmt.Sprintf("%.2f", vol.GarbageRatio),
|
||||
"garbage_size_gb": fmt.Sprintf("%.1f", garbageGB),
|
||||
"used_gb": fmt.Sprintf("%d", vol.UsedGB),
|
||||
"volume_age": volumeAge.String(),
|
||||
"last_vacuum_ago": timeSinceLastVacuum.String(),
|
||||
},
|
||||
SuggestedConfig: []*plugin_pb.ConfigFieldValue{
|
||||
{
|
||||
FieldName: "compactLevel",
|
||||
IntValue: compactLevel,
|
||||
},
|
||||
{
|
||||
FieldName: "enableCleanup",
|
||||
BoolValue: enableCleanup,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
detectedJobs = append(detectedJobs, job)
|
||||
glog.Infof("vacuum detector: detected job for volume %s (garbage=%.1fGB, priority=%d)",
|
||||
vol.ID, garbageGB, priority)
|
||||
}
|
||||
|
||||
glog.Infof("vacuum detector: found %d volumes to vacuum", len(detectedJobs))
|
||||
return detectedJobs, nil
|
||||
}
|
||||
|
||||
// scanVolumes performs a scan of available volumes from the master
|
||||
// This is a placeholder that would connect to master in production
|
||||
func (d *Detector) scanVolumes(ctx context.Context) []VolumeInfo {
|
||||
// TODO: Connect to master server at d.masterAddr and get volume list
|
||||
// For now, return empty list as a framework
|
||||
var volumes []VolumeInfo
|
||||
return volumes
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
package vacuum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Executor handles the 3-step vacuum execution process
|
||||
type Executor struct {
|
||||
jobID string
|
||||
volumeID string
|
||||
config *plugin_pb.JobTypeConfig
|
||||
}
|
||||
|
||||
// ExecutionStep represents a single step in the vacuum process
|
||||
type ExecutionStep struct {
|
||||
StepNumber int
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// NewExecutor creates a new vacuum executor
|
||||
func NewExecutor(jobID string, config *plugin_pb.JobTypeConfig) *Executor {
|
||||
return &Executor{
|
||||
jobID: jobID,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs the 3-step vacuum process
|
||||
// Step 1: vacuumVolumeCheck - Verify conditions are still met
|
||||
// Step 2: vacuumVolumeCompact - Compact and defragment volume
|
||||
// Step 3: vacuumVolumeCleanup - Clean up deleted space if enableCleanup is true
|
||||
// Returns progress updates via the progressChan
|
||||
func (e *Executor) Execute(ctx context.Context, metadata map[string]string, progressChan chan<- *plugin_pb.JobProgress) error {
|
||||
volumeID := metadata["volume_id"]
|
||||
e.volumeID = volumeID
|
||||
|
||||
steps := []ExecutionStep{
|
||||
{StepNumber: 1, Name: "vacuumVolumeCheck", Description: "Verify vacuum conditions are still met"},
|
||||
{StepNumber: 2, Name: "vacuumVolumeCompact", Description: "Compact and defragment volume data"},
|
||||
{StepNumber: 3, Name: "vacuumVolumeCleanup", Description: "Clean up deleted space"},
|
||||
}
|
||||
|
||||
totalSteps := len(steps)
|
||||
for i, step := range steps {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
glog.Infof("vacuum executor job=%s: executing step %d: %s", e.jobID, step.StepNumber, step.Name)
|
||||
|
||||
// Send progress update before executing step
|
||||
progress := int32((i * 100) / totalSteps)
|
||||
progressChan <- &plugin_pb.JobProgress{
|
||||
ProgressPercent: progress,
|
||||
CurrentStep: step.Name,
|
||||
StatusMessage: step.Description,
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
|
||||
var err error
|
||||
switch step.StepNumber {
|
||||
case 1:
|
||||
err = e.vacuumVolumeCheck(ctx)
|
||||
case 2:
|
||||
err = e.vacuumVolumeCompact(ctx)
|
||||
case 3:
|
||||
err = e.vacuumVolumeCleanup(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf("vacuum executor job=%s: step %d failed: %v", e.jobID, step.StepNumber, err)
|
||||
return fmt.Errorf("step %d (%s) failed: %w", step.StepNumber, step.Name, err)
|
||||
}
|
||||
|
||||
glog.Infof("vacuum executor job=%s: step %d completed", e.jobID, step.StepNumber)
|
||||
}
|
||||
|
||||
// Send final progress update
|
||||
progressChan <- &plugin_pb.JobProgress{
|
||||
ProgressPercent: 100,
|
||||
CurrentStep: "complete",
|
||||
StatusMessage: "Vacuum completed successfully",
|
||||
UpdatedAt: timestamppb.Now(),
|
||||
}
|
||||
|
||||
glog.Infof("vacuum executor job=%s: all steps completed", e.jobID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 1: vacuumVolumeCheck verifies that vacuum conditions are still met
|
||||
// before proceeding with the operation
|
||||
func (e *Executor) vacuumVolumeCheck(ctx context.Context) error {
|
||||
glog.Infof("vacuum executor job=%s: checking vacuum eligibility for volume %s", e.jobID, e.volumeID)
|
||||
|
||||
// Extract garbage threshold from config
|
||||
var garbageThreshold float64 = 30
|
||||
for _, cfv := range e.config.AdminConfig {
|
||||
if cfv.FieldName == "garbageThreshold" {
|
||||
garbageThreshold = float64(cfv.IntValue)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Connect to master and query volume status
|
||||
// Check current garbage ratio
|
||||
// Verify volume hasn't changed significantly since detection
|
||||
// Return error if conditions no longer met
|
||||
|
||||
glog.Infof("vacuum executor job=%s: volume %s meets vacuum criteria (threshold=%.1f%%)",
|
||||
e.jobID, e.volumeID, garbageThreshold)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 2: vacuumVolumeCompact compacts and defragments the volume
|
||||
// This is the main vacuum operation that reclaims space
|
||||
func (e *Executor) vacuumVolumeCompact(ctx context.Context) error {
|
||||
glog.Infof("vacuum executor job=%s: compacting volume %s", e.jobID, e.volumeID)
|
||||
|
||||
// Extract compaction level from config
|
||||
var compactLevel int64 = 2
|
||||
for _, cfv := range e.config.WorkerConfig {
|
||||
if cfv.FieldName == "compactLevel" {
|
||||
compactLevel = cfv.IntValue
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("vacuum executor job=%s: using compaction level %d for volume %s",
|
||||
e.jobID, compactLevel, e.volumeID)
|
||||
|
||||
// TODO: Connect to volume server and perform compaction
|
||||
// 1. Read volume data, skip deleted entries
|
||||
// 2. Rewrite volume file without garbage
|
||||
// 3. Update volume metadata
|
||||
// 4. Report compaction progress
|
||||
//
|
||||
// The compaction process should:
|
||||
// - Read the volume's .dat and .idx files
|
||||
// - Skip entries marked as deleted
|
||||
// - Write new .dat file with only valid entries
|
||||
// - Update .idx file with new offsets
|
||||
// - Use compactLevel to determine aggressiveness:
|
||||
// Level 0: Minimal compaction (fast, least space saved)
|
||||
// Level 5: Aggressive compaction (slower, maximum space saved)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step 3: vacuumVolumeCleanup cleans up deleted space if enabled
|
||||
// This step frees up disk space after compaction
|
||||
func (e *Executor) vacuumVolumeCleanup(ctx context.Context) error {
|
||||
// Extract enableCleanup flag from config
|
||||
enableCleanup := true
|
||||
for _, cfv := range e.config.WorkerConfig {
|
||||
if cfv.FieldName == "enableCleanup" {
|
||||
enableCleanup = cfv.BoolValue
|
||||
}
|
||||
}
|
||||
|
||||
if !enableCleanup {
|
||||
glog.Infof("vacuum executor job=%s: skipping cleanup for volume %s (enableCleanup=false)",
|
||||
e.jobID, e.volumeID)
|
||||
return nil
|
||||
}
|
||||
|
||||
glog.Infof("vacuum executor job=%s: cleaning up deleted space for volume %s", e.jobID, e.volumeID)
|
||||
|
||||
// TODO: Connect to volume server and perform cleanup
|
||||
// 1. Frees unused space at the end of volume files
|
||||
// 2. Truncates .dat file to actual size
|
||||
// 3. Optimizes file system allocation
|
||||
// 4. Updates volume metadata with new sizes
|
||||
//
|
||||
// The cleanup process should:
|
||||
// - Trim excess capacity from the volume files
|
||||
// - Update volume size metadata on master
|
||||
// - Log space reclaimed
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package vacuum
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
)
|
||||
|
||||
// GetConfigurationSchema returns the configuration schema for the vacuum job type
|
||||
func GetConfigurationSchema() *plugin_pb.JobTypeConfigSchema {
|
||||
return &plugin_pb.JobTypeConfigSchema{
|
||||
JobType: "vacuum",
|
||||
Version: "v1",
|
||||
Description: "Automatically vacuum volumes to reclaim space by removing deleted entries",
|
||||
AdminFields: []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "garbageThreshold",
|
||||
Label: "Garbage Threshold (%)",
|
||||
Description: "Vacuum volumes when garbage ratio exceeds this percentage (0-100)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: true,
|
||||
DefaultValue: "30",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "0",
|
||||
ErrorMessage: "Garbage threshold must be at least 0",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "100",
|
||||
ErrorMessage: "Garbage threshold must be at most 100",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 0,
|
||||
MaxValue: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "minVolumeAge",
|
||||
Label: "Minimum Volume Age",
|
||||
Description: "Skip vacuuming volumes created less than this duration ago (format: 24h, 7d, etc.)",
|
||||
FieldType: plugin_pb.ConfigField_STRING,
|
||||
Required: false,
|
||||
DefaultValue: "24h",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_PATTERN,
|
||||
Value: "^(\\d+[smhd])+$",
|
||||
ErrorMessage: "Must be a valid duration (e.g., 24h, 7d, 30m)",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
Placeholder: "24h",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "minInterval",
|
||||
Label: "Minimum Interval Between Vacuums",
|
||||
Description: "Don't vacuum the same volume more frequently than this interval (format: 7d, 168h, etc.)",
|
||||
FieldType: plugin_pb.ConfigField_STRING,
|
||||
Required: false,
|
||||
DefaultValue: "7d",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_PATTERN,
|
||||
Value: "^(\\d+[smhd])+$",
|
||||
ErrorMessage: "Must be a valid duration (e.g., 7d, 168h, 1w)",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
Placeholder: "7d",
|
||||
},
|
||||
},
|
||||
},
|
||||
WorkerFields: []*plugin_pb.ConfigField{
|
||||
{
|
||||
Name: "compactLevel",
|
||||
Label: "Compaction Level",
|
||||
Description: "Compaction level for volume defragmentation (0-5, higher = more aggressive)",
|
||||
FieldType: plugin_pb.ConfigField_INT,
|
||||
Required: false,
|
||||
DefaultValue: "2",
|
||||
ValidationRules: []*plugin_pb.ValidationRule{
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MIN_VALUE,
|
||||
Value: "0",
|
||||
ErrorMessage: "Compaction level must be at least 0",
|
||||
},
|
||||
{
|
||||
RuleType: plugin_pb.ValidationRule_MAX_VALUE,
|
||||
Value: "5",
|
||||
ErrorMessage: "Compaction level must be at most 5",
|
||||
},
|
||||
},
|
||||
OptionsMsg: &plugin_pb.ConfigField_Options{
|
||||
MinValue: 0,
|
||||
MaxValue: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enableCleanup",
|
||||
Label: "Enable Space Cleanup",
|
||||
Description: "Whether to clean up and release deleted space after compaction",
|
||||
FieldType: plugin_pb.ConfigField_BOOL,
|
||||
Required: false,
|
||||
DefaultValue: "true",
|
||||
},
|
||||
},
|
||||
FieldGroups: []*plugin_pb.ConfigGroup{
|
||||
{
|
||||
Name: "Vacuum Settings",
|
||||
Label: "Vacuum Settings",
|
||||
Description: "Configuration for volume vacuuming and garbage collection",
|
||||
FieldNames: []string{
|
||||
"garbageThreshold",
|
||||
"minVolumeAge",
|
||||
"minInterval",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "advanced",
|
||||
Label: "Advanced",
|
||||
Description: "Advanced vacuum parameters",
|
||||
FieldNames: []string{
|
||||
"compactLevel",
|
||||
"enableCleanup",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
package vacuum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
// Worker is the main vacuum plugin worker
|
||||
type Worker struct {
|
||||
id string
|
||||
name string
|
||||
version string
|
||||
masterAddr string
|
||||
httpPort int
|
||||
|
||||
// gRPC connection
|
||||
conn *grpc.ClientConn
|
||||
client plugin_pb.PluginServiceClient
|
||||
|
||||
// Detector and executor
|
||||
detector *Detector
|
||||
|
||||
// Context and coordination
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Running jobs
|
||||
jobsMu sync.RWMutex
|
||||
runningJobs map[string]context.CancelFunc
|
||||
}
|
||||
|
||||
// NewWorker creates a new vacuum worker
|
||||
func NewWorker(id, masterAddr string, httpPort int) *Worker {
|
||||
return &Worker{
|
||||
id: id,
|
||||
name: "vacuum_worker",
|
||||
version: "v1",
|
||||
masterAddr: masterAddr,
|
||||
httpPort: httpPort,
|
||||
runningJobs: make(map[string]context.CancelFunc),
|
||||
detector: NewDetector(masterAddr),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the worker and connects to the admin server
|
||||
func (w *Worker) Start(ctx context.Context) error {
|
||||
glog.Infof("vacuum worker: starting (id=%s, master=%s, httpPort=%d)", w.id, w.masterAddr, w.httpPort)
|
||||
|
||||
w.ctx, w.cancel = context.WithCancel(ctx)
|
||||
|
||||
// Connect to admin server via gRPC
|
||||
// Admin server runs on httpPort + 10000
|
||||
adminPort := w.httpPort + 10000
|
||||
adminAddr := fmt.Sprintf("localhost:%d", adminPort)
|
||||
|
||||
glog.Infof("vacuum worker: connecting to admin at %s", adminAddr)
|
||||
|
||||
conn, err := grpc.Dial(adminAddr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to admin server: %w", err)
|
||||
}
|
||||
|
||||
w.conn = conn
|
||||
w.client = plugin_pb.NewPluginServiceClient(conn)
|
||||
|
||||
// Start connection handler in goroutine
|
||||
w.wg.Add(1)
|
||||
go w.handleConnection()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the worker and closes all connections
|
||||
func (w *Worker) Stop() error {
|
||||
glog.Infof("vacuum worker: stopping (id=%s)", w.id)
|
||||
|
||||
// Cancel context to signal all goroutines
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
|
||||
// Wait for goroutines with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
w.wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
glog.Warningf("vacuum worker: graceful shutdown timeout")
|
||||
}
|
||||
|
||||
// Close gRPC connection
|
||||
if w.conn != nil {
|
||||
if err := w.conn.Close(); err != nil {
|
||||
glog.Errorf("vacuum worker: error closing gRPC connection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
glog.Infof("vacuum worker: stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleConnection handles the bidirectional gRPC connection with the admin server
|
||||
func (w *Worker) handleConnection() {
|
||||
defer w.wg.Done()
|
||||
|
||||
glog.Infof("vacuum worker: establishing bidirectional stream")
|
||||
|
||||
// Create bidirectional stream
|
||||
stream, err := w.client.Connect(w.ctx)
|
||||
if err != nil {
|
||||
glog.Errorf("vacuum worker: failed to create stream: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send registration message
|
||||
if err := w.sendRegistration(stream); err != nil {
|
||||
glog.Errorf("vacuum worker: failed to send registration: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start heartbeat goroutine
|
||||
w.wg.Add(1)
|
||||
go w.sendHeartbeats(stream)
|
||||
|
||||
// Start job executor goroutine
|
||||
w.wg.Add(1)
|
||||
go w.executeJobs(stream)
|
||||
|
||||
// Listen for messages from admin
|
||||
w.listenForMessages(stream)
|
||||
|
||||
glog.Infof("vacuum worker: connection closed")
|
||||
}
|
||||
|
||||
// sendRegistration sends the initial registration message
|
||||
func (w *Worker) sendRegistration(stream plugin_pb.PluginService_ConnectClient) error {
|
||||
capabilities := []*plugin_pb.JobTypeCapability{
|
||||
{
|
||||
JobType: "vacuum",
|
||||
CanDetect: true,
|
||||
CanExecute: true,
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
|
||||
register := &plugin_pb.PluginRegister{
|
||||
PluginId: w.id,
|
||||
Name: w.name,
|
||||
Version: w.version,
|
||||
ProtocolVersion: "v1",
|
||||
Capabilities: capabilities,
|
||||
}
|
||||
|
||||
msg := &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Register{
|
||||
Register: register,
|
||||
},
|
||||
}
|
||||
|
||||
return stream.Send(msg)
|
||||
}
|
||||
|
||||
// sendHeartbeats sends periodic heartbeat messages
|
||||
func (w *Worker) sendHeartbeats(stream plugin_pb.PluginService_ConnectClient) {
|
||||
defer w.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.jobsMu.RLock()
|
||||
pendingJobs := int32(len(w.runningJobs))
|
||||
w.jobsMu.RUnlock()
|
||||
|
||||
heartbeat := &plugin_pb.PluginHeartbeat{
|
||||
PluginId: w.id,
|
||||
Timestamp: timestamppb.Now(),
|
||||
UptimeSeconds: int64(time.Since(time.Now()).Seconds()),
|
||||
PendingJobs: pendingJobs,
|
||||
CpuUsagePercent: 0, // TODO: Get actual CPU usage
|
||||
MemoryUsageMb: 0, // TODO: Get actual memory usage
|
||||
}
|
||||
|
||||
msg := &plugin_pb.PluginMessage{
|
||||
Content: &plugin_pb.PluginMessage_Heartbeat{
|
||||
Heartbeat: heartbeat,
|
||||
},
|
||||
}
|
||||
|
||||
if err := stream.Send(msg); err != nil {
|
||||
glog.Errorf("vacuum worker: failed to send heartbeat: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeJobs periodically detects and executes jobs
|
||||
func (w *Worker) executeJobs(stream plugin_pb.PluginService_ConnectClient) {
|
||||
defer w.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// TODO: Implement job detection and execution
|
||||
// Query admin for jobs or use detector to find jobs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// listenForMessages listens for messages from the admin server
|
||||
func (w *Worker) listenForMessages(stream plugin_pb.PluginService_ConnectClient) {
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
glog.Infof("vacuum worker: admin closed connection")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
glog.Errorf("vacuum worker: failed to receive message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Content == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch content := msg.Content.(type) {
|
||||
case *plugin_pb.AdminMessage_JobRequest:
|
||||
w.handleJobRequest(content.JobRequest)
|
||||
case *plugin_pb.AdminMessage_ConfigUpdate:
|
||||
glog.Infof("vacuum worker: received config update: %s", content.ConfigUpdate.JobType)
|
||||
case *plugin_pb.AdminMessage_AdminCommand:
|
||||
w.handleAdminCommand(content.AdminCommand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleJobRequest processes a job request from the admin
|
||||
func (w *Worker) handleJobRequest(jobReq *plugin_pb.JobRequest) {
|
||||
glog.Infof("vacuum worker: received job request (id=%s, type=%s)", jobReq.JobId, jobReq.JobType)
|
||||
|
||||
// Create job context
|
||||
jobCtx, cancel := context.WithCancel(w.ctx)
|
||||
|
||||
w.jobsMu.Lock()
|
||||
w.runningJobs[jobReq.JobId] = cancel
|
||||
w.jobsMu.Unlock()
|
||||
|
||||
// Execute job in goroutine
|
||||
w.wg.Add(1)
|
||||
go func() {
|
||||
defer w.wg.Done()
|
||||
defer func() {
|
||||
w.jobsMu.Lock()
|
||||
delete(w.runningJobs, jobReq.JobId)
|
||||
w.jobsMu.Unlock()
|
||||
}()
|
||||
|
||||
w.executeJobRequest(jobCtx, jobReq)
|
||||
}()
|
||||
}
|
||||
|
||||
// executeJobRequest executes a single job request
|
||||
func (w *Worker) executeJobRequest(ctx context.Context, jobReq *plugin_pb.JobRequest) {
|
||||
// Build job config from request
|
||||
config := &plugin_pb.JobTypeConfig{
|
||||
JobType: jobReq.JobType,
|
||||
AdminConfig: nil,
|
||||
WorkerConfig: jobReq.Config,
|
||||
}
|
||||
|
||||
// Create executor
|
||||
executor := NewExecutor(jobReq.JobId, config)
|
||||
|
||||
// Progress channel
|
||||
progressChan := make(chan *plugin_pb.JobProgress, 10)
|
||||
|
||||
// Execute job
|
||||
go func() {
|
||||
if err := executor.Execute(ctx, jobReq.Metadata, progressChan); err != nil {
|
||||
glog.Errorf("vacuum worker: job execution failed (id=%s): %v", jobReq.JobId, err)
|
||||
}
|
||||
close(progressChan)
|
||||
}()
|
||||
|
||||
// TODO: Send progress updates back to admin via ExecuteJob RPC
|
||||
for progress := range progressChan {
|
||||
_ = progress
|
||||
glog.Infof("vacuum worker: job %s progress: %d%%", jobReq.JobId, progress.ProgressPercent)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAdminCommand processes admin commands
|
||||
func (w *Worker) handleAdminCommand(cmd *plugin_pb.AdminCommand) {
|
||||
glog.Infof("vacuum worker: received admin command: %v", cmd.CommandType)
|
||||
|
||||
switch cmd.CommandType {
|
||||
case plugin_pb.AdminCommand_RELOAD_CONFIG:
|
||||
glog.Infof("vacuum worker: reloading configuration")
|
||||
case plugin_pb.AdminCommand_ENABLE_JOB_TYPE:
|
||||
glog.Infof("vacuum worker: enabling vacuum job type")
|
||||
case plugin_pb.AdminCommand_DISABLE_JOB_TYPE:
|
||||
glog.Infof("vacuum worker: disabling vacuum job type")
|
||||
case plugin_pb.AdminCommand_SHUTDOWN:
|
||||
glog.Infof("vacuum worker: received shutdown command")
|
||||
w.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// GetCapabilities returns the worker's capabilities
|
||||
func (w *Worker) GetCapabilities() []*plugin_pb.JobTypeCapability {
|
||||
return []*plugin_pb.JobTypeCapability{
|
||||
{
|
||||
JobType: "vacuum",
|
||||
CanDetect: true,
|
||||
CanExecute: true,
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigurationSchema returns the configuration schema
|
||||
func (w *Worker) GetConfigurationSchema() *plugin_pb.JobTypeConfigSchema {
|
||||
return GetConfigurationSchema()
|
||||
}
|
||||
|
||||
// Run is a convenience method that starts the worker and waits for context cancellation
|
||||
func (w *Worker) Run(ctx context.Context) error {
|
||||
if err := w.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartWorkerServer starts the vacuum worker with the given configuration
|
||||
func StartWorkerServer(masterAddr string, httpPort int) error {
|
||||
pluginID := fmt.Sprintf("vacuum_worker_%d", time.Now().Unix())
|
||||
worker := NewWorker(pluginID, masterAddr, httpPort)
|
||||
|
||||
ctx := context.Background()
|
||||
return worker.Run(ctx)
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// formatNumber formats a number with commas
|
||||
func formatNumber(n interface{}) string {
|
||||
switch v := n.(type) {
|
||||
case int:
|
||||
return fmt.Sprintf("%d", v)
|
||||
case int64:
|
||||
return fmt.Sprintf("%d", v)
|
||||
case float64:
|
||||
return fmt.Sprintf("%.2f", v)
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// formatBytes formats bytes as human-readable size
|
||||
func formatBytes(bytes interface{}) string {
|
||||
switch v := bytes.(type) {
|
||||
case int:
|
||||
return formatBytesValue(int64(v))
|
||||
case int64:
|
||||
return formatBytesValue(v)
|
||||
case float64:
|
||||
return formatBytesValue(int64(v))
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func formatBytesValue(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// calculatePercent calculates percentage
|
||||
func calculatePercent(current, total interface{}) float64 {
|
||||
var c, t float64
|
||||
|
||||
switch v := current.(type) {
|
||||
case int:
|
||||
c = float64(v)
|
||||
case int64:
|
||||
c = float64(v)
|
||||
case float64:
|
||||
c = v
|
||||
}
|
||||
|
||||
switch v := total.(type) {
|
||||
case int:
|
||||
t = float64(v)
|
||||
case int64:
|
||||
t = float64(v)
|
||||
case float64:
|
||||
t = v
|
||||
}
|
||||
|
||||
if t == 0 {
|
||||
return 0
|
||||
}
|
||||
return (c / t) * 100
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ PluginConfigPage(data map[string]interface{}) {
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="fas fa-cog me-2"></i>{ fmt.Sprintf("%v", data["job_type"]) } Config</h1>
|
||||
<a href="/plugins" class="btn btn-sm btn-secondary">Back</a>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs mb-4" role="tablist">
|
||||
<li class="nav-item"><button class="nav-link active" id="config-tab" data-bs-toggle="tab" data-bs-target="#config" type="button">Configuration</button></li>
|
||||
<li class="nav-item"><button class="nav-link" id="detection-tab" data-bs-toggle="tab" data-bs-target="#detection" type="button">Detection History</button></li>
|
||||
<li class="nav-item"><button class="nav-link" id="execution-tab" data-bs-toggle="tab" data-bs-target="#execution" type="button">Execution History</button></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="config" role="tabpanel">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="enabled" checked?={ data["enabled"].(bool) }>
|
||||
<label class="form-check-label" for="enabled">Enabled</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>Configuration UI coming soon</div>
|
||||
<button type="button" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="detection" role="tabpanel">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<div class="d-flex justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Detection History</h6>
|
||||
<button type="button" class="btn btn-sm btn-primary">Run Detection</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data["detection_history"].([]map[string]interface{})) == 0 {
|
||||
<div class="alert alert-info">No detection history</div>
|
||||
} else {
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Timestamp</th><th>Duration</th><th>Jobs Found</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
for _, rec := range data["detection_history"].([]map[string]interface{}) {
|
||||
<tr>
|
||||
<td>{ fmt.Sprintf("%v", rec["timestamp"]) }</td>
|
||||
<td>{ fmt.Sprintf("%vms", rec["duration_ms"]) }</td>
|
||||
<td>{ fmt.Sprintf("%v", rec["jobs_detected"]) }</td>
|
||||
<td><span class="badge bg-success">{ fmt.Sprintf("%v", rec["status"]) }</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="execution" role="tabpanel">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Execution History</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data["execution_history"].([]map[string]interface{})) == 0 {
|
||||
<div class="alert alert-info">No execution history</div>
|
||||
} else {
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Job ID</th><th>Started</th><th>Duration</th><th>Items</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
for _, rec := range data["execution_history"].([]map[string]interface{}) {
|
||||
<tr>
|
||||
<td><code>{ fmt.Sprintf("%v", rec["job_id"]) }</code></td>
|
||||
<td>{ fmt.Sprintf("%v", rec["started_at"]) }</td>
|
||||
<td>{ fmt.Sprintf("%vms", rec["duration_ms"]) }</td>
|
||||
<td>{ fmt.Sprintf("%v", rec["items_processed"]) }</td>
|
||||
<td>
|
||||
if rec["success"].(bool) {
|
||||
<span class="badge bg-success">Success</span>
|
||||
} else {
|
||||
<span class="badge bg-danger">Failed</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.977
|
||||
package app
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func PluginConfigPage(data map[string]interface{}) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><h1 class=\"h2\"><i class=\"fas fa-cog me-2\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["job_type"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 9, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " Config</h1><a href=\"/plugins\" class=\"btn btn-sm btn-secondary\">Back</a></div><ul class=\"nav nav-tabs mb-4\" role=\"tablist\"><li class=\"nav-item\"><button class=\"nav-link active\" id=\"config-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#config\" type=\"button\">Configuration</button></li><li class=\"nav-item\"><button class=\"nav-link\" id=\"detection-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#detection\" type=\"button\">Detection History</button></li><li class=\"nav-item\"><button class=\"nav-link\" id=\"execution-tab\" data-bs-toggle=\"tab\" data-bs-target=\"#execution\" type=\"button\">Execution History</button></li></ul><div class=\"tab-content\"><div class=\"tab-pane fade show active\" id=\"config\" role=\"tabpanel\"><div class=\"card shadow\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Configuration</h6></div><div class=\"card-body\"><div class=\"mb-3\"><div class=\"form-check form-switch\"><input class=\"form-check-input\" type=\"checkbox\" id=\"enabled\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if data["enabled"].(bool) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "> <label class=\"form-check-label\" for=\"enabled\">Enabled</label></div></div><div class=\"alert alert-info\"><i class=\"fas fa-info-circle me-2\"></i>Configuration UI coming soon</div><button type=\"button\" class=\"btn btn-primary\">Save</button></div></div></div><div class=\"tab-pane fade\" id=\"detection\" role=\"tabpanel\"><div class=\"card shadow\"><div class=\"card-header py-3\"><div class=\"d-flex justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\">Detection History</h6><button type=\"button\" class=\"btn btn-sm btn-primary\">Run Detection</button></div></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data["detection_history"].([]map[string]interface{})) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"alert alert-info\">No detection history</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table class=\"table table-sm\"><thead><tr><th>Timestamp</th><th>Duration</th><th>Jobs Found</th><th>Status</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, rec := range data["detection_history"].([]map[string]interface{}) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["timestamp"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 55, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%vms", rec["duration_ms"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 56, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["jobs_detected"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 57, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</td><td><span class=\"badge bg-success\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["status"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 58, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</tbody></table>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></div></div><div class=\"tab-pane fade\" id=\"execution\" role=\"tabpanel\"><div class=\"card shadow\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Execution History</h6></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data["execution_history"].([]map[string]interface{})) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"alert alert-info\">No execution history</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<table class=\"table table-sm\"><thead><tr><th>Job ID</th><th>Started</th><th>Duration</th><th>Items</th><th>Status</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, rec := range data["execution_history"].([]map[string]interface{}) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<tr><td><code>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["job_id"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 82, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</code></td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["started_at"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 83, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%vms", rec["duration_ms"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 84, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", rec["items_processed"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_config.templ`, Line: 85, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if rec["success"].(bool) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"badge bg-success\">Success</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"badge bg-danger\">Failed</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</tbody></table>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,60 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ PluginJobsPage(data map[string]interface{}) {
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="fas fa-list me-2"></i>{ fmt.Sprintf("%v", data["job_type"]) } Jobs</h1>
|
||||
<div>
|
||||
<a href="/plugins" class="btn btn-sm btn-secondary me-2">Back</a>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="location.reload();">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Pending</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["pending_count"]) }</div></div></div></div>
|
||||
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-info text-uppercase mb-1">Running</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["running_count"]) }</div></div></div></div>
|
||||
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-success text-uppercase mb-1">Completed</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["completed_count"]) }</div></div></div></div>
|
||||
<div class="col-lg-3"><div class="card"><div class="card-body"><div class="text-xs font-weight-bold text-danger text-uppercase mb-1">Failed</div><div class="h5 mb-0">{ fmt.Sprintf("%v", data["failed_count"]) }</div></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Job List</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data["jobs"].([]map[string]interface{})) == 0 {
|
||||
<div class="alert alert-info">No jobs found</div>
|
||||
} else {
|
||||
<table class="table table-sm table-hover">
|
||||
<thead><tr><th>Job ID</th><th>Description</th><th>State</th><th>Progress</th><th>Created</th></tr></thead>
|
||||
<tbody>
|
||||
for _, job := range data["jobs"].([]map[string]interface{}) {
|
||||
<tr>
|
||||
<td><code>{ fmt.Sprintf("%v", job["id"]) }</code></td>
|
||||
<td>{ fmt.Sprintf("%v", job["description"]) }</td>
|
||||
<td><span class={ "badge", jobStateClass(fmt.Sprintf("%v", job["state"])) }>{ fmt.Sprintf("%v", job["state"]) }</span></td>
|
||||
<td>
|
||||
<div class="progress" style="height: 20px;">
|
||||
<div class="progress-bar" style={ "width: " + fmt.Sprintf("%v%%", job["progress"]) }></div>
|
||||
</div>
|
||||
</td>
|
||||
<td><small>{ fmt.Sprintf("%v", job["created_at"]) }</small></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
func jobStateClass(s string) string {
|
||||
if s == "pending" { return "bg-warning" }
|
||||
if s == "running" { return "bg-info" }
|
||||
if s == "completed" { return "bg-success" }
|
||||
if s == "failed" { return "bg-danger" }
|
||||
return "bg-secondary"
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.977
|
||||
package app
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func PluginJobsPage(data map[string]interface{}) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><h1 class=\"h2\"><i class=\"fas fa-list me-2\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["job_type"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 9, Col: 86}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " Jobs</h1><div><a href=\"/plugins\" class=\"btn btn-sm btn-secondary me-2\">Back</a> <button type=\"button\" class=\"btn btn-sm btn-primary\" onclick=\"location.reload();\">Refresh</button></div></div><div class=\"row mb-4\"><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-warning text-uppercase mb-1\">Pending</div><div class=\"h5 mb-0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["pending_count"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 17, Col: 212}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></div></div></div><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-info text-uppercase mb-1\">Running</div><div class=\"h5 mb-0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["running_count"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 18, Col: 209}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div></div></div><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-success text-uppercase mb-1\">Completed</div><div class=\"h5 mb-0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["completed_count"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 19, Col: 216}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div></div></div><div class=\"col-lg-3\"><div class=\"card\"><div class=\"card-body\"><div class=\"text-xs font-weight-bold text-danger text-uppercase mb-1\">Failed</div><div class=\"h5 mb-0\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["failed_count"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 20, Col: 209}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div></div></div></div><div class=\"card shadow\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\">Job List</h6></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data["jobs"].([]map[string]interface{})) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"alert alert-info\">No jobs found</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<table class=\"table table-sm table-hover\"><thead><tr><th>Job ID</th><th>Description</th><th>State</th><th>Progress</th><th>Created</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, job := range data["jobs"].([]map[string]interface{}) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<tr><td><code>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["id"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 36, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</code></td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["description"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 37, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 = []any{"badge", jobStateClass(fmt.Sprintf("%v", job["state"]))}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["state"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 38, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></td><td><div class=\"progress\" style=\"height: 20px;\"><div class=\"progress-bar\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%v%%", job["progress"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 41, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></div></div></td><td><small>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", job["created_at"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugin_jobs.templ`, Line: 44, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</small></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</tbody></table>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func jobStateClass(s string) string {
|
||||
if s == "pending" {
|
||||
return "bg-warning"
|
||||
}
|
||||
if s == "running" {
|
||||
return "bg-info"
|
||||
}
|
||||
if s == "completed" {
|
||||
return "bg-success"
|
||||
}
|
||||
if s == "failed" {
|
||||
return "bg-danger"
|
||||
}
|
||||
return "bg-secondary"
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -1,118 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ PluginsPage(data map[string]interface{}) {
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><i class="fas fa-puzzle-piece me-2"></i>Plugin System</h1>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="location.reload();"><i class="fas fa-sync me-1"></i>Refresh</button>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Connected Plugins</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{ fmt.Sprintf("%v", data["total_plugins"]) }</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-plug fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-success shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Total Jobs</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{ fmt.Sprintf("%v", data["total_jobs"]) }</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-tasks fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-info shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Job Types</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{ fmt.Sprintf("%v", len(data["job_types"].([]string))) }</div>
|
||||
</div>
|
||||
<div class="col-auto"><i class="fas fa-cubes fa-2x text-gray-300"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-plug me-2"></i>Connected Plugins</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data["plugins"].([]map[string]interface{})) == 0 {
|
||||
<div class="alert alert-info"><i class="fas fa-info-circle me-2"></i>No plugins connected</div>
|
||||
} else {
|
||||
<table class="table table-hover table-sm">
|
||||
<thead><tr><th>Name</th><th>Version</th><th>State</th><th>Health</th><th>Jobs</th></tr></thead>
|
||||
<tbody>
|
||||
for _, p := range data["plugins"].([]map[string]interface{}) {
|
||||
<tr>
|
||||
<td>{ fmt.Sprintf("%v", p["name"]) }</td>
|
||||
<td>{ fmt.Sprintf("%v", p["version"]) }</td>
|
||||
<td><span class={ "badge", stateClass(fmt.Sprintf("%v", p["state"])) }>{ fmt.Sprintf("%v", p["state"]) }</span></td>
|
||||
<td>
|
||||
if p["healthy"].(bool) {
|
||||
<span class="badge bg-success">Healthy</span>
|
||||
} else {
|
||||
<span class="badge bg-danger">Unhealthy</span>
|
||||
}
|
||||
</td>
|
||||
<td>Running: { fmt.Sprintf("%v", p["running_jobs"]) }</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary"><i class="fas fa-cubes me-2"></i>Job Types</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
if len(data["job_types"].([]string)) == 0 {
|
||||
<div class="alert alert-info">No job types available</div>
|
||||
} else {
|
||||
<div class="row">
|
||||
for _, jt := range data["job_types"].([]string) {
|
||||
<div class="col-lg-4 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{ jt }</h5>
|
||||
<a href={ templ.SafeURL("/plugins/config/" + jt) } class="btn btn-sm btn-primary">Config</a>
|
||||
<a href={ templ.SafeURL("/plugins/jobs/" + jt) } class="btn btn-sm btn-info">Jobs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
func stateClass(s string) string {
|
||||
if s == "healthy" { return "bg-success" }
|
||||
if s == "connected" { return "bg-info" }
|
||||
if s == "unhealthy" { return "bg-warning" }
|
||||
return "bg-danger"
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.977
|
||||
package app
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func PluginsPage(data map[string]interface{}) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><h1 class=\"h2\"><i class=\"fas fa-puzzle-piece me-2\"></i>Plugin System</h1><button type=\"button\" class=\"btn btn-sm btn-primary\" onclick=\"location.reload();\"><i class=\"fas fa-sync me-1\"></i>Refresh</button></div><div class=\"row mb-4\"><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-primary shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-primary text-uppercase mb-1\">Connected Plugins</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["total_plugins"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 20, Col: 101}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-plug fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-success shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-success text-uppercase mb-1\">Total Jobs</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", data["total_jobs"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 33, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></div><div class=\"col-auto\"><i class=\"fas fa-tasks fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-3 col-md-6 mb-4\"><div class=\"card border-left-info shadow h-100 py-2\"><div class=\"card-body\"><div class=\"row no-gutters align-items-center\"><div class=\"col mr-2\"><div class=\"text-xs font-weight-bold text-info text-uppercase mb-1\">Job Types</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", len(data["job_types"].([]string))))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 46, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div></div><div class=\"col-auto\"><i class=\"fas fa-cubes fa-2x text-gray-300\"></i></div></div></div></div></div></div><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-plug me-2\"></i>Connected Plugins</h6></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data["plugins"].([]map[string]interface{})) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"alert alert-info\"><i class=\"fas fa-info-circle me-2\"></i>No plugins connected</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<table class=\"table table-hover table-sm\"><thead><tr><th>Name</th><th>Version</th><th>State</th><th>Health</th><th>Jobs</th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, p := range data["plugins"].([]map[string]interface{}) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["name"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 68, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["version"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 69, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 = []any{"badge", stateClass(fmt.Sprintf("%v", p["state"]))}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["state"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 70, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span></td><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if p["healthy"].(bool) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"badge bg-success\">Healthy</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span class=\"badge bg-danger\">Unhealthy</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td><td>Running: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%v", p["running_jobs"]))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 78, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</tbody></table>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div><div class=\"card shadow mb-4\"><div class=\"card-header py-3\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-cubes me-2\"></i>Job Types</h6></div><div class=\"card-body\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(data["job_types"].([]string)) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"alert alert-info\">No job types available</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"row\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, jt := range data["job_types"].([]string) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"col-lg-4 mb-3\"><div class=\"card\"><div class=\"card-body\"><h5 class=\"card-title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(jt)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 100, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</h5><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 templ.SafeURL
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("/plugins/config/" + jt))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 101, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-sm btn-primary\">Config</a> <a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 templ.SafeURL
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL("/plugins/jobs/" + jt))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/app/plugins.templ`, Line: 102, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" class=\"btn btn-sm btn-info\">Jobs</a></div></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func stateClass(s string) string {
|
||||
if s == "healthy" {
|
||||
return "bg-success"
|
||||
}
|
||||
if s == "connected" {
|
||||
return "bg-info"
|
||||
}
|
||||
if s == "unhealthy" {
|
||||
return "bg-warning"
|
||||
}
|
||||
return "bg-danger"
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -0,0 +1,232 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test structs that mirror the actual configuration structure
|
||||
type TestBaseConfigForTemplate struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ScanIntervalSeconds int `json:"scan_interval_seconds"`
|
||||
MaxConcurrent int `json:"max_concurrent"`
|
||||
}
|
||||
|
||||
type TestTaskConfigForTemplate struct {
|
||||
TestBaseConfigForTemplate
|
||||
TaskSpecificField float64 `json:"task_specific_field"`
|
||||
AnotherSpecificField string `json:"another_specific_field"`
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_EmbeddedStructFields(t *testing.T) {
|
||||
config := &TestTaskConfigForTemplate{
|
||||
TestBaseConfigForTemplate: TestBaseConfigForTemplate{
|
||||
Enabled: true,
|
||||
ScanIntervalSeconds: 2400,
|
||||
MaxConcurrent: 5,
|
||||
},
|
||||
TaskSpecificField: 0.18,
|
||||
AnotherSpecificField: "test_value",
|
||||
}
|
||||
|
||||
// Test embedded struct fields
|
||||
tests := []struct {
|
||||
fieldName string
|
||||
expectedValue interface{}
|
||||
description string
|
||||
}{
|
||||
{"enabled", true, "BaseConfig boolean field"},
|
||||
{"scan_interval_seconds", 2400, "BaseConfig integer field"},
|
||||
{"max_concurrent", 5, "BaseConfig integer field"},
|
||||
{"task_specific_field", 0.18, "Task-specific float field"},
|
||||
{"another_specific_field", "test_value", "Task-specific string field"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
result := getTaskFieldValue(config, test.fieldName)
|
||||
|
||||
if result != test.expectedValue {
|
||||
t.Errorf("Field %s: expected %v (%T), got %v (%T)",
|
||||
test.fieldName, test.expectedValue, test.expectedValue, result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_NonExistentField(t *testing.T) {
|
||||
config := &TestTaskConfigForTemplate{
|
||||
TestBaseConfigForTemplate: TestBaseConfigForTemplate{
|
||||
Enabled: true,
|
||||
ScanIntervalSeconds: 1800,
|
||||
MaxConcurrent: 3,
|
||||
},
|
||||
}
|
||||
|
||||
result := getTaskFieldValue(config, "non_existent_field")
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for non-existent field, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_NilConfig(t *testing.T) {
|
||||
var config *TestTaskConfigForTemplate = nil
|
||||
|
||||
result := getTaskFieldValue(config, "enabled")
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for nil config, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_EmptyStruct(t *testing.T) {
|
||||
config := &TestTaskConfigForTemplate{}
|
||||
|
||||
// Test that we can extract zero values
|
||||
tests := []struct {
|
||||
fieldName string
|
||||
expectedValue interface{}
|
||||
description string
|
||||
}{
|
||||
{"enabled", false, "Zero value boolean"},
|
||||
{"scan_interval_seconds", 0, "Zero value integer"},
|
||||
{"max_concurrent", 0, "Zero value integer"},
|
||||
{"task_specific_field", 0.0, "Zero value float"},
|
||||
{"another_specific_field", "", "Zero value string"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
result := getTaskFieldValue(config, test.fieldName)
|
||||
|
||||
if result != test.expectedValue {
|
||||
t.Errorf("Field %s: expected %v (%T), got %v (%T)",
|
||||
test.fieldName, test.expectedValue, test.expectedValue, result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_NonStructConfig(t *testing.T) {
|
||||
var config interface{} = "not a struct"
|
||||
|
||||
result := getTaskFieldValue(config, "enabled")
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("Expected nil for non-struct config, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_PointerToStruct(t *testing.T) {
|
||||
config := &TestTaskConfigForTemplate{
|
||||
TestBaseConfigForTemplate: TestBaseConfigForTemplate{
|
||||
Enabled: false,
|
||||
ScanIntervalSeconds: 900,
|
||||
MaxConcurrent: 2,
|
||||
},
|
||||
TaskSpecificField: 0.35,
|
||||
}
|
||||
|
||||
// Test that pointers are handled correctly
|
||||
enabledResult := getTaskFieldValue(config, "enabled")
|
||||
if enabledResult != false {
|
||||
t.Errorf("Expected false for enabled field, got %v", enabledResult)
|
||||
}
|
||||
|
||||
intervalResult := getTaskFieldValue(config, "scan_interval_seconds")
|
||||
if intervalResult != 900 {
|
||||
t.Errorf("Expected 900 for scan_interval_seconds field, got %v", intervalResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_FieldsWithJSONOmitempty(t *testing.T) {
|
||||
// Test struct with omitempty tags
|
||||
type TestConfigWithOmitempty struct {
|
||||
TestBaseConfigForTemplate
|
||||
OptionalField string `json:"optional_field,omitempty"`
|
||||
}
|
||||
|
||||
config := &TestConfigWithOmitempty{
|
||||
TestBaseConfigForTemplate: TestBaseConfigForTemplate{
|
||||
Enabled: true,
|
||||
ScanIntervalSeconds: 1200,
|
||||
MaxConcurrent: 4,
|
||||
},
|
||||
OptionalField: "optional_value",
|
||||
}
|
||||
|
||||
// Test that fields with omitempty are still found
|
||||
result := getTaskFieldValue(config, "optional_field")
|
||||
if result != "optional_value" {
|
||||
t.Errorf("Expected 'optional_value' for optional_field, got %v", result)
|
||||
}
|
||||
|
||||
// Test embedded fields still work
|
||||
enabledResult := getTaskFieldValue(config, "enabled")
|
||||
if enabledResult != true {
|
||||
t.Errorf("Expected true for enabled field, got %v", enabledResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTaskFieldValue_DeepEmbedding(t *testing.T) {
|
||||
// Test with multiple levels of embedding
|
||||
type DeepBaseConfig struct {
|
||||
DeepField string `json:"deep_field"`
|
||||
}
|
||||
|
||||
type MiddleConfig struct {
|
||||
DeepBaseConfig
|
||||
MiddleField int `json:"middle_field"`
|
||||
}
|
||||
|
||||
type TopConfig struct {
|
||||
MiddleConfig
|
||||
TopField bool `json:"top_field"`
|
||||
}
|
||||
|
||||
config := &TopConfig{
|
||||
MiddleConfig: MiddleConfig{
|
||||
DeepBaseConfig: DeepBaseConfig{
|
||||
DeepField: "deep_value",
|
||||
},
|
||||
MiddleField: 123,
|
||||
},
|
||||
TopField: true,
|
||||
}
|
||||
|
||||
// Test that deeply embedded fields are found
|
||||
deepResult := getTaskFieldValue(config, "deep_field")
|
||||
if deepResult != "deep_value" {
|
||||
t.Errorf("Expected 'deep_value' for deep_field, got %v", deepResult)
|
||||
}
|
||||
|
||||
middleResult := getTaskFieldValue(config, "middle_field")
|
||||
if middleResult != 123 {
|
||||
t.Errorf("Expected 123 for middle_field, got %v", middleResult)
|
||||
}
|
||||
|
||||
topResult := getTaskFieldValue(config, "top_field")
|
||||
if topResult != true {
|
||||
t.Errorf("Expected true for top_field, got %v", topResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark to ensure performance is reasonable
|
||||
func BenchmarkGetTaskFieldValue(b *testing.B) {
|
||||
config := &TestTaskConfigForTemplate{
|
||||
TestBaseConfigForTemplate: TestBaseConfigForTemplate{
|
||||
Enabled: true,
|
||||
ScanIntervalSeconds: 1800,
|
||||
MaxConcurrent: 3,
|
||||
},
|
||||
TaskSpecificField: 0.25,
|
||||
AnotherSpecificField: "benchmark_test",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Test both embedded and regular fields
|
||||
_ = getTaskFieldValue(config, "enabled")
|
||||
_ = getTaskFieldValue(config, "task_specific_field")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// getStatusColor returns Bootstrap color class for status
|
||||
func getStatusColor(status string) string {
|
||||
switch status {
|
||||
case "active", "healthy":
|
||||
return "success"
|
||||
case "warning":
|
||||
return "warning"
|
||||
case "critical", "unreachable":
|
||||
return "danger"
|
||||
default:
|
||||
return "secondary"
|
||||
}
|
||||
}
|
||||
|
||||
// formatBytes converts bytes to human readable format
|
||||
func formatBytes(bytes int64) string {
|
||||
if bytes == 0 {
|
||||
return "0 B"
|
||||
}
|
||||
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB"}
|
||||
var i int
|
||||
value := float64(bytes)
|
||||
|
||||
for value >= 1024 && i < len(units)-1 {
|
||||
value /= 1024
|
||||
i++
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
return fmt.Sprintf("%.0f %s", value, units[i])
|
||||
}
|
||||
return fmt.Sprintf("%.1f %s", value, units[i])
|
||||
}
|
||||
|
||||
// formatNumber formats large numbers with commas
|
||||
func formatNumber(num int64) string {
|
||||
if num == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
str := strconv.FormatInt(num, 10)
|
||||
result := ""
|
||||
|
||||
for i, char := range str {
|
||||
if i > 0 && (len(str)-i)%3 == 0 {
|
||||
result += ","
|
||||
}
|
||||
result += string(char)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// calculatePercent calculates percentage for progress bars
|
||||
func calculatePercent(current, max int) int {
|
||||
if max == 0 {
|
||||
return 0
|
||||
}
|
||||
return (current * 100) / max
|
||||
}
|
||||
@@ -332,27 +332,9 @@ templ Layout(c *gin.Context, content templ.Component) {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- PLUGIN SECTION -->
|
||||
<div class="sidebar-menu-group">
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
||||
<span>PLUGINS</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
if currentPath == "/plugins" {
|
||||
<a class="nav-link active" href="/plugins">
|
||||
<i class="fas fa-puzzle-piece me-2"></i>Plugins
|
||||
</a>
|
||||
} else {
|
||||
<a class="nav-link" href="/plugins">
|
||||
<i class="fas fa-puzzle-piece me-2"></i>Plugins
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<div class="pt-3">
|
||||
@content
|
||||
|
||||
@@ -487,22 +487,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</li></ul></div><!-- PLUGIN SECTION --><div class=\"sidebar-menu-group\"><h6 class=\"sidebar-heading px-3 mt-4 mb-1 text-muted\"><span>PLUGINS</span></h6><ul class=\"nav flex-column\"><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if currentPath == "/plugins" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<a class=\"nav-link active\" href=\"/plugins\"><i class=\"fas fa-puzzle-piece me-2\"></i>Plugins</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<a class=\"nav-link\" href=\"/plugins\"><i class=\"fas fa-puzzle-piece me-2\"></i>Plugins</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</li></ul></div></div><main class=\"col-md-9 ms-sm-auto col-lg-10 px-md-4\"><div class=\"pt-3\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</li></ul></div></div><!-- Main content --><main class=\"col-md-9 ms-sm-auto col-lg-10 px-md-4\"><div class=\"pt-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -510,43 +495,43 @@ func Layout(c *gin.Context, content templ.Component) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div></main></div></div><!-- Footer --><footer class=\"footer mt-auto py-3 bg-light\"><div class=\"container-fluid text-center\"><small class=\"text-muted\">© ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</div></main></div></div><!-- Footer --><footer class=\"footer mt-auto py-3 bg-light\"><div class=\"container-fluid text-center\"><small class=\"text-muted\">© ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 368, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 350, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " SeaweedFS Admin v")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " SeaweedFS Admin v")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(version.VERSION_NUMBER)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 368, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 350, Col: 102}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !strings.Contains(version.VERSION, "enterprise") {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<span class=\"mx-2\">•</span> <a href=\"https://seaweedfs.com\" target=\"_blank\" class=\"text-decoration-none\"><i class=\"fas fa-star me-1\"></i>Enterprise Version Available</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<span class=\"mx-2\">•</span> <a href=\"https://seaweedfs.com\" target=\"_blank\" class=\"text-decoration-none\"><i class=\"fas fa-star me-1\"></i>Enterprise Version Available</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script><script src=\"/static/js/iam-utils.js\"></script><script src=\"/static/js/s3tables.js\"></script></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script><script src=\"/static/js/iam-utils.js\"></script><script src=\"/static/js/s3tables.js\"></script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -575,56 +560,56 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
|
||||
templ_7745c5c3_Var28 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 396, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 378, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " - Login</title><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"></head><body class=\"bg-light\"><div class=\"container\"><div class=\"row justify-content-center min-vh-100 align-items-center\"><div class=\"col-md-6 col-lg-4\"><div class=\"card shadow\"><div class=\"card-body p-5\"><div class=\"text-center mb-4\"><i class=\"fas fa-server fa-3x text-primary mb-3\"></i><h4 class=\"card-title\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " - Login</title><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"></head><body class=\"bg-light\"><div class=\"container\"><div class=\"row justify-content-center min-vh-100 align-items-center\"><div class=\"col-md-6 col-lg-4\"><div class=\"card shadow\"><div class=\"card-body p-5\"><div class=\"text-center mb-4\"><i class=\"fas fa-server fa-3x text-primary mb-3\"></i><h4 class=\"card-title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 410, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 392, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</h4><p class=\"text-muted\">Please sign in to continue</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</h4><p class=\"text-muted\">Please sign in to continue</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<div class=\"alert alert-danger\" role=\"alert\"><i class=\"fas fa-exclamation-triangle me-2\"></i> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<div class=\"alert alert-danger\" role=\"alert\"><i class=\"fas fa-exclamation-triangle me-2\"></i> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 417, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `weed/admin/view/layout/layout.templ`, Line: 399, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<form method=\"POST\" action=\"/login\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-user\"></i></span> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div></div><div class=\"mb-4\"><label for=\"password\" class=\"form-label\">Password</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-lock\"></i></span> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div></div><button type=\"submit\" class=\"btn btn-primary w-100\"><i class=\"fas fa-sign-in-alt me-2\"></i>Sign In</button></form></div></div></div></div></div><script src=\"/static/js/bootstrap.bundle.min.js\"></script></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<form method=\"POST\" action=\"/login\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-user\"></i></span> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div></div><div class=\"mb-4\"><label for=\"password\" class=\"form-label\">Password</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-lock\"></i></span> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div></div><button type=\"submit\" class=\"btn btn-primary w-100\"><i class=\"fas fa-sign-in-alt me-2\"></i>Sign In</button></form></div></div></div></div></div><script src=\"/static/js/bootstrap.bundle.min.js\"></script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ gen:
|
||||
protoc mq_schema.proto --go_out=./schema_pb --go-grpc_out=./schema_pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative
|
||||
protoc mq_agent.proto --go_out=./mq_agent_pb --go-grpc_out=./mq_agent_pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative
|
||||
protoc worker.proto --go_out=./worker_pb --go-grpc_out=./worker_pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative
|
||||
protoc plugin.proto --go_out=./plugin_pb --go-grpc_out=./plugin_pb --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative
|
||||
# protoc filer.proto --java_out=../../other/java/client/src/main/java
|
||||
cp filer.proto ../../other/java/client/src/main/proto
|
||||
|
||||
|
||||
@@ -1,558 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package seaweed_pb;
|
||||
|
||||
option go_package = "github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb";
|
||||
|
||||
import "google/protobuf/duration.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
// PluginService: Worker-to-Admin communication service
|
||||
// Workers establish bidirectional streaming connections to register and execute jobs
|
||||
service PluginService {
|
||||
// Connect: Bidirectional stream for plugin registration, heartbeat, and lifecycle
|
||||
rpc Connect(stream PluginMessage) returns (stream AdminMessage);
|
||||
|
||||
// ExecuteJob: Bidirectional stream for job execution with progress updates
|
||||
rpc ExecuteJob(stream JobExecutionMessage) returns (stream JobProgressMessage);
|
||||
}
|
||||
|
||||
// AdminQueryService: Read-only queries for UI and monitoring
|
||||
service AdminQueryService {
|
||||
// GetPluginStats: Get current plugin system statistics
|
||||
rpc GetPluginStats(google.protobuf.Empty) returns (PluginStats);
|
||||
|
||||
// ListPlugins: List all connected plugins with their capabilities
|
||||
rpc ListPlugins(google.protobuf.Empty) returns (PluginList);
|
||||
|
||||
// ListJobs: List jobs by type and status
|
||||
rpc ListJobs(ListJobsRequest) returns (JobList);
|
||||
|
||||
// GetJob: Get detailed job information
|
||||
rpc GetJob(GetJobRequest) returns (JobDetail);
|
||||
|
||||
// GetJobExecutionLog: Stream job execution logs
|
||||
rpc GetJobExecutionLog(GetJobExecutionLogRequest) returns (stream ExecutionLogEntry);
|
||||
}
|
||||
|
||||
// AdminCommandService: Write operations for job management
|
||||
service AdminCommandService {
|
||||
// UpdateJobTypeConfig: Update configuration for a job type
|
||||
rpc UpdateJobTypeConfig(UpdateJobTypeConfigRequest) returns (JobTypeConfig);
|
||||
|
||||
// CreateJob: Manually create a job
|
||||
rpc CreateJob(CreateJobRequest) returns (Job);
|
||||
|
||||
// CancelJob: Cancel a running job
|
||||
rpc CancelJob(CancelJobRequest) returns (google.protobuf.Empty);
|
||||
|
||||
// RetryJob: Retry a failed job
|
||||
rpc RetryJob(RetryJobRequest) returns (Job);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LIFECYCLE AND REGISTRATION MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
// PluginMessage: Messages sent by worker to admin
|
||||
message PluginMessage {
|
||||
oneof content {
|
||||
PluginRegister register = 1;
|
||||
PluginHeartbeat heartbeat = 2;
|
||||
ExecutionStatusUpdate status_update = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// AdminMessage: Messages sent by admin to worker
|
||||
message AdminMessage {
|
||||
oneof content {
|
||||
JobRequest job_request = 1;
|
||||
ConfigUpdate config_update = 2;
|
||||
AdminCommand admin_command = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// PluginRegister: Initial registration message from worker
|
||||
message PluginRegister {
|
||||
string plugin_id = 1; // Unique plugin identifier
|
||||
string name = 2; // Human-readable plugin name
|
||||
string version = 3; // Plugin version
|
||||
string protocol_version = 4; // Protocol version (e.g., "v1")
|
||||
repeated JobTypeCapability capabilities = 5; // Job types this plugin can handle
|
||||
}
|
||||
|
||||
// JobTypeCapability: Declares what job type a plugin handles
|
||||
message JobTypeCapability {
|
||||
string job_type = 1; // Job type identifier (e.g., "erasure_coding", "vacuum")
|
||||
bool can_detect = 2; // Plugin can detect jobs of this type
|
||||
bool can_execute = 3; // Plugin can execute jobs of this type
|
||||
string version = 4; // Job type version
|
||||
}
|
||||
|
||||
// PluginHeartbeat: Periodic heartbeat to keep connection alive
|
||||
message PluginHeartbeat {
|
||||
string plugin_id = 1;
|
||||
google.protobuf.Timestamp timestamp = 2;
|
||||
int64 uptime_seconds = 3;
|
||||
int32 pending_jobs = 4;
|
||||
float cpu_usage_percent = 5;
|
||||
float memory_usage_mb = 6;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION SCHEMA MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
// ConfigField: Declarative field definition for UI generation
|
||||
message ConfigField {
|
||||
enum FieldType {
|
||||
BOOL = 0;
|
||||
INT = 1;
|
||||
FLOAT = 2;
|
||||
STRING = 3;
|
||||
DURATION = 4;
|
||||
SELECT = 5;
|
||||
MULTISELECT = 6;
|
||||
SECRET = 7;
|
||||
TEXTAREA = 8;
|
||||
JSON = 9;
|
||||
PERCENTAGE = 10;
|
||||
BYTES = 11;
|
||||
CRON = 12;
|
||||
}
|
||||
|
||||
string name = 1; // Field identifier
|
||||
string label = 2; // Display label
|
||||
string description = 3; // Help text
|
||||
FieldType field_type = 4; // UI control type
|
||||
bool required = 5; // Is required
|
||||
string default_value = 6; // Default value
|
||||
repeated ValidationRule validation_rules = 7; // Validation constraints
|
||||
repeated ConfigOption options = 8; // For SELECT/MULTISELECT
|
||||
ConfigField.Options options_msg = 9; // Additional options
|
||||
|
||||
message Options {
|
||||
string placeholder = 1;
|
||||
repeated string suggestions = 2;
|
||||
int32 min_length = 3;
|
||||
int32 max_length = 4;
|
||||
int32 min_value = 5;
|
||||
int32 max_value = 6;
|
||||
float min_float = 7;
|
||||
float max_float = 8;
|
||||
}
|
||||
}
|
||||
|
||||
// ValidationRule: Constraint for field validation
|
||||
message ValidationRule {
|
||||
enum RuleType {
|
||||
MIN_VALUE = 0;
|
||||
MAX_VALUE = 1;
|
||||
PATTERN = 2;
|
||||
MIN_LENGTH = 3;
|
||||
MAX_LENGTH = 4;
|
||||
CUSTOM = 5;
|
||||
}
|
||||
|
||||
RuleType rule_type = 1;
|
||||
string value = 2;
|
||||
string error_message = 3;
|
||||
}
|
||||
|
||||
// ConfigOption: Selection option for SELECT/MULTISELECT
|
||||
message ConfigOption {
|
||||
string value = 1;
|
||||
string label = 2;
|
||||
string description = 3;
|
||||
}
|
||||
|
||||
// ConfigFieldValue: Value for a config field
|
||||
message ConfigFieldValue {
|
||||
string field_name = 1;
|
||||
string string_value = 2; // For STRING, CRON
|
||||
int64 int_value = 3; // For INT, BYTES
|
||||
float float_value = 4; // For FLOAT, PERCENTAGE
|
||||
bool bool_value = 5; // For BOOL
|
||||
google.protobuf.Duration duration_value = 6; // For DURATION
|
||||
repeated string multiselect_values = 7; // For MULTISELECT
|
||||
string json_value = 8; // For JSON
|
||||
}
|
||||
|
||||
// JobTypeConfig: Configuration for a job type
|
||||
message JobTypeConfig {
|
||||
string job_type = 1;
|
||||
bool enabled = 2;
|
||||
repeated ConfigFieldValue admin_config = 3; // Admin-managed settings
|
||||
repeated ConfigFieldValue worker_config = 4; // Worker-managed settings
|
||||
google.protobuf.Timestamp created_at = 5;
|
||||
google.protobuf.Timestamp updated_at = 6;
|
||||
string created_by = 7;
|
||||
}
|
||||
|
||||
// ConfigUpdateRequest: Request to update job type configuration
|
||||
message ConfigUpdateRequest {
|
||||
string job_type = 1;
|
||||
repeated ConfigFieldValue config_values = 2;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION SCHEMA DISCOVERY
|
||||
// ============================================================================
|
||||
|
||||
// GetConfigurationSchemaRequest: Request for config schema
|
||||
message GetConfigurationSchemaRequest {
|
||||
string job_type = 1;
|
||||
}
|
||||
|
||||
// JobTypeConfigSchema: Complete configuration schema for a job type
|
||||
message JobTypeConfigSchema {
|
||||
string job_type = 1;
|
||||
string version = 2;
|
||||
string description = 3;
|
||||
repeated ConfigField admin_fields = 4; // Fields managed by admin UI
|
||||
repeated ConfigField worker_fields = 5; // Fields managed by worker
|
||||
repeated ConfigGroup field_groups = 6; // Field grouping for UI
|
||||
}
|
||||
|
||||
// ConfigGroup: Grouping of related fields for UI organization
|
||||
message ConfigGroup {
|
||||
string name = 1;
|
||||
string label = 2;
|
||||
string description = 3;
|
||||
repeated string field_names = 4;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JOB DETECTION MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
// DetectionRequest: Request to detect jobs
|
||||
message DetectionRequest {
|
||||
string job_type = 1;
|
||||
JobTypeConfig config = 2; // Current configuration
|
||||
repeated string filter_tags = 3; // Optional filtering
|
||||
}
|
||||
|
||||
// DetectionResponse: Response from detection
|
||||
message DetectionResponse {
|
||||
repeated DetectedJob detected_jobs = 1;
|
||||
google.protobuf.Timestamp detection_time = 2;
|
||||
string detector_plugin_id = 3;
|
||||
}
|
||||
|
||||
// DetectedJob: A job candidate detected by worker
|
||||
message DetectedJob {
|
||||
string job_key = 1; // Unique key for deduplication
|
||||
string job_type = 2;
|
||||
string description = 3; // Human-readable description
|
||||
int64 priority = 4; // Higher = higher priority
|
||||
google.protobuf.Duration estimated_duration = 5; // Estimated execution time
|
||||
map<string, string> metadata = 6; // Job-specific metadata
|
||||
repeated ConfigFieldValue suggested_config = 7; // Suggested overrides
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JOB EXECUTION MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
// JobRequest: Request to execute a job
|
||||
message JobRequest {
|
||||
string job_id = 1;
|
||||
string job_type = 2;
|
||||
string description = 3;
|
||||
int64 priority = 4;
|
||||
google.protobuf.Timestamp created_at = 5;
|
||||
repeated ConfigFieldValue config = 6; // Job-specific config
|
||||
map<string, string> metadata = 7; // Job metadata
|
||||
string request_source = 8; // admin | detection | retry
|
||||
}
|
||||
|
||||
// JobExecutionMessage: Messages from worker during job execution
|
||||
message JobExecutionMessage {
|
||||
string job_id = 1;
|
||||
oneof content {
|
||||
JobStarted job_started = 2;
|
||||
JobProgress progress = 3;
|
||||
JobCheckpoint checkpoint = 4;
|
||||
JobCompleted job_completed = 5;
|
||||
JobFailed job_failed = 6;
|
||||
ExecutionLog log_entry = 7;
|
||||
}
|
||||
}
|
||||
|
||||
// JobStarted: Job execution started
|
||||
message JobStarted {
|
||||
google.protobuf.Timestamp started_at = 1;
|
||||
string executor_id = 2; // Plugin executor ID
|
||||
}
|
||||
|
||||
// JobProgress: Progress update during execution
|
||||
message JobProgress {
|
||||
int32 progress_percent = 1; // 0-100
|
||||
string current_step = 2;
|
||||
string status_message = 3;
|
||||
google.protobuf.Timestamp updated_at = 4;
|
||||
}
|
||||
|
||||
// JobCheckpoint: Checkpoint for resuming jobs
|
||||
message JobCheckpoint {
|
||||
string checkpoint_id = 1;
|
||||
bytes checkpoint_data = 2; // Serialized checkpoint
|
||||
google.protobuf.Timestamp created_at = 3;
|
||||
}
|
||||
|
||||
// JobCompleted: Job execution completed successfully
|
||||
message JobCompleted {
|
||||
google.protobuf.Timestamp completed_at = 1;
|
||||
string summary = 2;
|
||||
map<string, string> output = 3; // Job output data
|
||||
}
|
||||
|
||||
// JobFailed: Job execution failed
|
||||
message JobFailed {
|
||||
string error_code = 1; // Error classification
|
||||
string error_message = 2; // Human-readable error
|
||||
bytes error_details = 3; // Detailed error data
|
||||
bool retryable = 4; // Can this job be retried
|
||||
google.protobuf.Timestamp failed_at = 5;
|
||||
int32 retry_count = 6;
|
||||
}
|
||||
|
||||
// ExecutionLog: Log entry during job execution
|
||||
message ExecutionLog {
|
||||
enum LogLevel {
|
||||
DEBUG = 0;
|
||||
INFO = 1;
|
||||
WARNING = 2;
|
||||
ERROR = 3;
|
||||
}
|
||||
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
LogLevel level = 2;
|
||||
string message = 3;
|
||||
map<string, string> context = 4;
|
||||
}
|
||||
|
||||
// JobProgressMessage: Messages from admin to worker
|
||||
message JobProgressMessage {
|
||||
string job_id = 1;
|
||||
oneof content {
|
||||
JobProgressUpdate update = 2;
|
||||
ExecutionCommand command = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// JobProgressUpdate: Status update for UI
|
||||
message JobProgressUpdate {
|
||||
string job_id = 1;
|
||||
int32 progress_percent = 2;
|
||||
string current_step = 3;
|
||||
string status_message = 4;
|
||||
google.protobuf.Timestamp timestamp = 5;
|
||||
}
|
||||
|
||||
// ExecutionCommand: Command from admin to worker
|
||||
message ExecutionCommand {
|
||||
enum CommandType {
|
||||
PAUSE = 0;
|
||||
RESUME = 1;
|
||||
CANCEL = 2;
|
||||
GET_STATUS = 3;
|
||||
}
|
||||
|
||||
string job_id = 1;
|
||||
CommandType command_type = 2;
|
||||
map<string, string> parameters = 3;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STATUS UPDATE MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
// ExecutionStatusUpdate: Status update from worker
|
||||
message ExecutionStatusUpdate {
|
||||
string plugin_id = 1;
|
||||
string job_id = 2;
|
||||
string status = 3; // running | completed | failed | paused
|
||||
int32 progress_percent = 4;
|
||||
google.protobuf.Timestamp timestamp = 5;
|
||||
}
|
||||
|
||||
// ConfigUpdate: Configuration update message
|
||||
message ConfigUpdate {
|
||||
string job_type = 1;
|
||||
repeated ConfigFieldValue config_values = 2;
|
||||
google.protobuf.Timestamp updated_at = 3;
|
||||
}
|
||||
|
||||
// AdminCommand: Admin command to worker
|
||||
message AdminCommand {
|
||||
enum CommandType {
|
||||
RELOAD_CONFIG = 0;
|
||||
ENABLE_JOB_TYPE = 1;
|
||||
DISABLE_JOB_TYPE = 2;
|
||||
SHUTDOWN = 3;
|
||||
}
|
||||
|
||||
CommandType command_type = 1;
|
||||
map<string, string> parameters = 2;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// QUERY/COMMAND REQUEST/RESPONSE MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
// ListJobsRequest: Request to list jobs
|
||||
message ListJobsRequest {
|
||||
string job_type = 1;
|
||||
enum StatusFilter {
|
||||
ALL = 0;
|
||||
PENDING = 1;
|
||||
RUNNING = 2;
|
||||
COMPLETED = 3;
|
||||
FAILED = 4;
|
||||
}
|
||||
|
||||
StatusFilter status = 2;
|
||||
int32 limit = 3;
|
||||
int32 offset = 4;
|
||||
}
|
||||
|
||||
// GetJobRequest: Request for specific job
|
||||
message GetJobRequest {
|
||||
string job_id = 1;
|
||||
}
|
||||
|
||||
// GetJobExecutionLogRequest: Request for job logs
|
||||
message GetJobExecutionLogRequest {
|
||||
string job_id = 1;
|
||||
enum LogLevel {
|
||||
DEBUG = 0;
|
||||
INFO = 1;
|
||||
WARNING = 2;
|
||||
ERROR = 3;
|
||||
ALL = 4;
|
||||
}
|
||||
|
||||
LogLevel level = 2;
|
||||
int32 tail_lines = 3;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RESPONSE MESSAGES
|
||||
// ============================================================================
|
||||
|
||||
// Job: Job information
|
||||
message Job {
|
||||
string job_id = 1;
|
||||
string job_type = 2;
|
||||
string description = 3;
|
||||
int64 priority = 4;
|
||||
enum Status {
|
||||
PENDING = 0;
|
||||
RUNNING = 1;
|
||||
COMPLETED = 2;
|
||||
FAILED = 3;
|
||||
CANCELLED = 4;
|
||||
PAUSED = 5;
|
||||
}
|
||||
|
||||
Status status = 5;
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
google.protobuf.Timestamp updated_at = 7;
|
||||
string executor_plugin_id = 8;
|
||||
int32 progress_percent = 9;
|
||||
repeated ConfigFieldValue config = 10;
|
||||
}
|
||||
|
||||
// JobDetail: Detailed job information with execution history
|
||||
message JobDetail {
|
||||
Job job = 1;
|
||||
repeated ExecutionLogEntry log_entries = 2;
|
||||
JobCompleted completion_info = 3;
|
||||
JobFailed failure_info = 4;
|
||||
repeated string checkpoint_ids = 5;
|
||||
}
|
||||
|
||||
// ExecutionLogEntry: Log entry with metadata
|
||||
message ExecutionLogEntry {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
ExecutionLog.LogLevel level = 2;
|
||||
string message = 3;
|
||||
map<string, string> context = 4;
|
||||
}
|
||||
|
||||
// JobList: List of jobs
|
||||
message JobList {
|
||||
repeated Job jobs = 1;
|
||||
int32 total_count = 2;
|
||||
int32 returned_count = 3;
|
||||
}
|
||||
|
||||
// PluginStats: System-wide statistics
|
||||
message PluginStats {
|
||||
int32 total_plugins = 1;
|
||||
int32 active_plugins = 2;
|
||||
int32 total_jobs = 3;
|
||||
int32 pending_jobs = 4;
|
||||
int32 running_jobs = 5;
|
||||
int32 completed_jobs = 6;
|
||||
int32 failed_jobs = 7;
|
||||
google.protobuf.Timestamp last_update = 8;
|
||||
map<string, JobTypeStats> job_type_stats = 9;
|
||||
}
|
||||
|
||||
// JobTypeStats: Statistics for a job type
|
||||
message JobTypeStats {
|
||||
string job_type = 1;
|
||||
int32 total = 2;
|
||||
int32 pending = 3;
|
||||
int32 running = 4;
|
||||
int32 completed = 5;
|
||||
int32 failed = 6;
|
||||
float success_rate = 7;
|
||||
google.protobuf.Duration avg_execution_time = 8;
|
||||
}
|
||||
|
||||
// PluginList: List of connected plugins
|
||||
message PluginList {
|
||||
repeated PluginInfo plugins = 1;
|
||||
}
|
||||
|
||||
// PluginInfo: Information about a connected plugin
|
||||
message PluginInfo {
|
||||
string plugin_id = 1;
|
||||
string name = 2;
|
||||
string version = 3;
|
||||
string protocol_version = 4;
|
||||
google.protobuf.Timestamp connected_at = 5;
|
||||
google.protobuf.Timestamp last_heartbeat = 6;
|
||||
repeated JobTypeCapability capabilities = 7;
|
||||
bool healthy = 8;
|
||||
string status = 9;
|
||||
}
|
||||
|
||||
// UpdateJobTypeConfigRequest: Request to update config
|
||||
message UpdateJobTypeConfigRequest {
|
||||
JobTypeConfig config = 1;
|
||||
}
|
||||
|
||||
// CreateJobRequest: Request to create a job
|
||||
message CreateJobRequest {
|
||||
string job_type = 1;
|
||||
string description = 2;
|
||||
int64 priority = 3;
|
||||
repeated ConfigFieldValue config = 4;
|
||||
map<string, string> metadata = 5;
|
||||
}
|
||||
|
||||
// CancelJobRequest: Request to cancel a job
|
||||
message CancelJobRequest {
|
||||
string job_id = 1;
|
||||
}
|
||||
|
||||
// RetryJobRequest: Request to retry a job
|
||||
message RetryJobRequest {
|
||||
string job_id = 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,658 +0,0 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v6.33.4
|
||||
// source: plugin.proto
|
||||
|
||||
package plugin_pb
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
PluginService_Connect_FullMethodName = "/seaweed_pb.PluginService/Connect"
|
||||
PluginService_ExecuteJob_FullMethodName = "/seaweed_pb.PluginService/ExecuteJob"
|
||||
)
|
||||
|
||||
// PluginServiceClient is the client API for PluginService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// PluginService: Worker-to-Admin communication service
|
||||
// Workers establish bidirectional streaming connections to register and execute jobs
|
||||
type PluginServiceClient interface {
|
||||
// Connect: Bidirectional stream for plugin registration, heartbeat, and lifecycle
|
||||
Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[PluginMessage, AdminMessage], error)
|
||||
// ExecuteJob: Bidirectional stream for job execution with progress updates
|
||||
ExecuteJob(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[JobExecutionMessage, JobProgressMessage], error)
|
||||
}
|
||||
|
||||
type pluginServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewPluginServiceClient(cc grpc.ClientConnInterface) PluginServiceClient {
|
||||
return &pluginServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *pluginServiceClient) Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[PluginMessage, AdminMessage], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &PluginService_ServiceDesc.Streams[0], PluginService_Connect_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[PluginMessage, AdminMessage]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type PluginService_ConnectClient = grpc.BidiStreamingClient[PluginMessage, AdminMessage]
|
||||
|
||||
func (c *pluginServiceClient) ExecuteJob(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[JobExecutionMessage, JobProgressMessage], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &PluginService_ServiceDesc.Streams[1], PluginService_ExecuteJob_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[JobExecutionMessage, JobProgressMessage]{ClientStream: stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type PluginService_ExecuteJobClient = grpc.BidiStreamingClient[JobExecutionMessage, JobProgressMessage]
|
||||
|
||||
// PluginServiceServer is the server API for PluginService service.
|
||||
// All implementations must embed UnimplementedPluginServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// PluginService: Worker-to-Admin communication service
|
||||
// Workers establish bidirectional streaming connections to register and execute jobs
|
||||
type PluginServiceServer interface {
|
||||
// Connect: Bidirectional stream for plugin registration, heartbeat, and lifecycle
|
||||
Connect(grpc.BidiStreamingServer[PluginMessage, AdminMessage]) error
|
||||
// ExecuteJob: Bidirectional stream for job execution with progress updates
|
||||
ExecuteJob(grpc.BidiStreamingServer[JobExecutionMessage, JobProgressMessage]) error
|
||||
mustEmbedUnimplementedPluginServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedPluginServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedPluginServiceServer struct{}
|
||||
|
||||
func (UnimplementedPluginServiceServer) Connect(grpc.BidiStreamingServer[PluginMessage, AdminMessage]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method Connect not implemented")
|
||||
}
|
||||
func (UnimplementedPluginServiceServer) ExecuteJob(grpc.BidiStreamingServer[JobExecutionMessage, JobProgressMessage]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method ExecuteJob not implemented")
|
||||
}
|
||||
func (UnimplementedPluginServiceServer) mustEmbedUnimplementedPluginServiceServer() {}
|
||||
func (UnimplementedPluginServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafePluginServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to PluginServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafePluginServiceServer interface {
|
||||
mustEmbedUnimplementedPluginServiceServer()
|
||||
}
|
||||
|
||||
func RegisterPluginServiceServer(s grpc.ServiceRegistrar, srv PluginServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedPluginServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&PluginService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _PluginService_Connect_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(PluginServiceServer).Connect(&grpc.GenericServerStream[PluginMessage, AdminMessage]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type PluginService_ConnectServer = grpc.BidiStreamingServer[PluginMessage, AdminMessage]
|
||||
|
||||
func _PluginService_ExecuteJob_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(PluginServiceServer).ExecuteJob(&grpc.GenericServerStream[JobExecutionMessage, JobProgressMessage]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type PluginService_ExecuteJobServer = grpc.BidiStreamingServer[JobExecutionMessage, JobProgressMessage]
|
||||
|
||||
// PluginService_ServiceDesc is the grpc.ServiceDesc for PluginService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var PluginService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "seaweed_pb.PluginService",
|
||||
HandlerType: (*PluginServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "Connect",
|
||||
Handler: _PluginService_Connect_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "ExecuteJob",
|
||||
Handler: _PluginService_ExecuteJob_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "plugin.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
AdminQueryService_GetPluginStats_FullMethodName = "/seaweed_pb.AdminQueryService/GetPluginStats"
|
||||
AdminQueryService_ListPlugins_FullMethodName = "/seaweed_pb.AdminQueryService/ListPlugins"
|
||||
AdminQueryService_ListJobs_FullMethodName = "/seaweed_pb.AdminQueryService/ListJobs"
|
||||
AdminQueryService_GetJob_FullMethodName = "/seaweed_pb.AdminQueryService/GetJob"
|
||||
AdminQueryService_GetJobExecutionLog_FullMethodName = "/seaweed_pb.AdminQueryService/GetJobExecutionLog"
|
||||
)
|
||||
|
||||
// AdminQueryServiceClient is the client API for AdminQueryService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// AdminQueryService: Read-only queries for UI and monitoring
|
||||
type AdminQueryServiceClient interface {
|
||||
// GetPluginStats: Get current plugin system statistics
|
||||
GetPluginStats(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PluginStats, error)
|
||||
// ListPlugins: List all connected plugins with their capabilities
|
||||
ListPlugins(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PluginList, error)
|
||||
// ListJobs: List jobs by type and status
|
||||
ListJobs(ctx context.Context, in *ListJobsRequest, opts ...grpc.CallOption) (*JobList, error)
|
||||
// GetJob: Get detailed job information
|
||||
GetJob(ctx context.Context, in *GetJobRequest, opts ...grpc.CallOption) (*JobDetail, error)
|
||||
// GetJobExecutionLog: Stream job execution logs
|
||||
GetJobExecutionLog(ctx context.Context, in *GetJobExecutionLogRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecutionLogEntry], error)
|
||||
}
|
||||
|
||||
type adminQueryServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAdminQueryServiceClient(cc grpc.ClientConnInterface) AdminQueryServiceClient {
|
||||
return &adminQueryServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *adminQueryServiceClient) GetPluginStats(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PluginStats, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PluginStats)
|
||||
err := c.cc.Invoke(ctx, AdminQueryService_GetPluginStats_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminQueryServiceClient) ListPlugins(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PluginList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PluginList)
|
||||
err := c.cc.Invoke(ctx, AdminQueryService_ListPlugins_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminQueryServiceClient) ListJobs(ctx context.Context, in *ListJobsRequest, opts ...grpc.CallOption) (*JobList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(JobList)
|
||||
err := c.cc.Invoke(ctx, AdminQueryService_ListJobs_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminQueryServiceClient) GetJob(ctx context.Context, in *GetJobRequest, opts ...grpc.CallOption) (*JobDetail, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(JobDetail)
|
||||
err := c.cc.Invoke(ctx, AdminQueryService_GetJob_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminQueryServiceClient) GetJobExecutionLog(ctx context.Context, in *GetJobExecutionLogRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ExecutionLogEntry], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &AdminQueryService_ServiceDesc.Streams[0], AdminQueryService_GetJobExecutionLog_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[GetJobExecutionLogRequest, ExecutionLogEntry]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type AdminQueryService_GetJobExecutionLogClient = grpc.ServerStreamingClient[ExecutionLogEntry]
|
||||
|
||||
// AdminQueryServiceServer is the server API for AdminQueryService service.
|
||||
// All implementations must embed UnimplementedAdminQueryServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// AdminQueryService: Read-only queries for UI and monitoring
|
||||
type AdminQueryServiceServer interface {
|
||||
// GetPluginStats: Get current plugin system statistics
|
||||
GetPluginStats(context.Context, *emptypb.Empty) (*PluginStats, error)
|
||||
// ListPlugins: List all connected plugins with their capabilities
|
||||
ListPlugins(context.Context, *emptypb.Empty) (*PluginList, error)
|
||||
// ListJobs: List jobs by type and status
|
||||
ListJobs(context.Context, *ListJobsRequest) (*JobList, error)
|
||||
// GetJob: Get detailed job information
|
||||
GetJob(context.Context, *GetJobRequest) (*JobDetail, error)
|
||||
// GetJobExecutionLog: Stream job execution logs
|
||||
GetJobExecutionLog(*GetJobExecutionLogRequest, grpc.ServerStreamingServer[ExecutionLogEntry]) error
|
||||
mustEmbedUnimplementedAdminQueryServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAdminQueryServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedAdminQueryServiceServer struct{}
|
||||
|
||||
func (UnimplementedAdminQueryServiceServer) GetPluginStats(context.Context, *emptypb.Empty) (*PluginStats, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPluginStats not implemented")
|
||||
}
|
||||
func (UnimplementedAdminQueryServiceServer) ListPlugins(context.Context, *emptypb.Empty) (*PluginList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListPlugins not implemented")
|
||||
}
|
||||
func (UnimplementedAdminQueryServiceServer) ListJobs(context.Context, *ListJobsRequest) (*JobList, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListJobs not implemented")
|
||||
}
|
||||
func (UnimplementedAdminQueryServiceServer) GetJob(context.Context, *GetJobRequest) (*JobDetail, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetJob not implemented")
|
||||
}
|
||||
func (UnimplementedAdminQueryServiceServer) GetJobExecutionLog(*GetJobExecutionLogRequest, grpc.ServerStreamingServer[ExecutionLogEntry]) error {
|
||||
return status.Errorf(codes.Unimplemented, "method GetJobExecutionLog not implemented")
|
||||
}
|
||||
func (UnimplementedAdminQueryServiceServer) mustEmbedUnimplementedAdminQueryServiceServer() {}
|
||||
func (UnimplementedAdminQueryServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAdminQueryServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AdminQueryServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAdminQueryServiceServer interface {
|
||||
mustEmbedUnimplementedAdminQueryServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAdminQueryServiceServer(s grpc.ServiceRegistrar, srv AdminQueryServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAdminQueryServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&AdminQueryService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AdminQueryService_GetPluginStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminQueryServiceServer).GetPluginStats(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminQueryService_GetPluginStats_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminQueryServiceServer).GetPluginStats(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminQueryService_ListPlugins_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminQueryServiceServer).ListPlugins(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminQueryService_ListPlugins_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminQueryServiceServer).ListPlugins(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminQueryService_ListJobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListJobsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminQueryServiceServer).ListJobs(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminQueryService_ListJobs_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminQueryServiceServer).ListJobs(ctx, req.(*ListJobsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminQueryService_GetJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetJobRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminQueryServiceServer).GetJob(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminQueryService_GetJob_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminQueryServiceServer).GetJob(ctx, req.(*GetJobRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminQueryService_GetJobExecutionLog_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(GetJobExecutionLogRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(AdminQueryServiceServer).GetJobExecutionLog(m, &grpc.GenericServerStream[GetJobExecutionLogRequest, ExecutionLogEntry]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type AdminQueryService_GetJobExecutionLogServer = grpc.ServerStreamingServer[ExecutionLogEntry]
|
||||
|
||||
// AdminQueryService_ServiceDesc is the grpc.ServiceDesc for AdminQueryService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AdminQueryService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "seaweed_pb.AdminQueryService",
|
||||
HandlerType: (*AdminQueryServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetPluginStats",
|
||||
Handler: _AdminQueryService_GetPluginStats_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListPlugins",
|
||||
Handler: _AdminQueryService_ListPlugins_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListJobs",
|
||||
Handler: _AdminQueryService_ListJobs_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetJob",
|
||||
Handler: _AdminQueryService_GetJob_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "GetJobExecutionLog",
|
||||
Handler: _AdminQueryService_GetJobExecutionLog_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "plugin.proto",
|
||||
}
|
||||
|
||||
const (
|
||||
AdminCommandService_UpdateJobTypeConfig_FullMethodName = "/seaweed_pb.AdminCommandService/UpdateJobTypeConfig"
|
||||
AdminCommandService_CreateJob_FullMethodName = "/seaweed_pb.AdminCommandService/CreateJob"
|
||||
AdminCommandService_CancelJob_FullMethodName = "/seaweed_pb.AdminCommandService/CancelJob"
|
||||
AdminCommandService_RetryJob_FullMethodName = "/seaweed_pb.AdminCommandService/RetryJob"
|
||||
)
|
||||
|
||||
// AdminCommandServiceClient is the client API for AdminCommandService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// AdminCommandService: Write operations for job management
|
||||
type AdminCommandServiceClient interface {
|
||||
// UpdateJobTypeConfig: Update configuration for a job type
|
||||
UpdateJobTypeConfig(ctx context.Context, in *UpdateJobTypeConfigRequest, opts ...grpc.CallOption) (*JobTypeConfig, error)
|
||||
// CreateJob: Manually create a job
|
||||
CreateJob(ctx context.Context, in *CreateJobRequest, opts ...grpc.CallOption) (*Job, error)
|
||||
// CancelJob: Cancel a running job
|
||||
CancelJob(ctx context.Context, in *CancelJobRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
// RetryJob: Retry a failed job
|
||||
RetryJob(ctx context.Context, in *RetryJobRequest, opts ...grpc.CallOption) (*Job, error)
|
||||
}
|
||||
|
||||
type adminCommandServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewAdminCommandServiceClient(cc grpc.ClientConnInterface) AdminCommandServiceClient {
|
||||
return &adminCommandServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *adminCommandServiceClient) UpdateJobTypeConfig(ctx context.Context, in *UpdateJobTypeConfigRequest, opts ...grpc.CallOption) (*JobTypeConfig, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(JobTypeConfig)
|
||||
err := c.cc.Invoke(ctx, AdminCommandService_UpdateJobTypeConfig_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminCommandServiceClient) CreateJob(ctx context.Context, in *CreateJobRequest, opts ...grpc.CallOption) (*Job, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Job)
|
||||
err := c.cc.Invoke(ctx, AdminCommandService_CreateJob_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminCommandServiceClient) CancelJob(ctx context.Context, in *CancelJobRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, AdminCommandService_CancelJob_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *adminCommandServiceClient) RetryJob(ctx context.Context, in *RetryJobRequest, opts ...grpc.CallOption) (*Job, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(Job)
|
||||
err := c.cc.Invoke(ctx, AdminCommandService_RetryJob_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminCommandServiceServer is the server API for AdminCommandService service.
|
||||
// All implementations must embed UnimplementedAdminCommandServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// AdminCommandService: Write operations for job management
|
||||
type AdminCommandServiceServer interface {
|
||||
// UpdateJobTypeConfig: Update configuration for a job type
|
||||
UpdateJobTypeConfig(context.Context, *UpdateJobTypeConfigRequest) (*JobTypeConfig, error)
|
||||
// CreateJob: Manually create a job
|
||||
CreateJob(context.Context, *CreateJobRequest) (*Job, error)
|
||||
// CancelJob: Cancel a running job
|
||||
CancelJob(context.Context, *CancelJobRequest) (*emptypb.Empty, error)
|
||||
// RetryJob: Retry a failed job
|
||||
RetryJob(context.Context, *RetryJobRequest) (*Job, error)
|
||||
mustEmbedUnimplementedAdminCommandServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedAdminCommandServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedAdminCommandServiceServer struct{}
|
||||
|
||||
func (UnimplementedAdminCommandServiceServer) UpdateJobTypeConfig(context.Context, *UpdateJobTypeConfigRequest) (*JobTypeConfig, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateJobTypeConfig not implemented")
|
||||
}
|
||||
func (UnimplementedAdminCommandServiceServer) CreateJob(context.Context, *CreateJobRequest) (*Job, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateJob not implemented")
|
||||
}
|
||||
func (UnimplementedAdminCommandServiceServer) CancelJob(context.Context, *CancelJobRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CancelJob not implemented")
|
||||
}
|
||||
func (UnimplementedAdminCommandServiceServer) RetryJob(context.Context, *RetryJobRequest) (*Job, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RetryJob not implemented")
|
||||
}
|
||||
func (UnimplementedAdminCommandServiceServer) mustEmbedUnimplementedAdminCommandServiceServer() {}
|
||||
func (UnimplementedAdminCommandServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeAdminCommandServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to AdminCommandServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeAdminCommandServiceServer interface {
|
||||
mustEmbedUnimplementedAdminCommandServiceServer()
|
||||
}
|
||||
|
||||
func RegisterAdminCommandServiceServer(s grpc.ServiceRegistrar, srv AdminCommandServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAdminCommandServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&AdminCommandService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _AdminCommandService_UpdateJobTypeConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(UpdateJobTypeConfigRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminCommandServiceServer).UpdateJobTypeConfig(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminCommandService_UpdateJobTypeConfig_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminCommandServiceServer).UpdateJobTypeConfig(ctx, req.(*UpdateJobTypeConfigRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminCommandService_CreateJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CreateJobRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminCommandServiceServer).CreateJob(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminCommandService_CreateJob_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminCommandServiceServer).CreateJob(ctx, req.(*CreateJobRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminCommandService_CancelJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CancelJobRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminCommandServiceServer).CancelJob(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminCommandService_CancelJob_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminCommandServiceServer).CancelJob(ctx, req.(*CancelJobRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _AdminCommandService_RetryJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RetryJobRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(AdminCommandServiceServer).RetryJob(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: AdminCommandService_RetryJob_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(AdminCommandServiceServer).RetryJob(ctx, req.(*RetryJobRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// AdminCommandService_ServiceDesc is the grpc.ServiceDesc for AdminCommandService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var AdminCommandService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "seaweed_pb.AdminCommandService",
|
||||
HandlerType: (*AdminCommandServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "UpdateJobTypeConfig",
|
||||
Handler: _AdminCommandService_UpdateJobTypeConfig_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CreateJob",
|
||||
Handler: _AdminCommandService_CreateJob_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "CancelJob",
|
||||
Handler: _AdminCommandService_CancelJob_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RetryJob",
|
||||
Handler: _AdminCommandService_RetryJob_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "plugin.proto",
|
||||
}
|
||||
@@ -19,6 +19,21 @@ const (
|
||||
deleteMultipleObjectsLimit = 1000
|
||||
)
|
||||
|
||||
// objectLockVersionToCheckForDelete resolves which version should be validated for Object Lock protections.
|
||||
// For versioned delete without explicit versionId, this targets the latest version (empty versionId for enabled,
|
||||
// "null" for suspended) because DeleteObject affects that version's visibility.
|
||||
// This always returns a version ID to check; object lock protections are fail-closed.
|
||||
func objectLockVersionToCheckForDelete(versioningState, requestedVersionID string) string {
|
||||
if requestedVersionID != "" {
|
||||
return requestedVersionID
|
||||
}
|
||||
|
||||
if versioningState == s3_constants.VersioningSuspended {
|
||||
return "null"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
bucket, object := s3_constants.GetBucketAndObject(r)
|
||||
@@ -52,18 +67,18 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||
auditLog = s3err.GetAccessLog(r, http.StatusNoContent, s3err.ErrNone)
|
||||
}
|
||||
|
||||
lockCheckVersionID := objectLockVersionToCheckForDelete(versioningState, versionId)
|
||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
||||
if err := s3a.enforceObjectLockProtections(r, bucket, object, lockCheckVersionID, governanceBypassAllowed); err != nil {
|
||||
glog.V(2).Infof("DeleteObjectHandler: object lock check failed for %s/%s (version: %s): %v", bucket, object, lockCheckVersionID, err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
if versioningConfigured {
|
||||
// Handle versioned delete based on specific versioning state
|
||||
if versionId != "" {
|
||||
// Delete specific version (same for both enabled and suspended)
|
||||
// Check object lock permissions before deleting specific version
|
||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
||||
if err := s3a.enforceObjectLockProtections(r, bucket, object, versionId, governanceBypassAllowed); err != nil {
|
||||
glog.V(2).Infof("DeleteObjectHandler: object lock check failed for %s/%s: %v", bucket, object, err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete specific version
|
||||
err := s3a.deleteSpecificObjectVersion(bucket, object, versionId)
|
||||
if err != nil {
|
||||
@@ -77,9 +92,7 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||
} else {
|
||||
// Delete without version ID - behavior depends on versioning state
|
||||
if versioningEnabled {
|
||||
// Enabled versioning: Create delete marker (logical delete)
|
||||
// AWS S3 behavior: Delete marker creation is NOT blocked by object retention
|
||||
// because it's a logical delete that doesn't actually remove the retained version
|
||||
// Enabled versioning: create delete marker (logical delete)
|
||||
deleteMarkerVersionId, err := s3a.createDeleteMarker(bucket, object)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to create delete marker: %v", err)
|
||||
@@ -94,14 +107,6 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||
// Suspended versioning: Actually delete the "null" version object
|
||||
glog.V(2).Infof("DeleteObjectHandler: deleting null version for suspended versioning %s/%s", bucket, object)
|
||||
|
||||
// Check object lock permissions before deleting "null" version
|
||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
||||
if err := s3a.enforceObjectLockProtections(r, bucket, object, "null", governanceBypassAllowed); err != nil {
|
||||
glog.V(2).Infof("DeleteObjectHandler: object lock check failed for %s/%s: %v", bucket, object, err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the "null" version (the regular file)
|
||||
err := s3a.deleteSpecificObjectVersion(bucket, object, "null")
|
||||
if err != nil {
|
||||
@@ -116,14 +121,6 @@ func (s3a *S3ApiServer) DeleteObjectHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
} else {
|
||||
// Handle regular delete (non-versioned)
|
||||
// Check object lock permissions before deleting object
|
||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
|
||||
if err := s3a.enforceObjectLockProtections(r, bucket, object, "", governanceBypassAllowed); err != nil {
|
||||
glog.V(2).Infof("DeleteObjectHandler: object lock check failed for %s/%s: %v", bucket, object, err)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize trailing-slash object keys (e.g. "path/") to the
|
||||
// underlying directory entry path so DeleteEntry gets a valid name.
|
||||
target := util.NewFullPath(s3a.bucketDir(bucket), object)
|
||||
@@ -249,20 +246,19 @@ func (s3a *S3ApiServer) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *h
|
||||
continue
|
||||
}
|
||||
|
||||
// Check object lock permissions before deletion (only for versioned buckets)
|
||||
if versioningConfigured {
|
||||
// Validate governance bypass for this specific object
|
||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object.Key)
|
||||
if err := s3a.enforceObjectLockProtections(r, bucket, object.Key, object.VersionId, governanceBypassAllowed); err != nil {
|
||||
glog.V(2).Infof("DeleteMultipleObjectsHandler: object lock check failed for %s/%s (version: %s): %v", bucket, object.Key, object.VersionId, err)
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code,
|
||||
Message: s3err.GetAPIError(s3err.ErrAccessDenied).Description,
|
||||
Key: object.Key,
|
||||
VersionId: object.VersionId,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// Check object lock permissions before deletion (applies to all buckets: versioned or non-versioned)
|
||||
lockCheckVersionID := objectLockVersionToCheckForDelete(versioningState, object.VersionId)
|
||||
// Validate governance bypass for this specific object
|
||||
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object.Key)
|
||||
if err := s3a.enforceObjectLockProtections(r, bucket, object.Key, lockCheckVersionID, governanceBypassAllowed); err != nil {
|
||||
glog.V(2).Infof("DeleteMultipleObjectsHandler: object lock check failed for %s/%s (version: %s): %v", bucket, object.Key, lockCheckVersionID, err)
|
||||
deleteErrors = append(deleteErrors, DeleteError{
|
||||
Code: s3err.GetAPIError(s3err.ErrAccessDenied).Code,
|
||||
Message: s3err.GetAPIError(s3err.ErrAccessDenied).Description,
|
||||
Key: object.Key,
|
||||
VersionId: object.VersionId,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
var deleteVersionId string
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestObjectLockVersionToCheckForDelete(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
versioningState string
|
||||
requestedVersionID string
|
||||
expectedVersionID string
|
||||
}{
|
||||
{
|
||||
name: "enabled versioning without version id checks latest version",
|
||||
versioningState: s3_constants.VersioningEnabled,
|
||||
requestedVersionID: "",
|
||||
expectedVersionID: "",
|
||||
},
|
||||
{
|
||||
name: "suspended versioning without version id checks null version",
|
||||
versioningState: s3_constants.VersioningSuspended,
|
||||
requestedVersionID: "",
|
||||
expectedVersionID: "null",
|
||||
},
|
||||
{
|
||||
name: "specific version id is always checked",
|
||||
versioningState: s3_constants.VersioningEnabled,
|
||||
requestedVersionID: "3LgYQ7f7VxQ3",
|
||||
expectedVersionID: "3LgYQ7f7VxQ3",
|
||||
},
|
||||
{
|
||||
name: "non-versioned buckets still check current object",
|
||||
versioningState: "",
|
||||
requestedVersionID: "",
|
||||
expectedVersionID: "",
|
||||
},
|
||||
{
|
||||
name: "unknown versioning state defaults to empty version",
|
||||
versioningState: "UnexpectedState",
|
||||
requestedVersionID: "",
|
||||
expectedVersionID: "",
|
||||
},
|
||||
{
|
||||
name: "suspended versioning with specific version id checks that version",
|
||||
versioningState: s3_constants.VersioningSuspended,
|
||||
requestedVersionID: "abc123",
|
||||
expectedVersionID: "abc123",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
versionID := objectLockVersionToCheckForDelete(tc.versioningState, tc.requestedVersionID)
|
||||
assert.Equal(t, tc.expectedVersionID, versionID)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user