diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 00000000..c726f799 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,26 @@ +name: Run Tests CI + +on: + push: + + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: "1.24.0" + + - name: Checkout branch + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: Install dependencies + run: make + + - name: Run tests + run: make unit-test && make test diff --git a/Makefile b/Makefile index e8181544..358d5617 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ serverledge-cli: executor: CGO_ENABLED=0 $(GO) build -o $(BIN)/$@ cmd/$@/executor.go -DOCKERHUB_USER=grussorusso +DOCKERHUB_USER=fmuschera images: image-python310 image-nodejs17ng image-base image-python310: docker build -t $(DOCKERHUB_USER)/serverledge-python310 -f images/python310/Dockerfile . @@ -28,8 +28,16 @@ push-images: docker push $(DOCKERHUB_USER)/serverledge-base docker push $(DOCKERHUB_USER)/serverledge-nodejs17ng +# Runs integration tests (all tests EXCEPT unit tests) test: - go test -v ./... + $(GO) test -v $(shell $(GO) list ./... | grep -Ev 'internal/container|examples') -.PHONY: serverledge serverledge-cli lb executor test images +# Runs only unit tests +unit-test: + go test -v -short ./internal/container/... ./internal/lb/... + +.PHONY: serverledge serverledge-cli lb executor test unit-test integration-test images + +clean: + @test -n "$(BIN)" && [ -d "$(BIN)" ] && rm -rf $(BIN) || { echo "Invalid BIN directory: $(BIN)"; exit 1; } && go clean -testcache diff --git a/README.md b/README.md index 13642504..144b1a5b 100644 --- a/README.md +++ b/README.md @@ -69,12 +69,12 @@ Start a local Serverledge node: Register a function `func` from example python code (the handler is formatted like this: $(filename).$(functionName)): - $ bin/serverledge-cli create -f func --memory 600 --src examples/hello.py --runtime python310 --handler "hello.handler" + $ bin/serverledge-cli create -f func --memory 600 --src examples/hello.py --runtime python314 --handler "hello.handler" Register a function `func` from example javascript code (the handler is formatted like this: $(filename) and the name of the function is "handler"): - $ bin/serverledge-cli create -f func --memory 600 --src examples/hello.js --runtime nodejs17 --handler "hello" - $ bin/serverledge-cli create -f func --memory 600 --src examples/inc.js --runtime nodejs17 --handler "inc" + $ bin/serverledge-cli create -f func --memory 600 --src examples/hello.js --runtime nodejs17ng --handler "hello" + $ bin/serverledge-cli create -f func --memory 600 --src examples/inc.js --runtime nodejs17ng --handler "inc" Invoke `func` with arguments `a=2` and `b=3`: diff --git a/cmd/lb/main.go b/cmd/lb/main.go index 539ffaf8..b7308972 100644 --- a/cmd/lb/main.go +++ b/cmd/lb/main.go @@ -2,12 +2,14 @@ package main import ( "fmt" - "github.com/serverledge-faas/serverledge/internal/node" "log" "os" "os/signal" "time" + "github.com/serverledge-faas/serverledge/internal/mab" + "github.com/serverledge-faas/serverledge/internal/node" + "golang.org/x/net/context" "github.com/labstack/echo/v4" @@ -59,5 +61,6 @@ func main() { // Register a signal handler to cleanup things on termination registerTerminationHandler(e) + mab.InitBanditManager() lb.StartReverseProxy(e, myArea) } diff --git a/examples/HelloFunction.java b/examples/HelloFunction.java new file mode 100644 index 00000000..bf2c2413 --- /dev/null +++ b/examples/HelloFunction.java @@ -0,0 +1,29 @@ +package com.test; + +// The compiled jar used by serverledge-cli is in ./java_build/target + + +import java.util.Map; +import java.util.HashMap; + + +public class HelloFunction { + + + public Map handler(Object params, Map context) { + // Test output capture (stdout) + System.out.println("Log: Iniziata esecuzione Java Function!"); + System.out.println("Log: Parametri ricevuti: " + params); + + // Test output capture (stderr) + System.err.println("[DEBUG]: This is a debug message on stderr"); + + + Map result = new HashMap<>(); + result.put("message", "Hello from the Java default runtime of Serverledge!"); + result.put("received_params", params); + result.put("java_version", System.getProperty("java.version")); + + return result; + } +} \ No newline at end of file diff --git a/examples/experiments/amd-faster.go b/examples/experiments/amd-faster.go new file mode 100644 index 00000000..4a3f1782 --- /dev/null +++ b/examples/experiments/amd-faster.go @@ -0,0 +1,63 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "runtime" + + "github.com/serverledge-faas/serverledge/serverledge" +) + +// Handler is the entry point for Serverledge. +// It performs AES-GCM encryption on a generated dataset. +func myHandler(params map[string]interface{}) (interface{}, error) { + // Default data size: 128MB + dataSize := 256 * 1024 * 1024 + if val, ok := params["size"].(float64); ok { + dataSize = int(val) + } + + // Default passes: 5 + passes := 5 + + // Prepare random-like data (deterministic fill to avoid generation overhead) + data := make([]byte, dataSize) + for i := 0; i < len(data); i++ { + data[i] = byte(i) + } + + key := make([]byte, 32) // 256-bit key + nonce := make([]byte, 12) + + block, err := aes.NewCipher(key) + if err != nil { + panic(0) + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + panic(0) + } + + // Destination buffer with capacity for overhead + dst := make([]byte, 0, len(data)+aesgcm.Overhead()) + + // Execution: Encrypt data multiple times on a single thread + // This stresses the CPU pipeline and vector instructions (VAES/NEON) + for i := 0; i < passes; i++ { + dst = aesgcm.Seal(dst[:0], nonce, data, nil) + } + + return map[string]interface{}{ + "message": "AES encryption completed", + "processed_gb": (float64(dataSize) * float64(passes)) / 1024 / 1024 / 1024, + "passes": passes, + "arch": runtime.GOARCH, + }, nil +} + +func main() { + // Start the Serverledge runtime. This is a blocking function since it will start an HTTP server inside the + // container, waiting for the signal to execute the function. + serverledge.Start(myHandler) +} diff --git a/examples/experiments/amd_faster.tar b/examples/experiments/amd_faster.tar new file mode 100644 index 00000000..c3a467b1 Binary files /dev/null and b/examples/experiments/amd_faster.tar differ diff --git a/examples/experiments/amd_faster.zip b/examples/experiments/amd_faster.zip new file mode 100644 index 00000000..70c43861 Binary files /dev/null and b/examples/experiments/amd_faster.zip differ diff --git a/examples/experiments/amd_fasterV2.tar b/examples/experiments/amd_fasterV2.tar new file mode 100644 index 00000000..42f290ec Binary files /dev/null and b/examples/experiments/amd_fasterV2.tar differ diff --git a/examples/experiments/arm-faster.go b/examples/experiments/arm-faster.go new file mode 100644 index 00000000..45c3d023 --- /dev/null +++ b/examples/experiments/arm-faster.go @@ -0,0 +1,74 @@ +package main + +import ( + "compress/gzip" + "io" + "runtime" + "sync" + + "github.com/serverledge-faas/serverledge/serverledge" + _ "go.uber.org/automaxprocs" +) + +// Handler is the entry point for Serverledge. +// It performs parallel Gzip compression utilizing all available logical CPUs. +func myHandler(params map[string]interface{}) (interface{}, error) { + // Default base size: 128MB + baseSize := 256 * 1024 * 1024 + if val, ok := params["size"].(float64); ok { + baseSize = int(val) + } + + // Multiplier to increase total workload: default 4 + multiplier := 10 + + // Prepare data + data := make([]byte, baseSize) + for i := 0; i < len(data); i++ { + data[i] = byte(i) + } + + // Detect available CPUs to spawn correct number of goroutines + numThreads := runtime.NumCPU() + + // Calculate total workload and chunk size per thread + totalDataSize := baseSize * multiplier + chunkSize := totalDataSize / numThreads + if chunkSize > len(data) { + chunkSize = len(data) + } + + var wg sync.WaitGroup + + // Execution: Parallel compression + for t := 0; t < numThreads; t++ { + wg.Add(1) + go func() { + defer wg.Done() + + // We define the source slice based on chunk size. + // Note: We are compressing the same memory region repeatedly if total > base, + // but this is fine for testing CPU throughput. + src := data[:chunkSize] + + w := gzip.NewWriter(io.Discard) + _, _ = w.Write(src) + w.Close() + }() + } + + wg.Wait() + + return map[string]interface{}{ + "message": "Gzip compression completed", + "total_size": totalDataSize, + "arch": runtime.GOARCH, + "cpu": runtime.NumCPU(), + }, nil +} + +func main() { + // Start the Serverledge runtime. This is a blocking function since it will start an HTTP server inside the + // container, waiting for the signal to execute the function. + serverledge.Start(myHandler) +} diff --git a/examples/experiments/arm-target.yaml b/examples/experiments/arm-target.yaml new file mode 100644 index 00000000..3daf55ea --- /dev/null +++ b/examples/experiments/arm-target.yaml @@ -0,0 +1,11 @@ +registry: + area: "cloud-region" + udp.port: 9877 + + +etcd: + address: "5.6.7.8:2379" # set to IP of VM hosting the LB (public one) + +container: + pool: + memory: 4096 \ No newline at end of file diff --git a/examples/experiments/arm_faster.tar b/examples/experiments/arm_faster.tar new file mode 100644 index 00000000..e0a5a57d Binary files /dev/null and b/examples/experiments/arm_faster.tar differ diff --git a/examples/experiments/arm_faster.zip b/examples/experiments/arm_faster.zip new file mode 100644 index 00000000..73bb9f69 Binary files /dev/null and b/examples/experiments/arm_faster.zip differ diff --git a/examples/experiments/arm_fasterV2.tar b/examples/experiments/arm_fasterV2.tar new file mode 100644 index 00000000..c42f29b4 Binary files /dev/null and b/examples/experiments/arm_fasterV2.tar differ diff --git a/examples/experiments/cpu_hogger.sh b/examples/experiments/cpu_hogger.sh new file mode 100755 index 00000000..d5a2abe1 --- /dev/null +++ b/examples/experiments/cpu_hogger.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUTPUT_FILE="$SCRIPT_DIR/cpu_hogger.csv" + +start_ts=$(date +%s.%N) + +./bin/serverledge-cli invoke -f cpu_hogger -p 'duration:180' -p 'memory:800' + +end_ts=$(date +%s.%N) + +if [[ ! -s "$OUTPUT_FILE" ]]; then + printf "start_ts,end_ts\n" >> "$OUTPUT_FILE" +fi + +printf "%s,%s\n" "$start_ts" "$end_ts" >> "$OUTPUT_FILE" + + + +echo "Done" diff --git a/examples/experiments/cpu_hogger.tar b/examples/experiments/cpu_hogger.tar new file mode 100644 index 00000000..d53f2850 Binary files /dev/null and b/examples/experiments/cpu_hogger.tar differ diff --git a/examples/experiments/lb-config-MAB.yaml b/examples/experiments/lb-config-MAB.yaml new file mode 100644 index 00000000..6eb1aa4a --- /dev/null +++ b/examples/experiments/lb-config-MAB.yaml @@ -0,0 +1,14 @@ + +registry: + area: "cloud-region" + udp.port: 9877 + +lb: + replicas: 128 + mode: "MAB" + refresh_interval: 30 + +mab.policy: "LinUCB" + +etcd: + address: "127.0.0.1:2379" diff --git a/examples/experiments/lb-config-RR.yaml b/examples/experiments/lb-config-RR.yaml new file mode 100644 index 00000000..3adbb06c --- /dev/null +++ b/examples/experiments/lb-config-RR.yaml @@ -0,0 +1,13 @@ + +registry: + area: "cloud-region" + udp.port: 9877 + +lb: + replicas: 128 + mode: "RoundRobin" + refresh_interval: 30 + + +etcd: + address: "127.0.0.1:2379" diff --git a/examples/experiments/linpack.py b/examples/experiments/linpack.py new file mode 100644 index 00000000..3aacce81 --- /dev/null +++ b/examples/experiments/linpack.py @@ -0,0 +1,61 @@ +import time +import random + + +MATRIX_SIZE = 400 + +def gaussian_elimination(A, B): + + n = len(A) + M = [A[i][:] + [B[i]] for i in range(n)] + + for i in range(n): + pivot = M[i][i] + if pivot == 0: pivot = 1.0e-10 + + for j in range(i + 1, n): + factor = M[j][i] / pivot + for k in range(i, n + 1): + M[j][k] -= factor * M[i][k] + + x = [0.0] * n + for i in range(n - 1, -1, -1): + sum_ax = sum(M[i][j] * x[j] for j in range(i + 1, n)) + x[i] = (M[i][n] - sum_ax) / M[i][i] + + return x + +def handler(params, context): + n = MATRIX_SIZE + + ops = (2.0 * n * n * n) / 3.0 + (2.0 * n * n) + + A = [] + for _ in range(n): + row = [(random.random() - 0.5) for _ in range(n)] + A.append(row) + + B = [] + for row in A: + B.append(sum(row)) + + start = time.time() + + x = gaussian_elimination(A, B) + + duration = time.time() - start + + if duration <= 0: + duration = 0.000001 + + mflops = (ops * 1e-6 / duration) + + validity_check = abs(x[0] - 1.0) < 1e-4 + + return { + "function": "linpack_pure", + "matrix_size": n, + "mflops": mflops, + "latency_seconds": duration, + "valid": validity_check + } \ No newline at end of file diff --git a/examples/experiments/locustfile.py b/examples/experiments/locustfile.py new file mode 100644 index 00000000..7bfdc044 --- /dev/null +++ b/examples/experiments/locustfile.py @@ -0,0 +1,85 @@ +import time +import csv +import os +from locust import HttpUser, task, between, events, constant + +CSV_FILE = "experiment_results.csv" + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + if not os.path.exists(CSV_FILE): + with open(CSV_FILE, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["timestamp", "function", "response_time_s", "node_arch", "status_code", "policy", "locust_response_time"]) + +@events.request.add_listener +def on_request(request_type, name, response_time, response_length, response, exception, context, **kwargs): + policy = os.environ.get("LB_POLICY", "unknown") + + # GESTIONE DEI FALLIMENTI + if exception: + print(f"Request failed: {exception}") + with open(CSV_FILE, "a", newline="") as f: + writer = csv.writer(f) + # Scriviamo il fallimento nel CSV. Usiamo il nome dell'eccezione come status code. + writer.writerow([ + time.time(), + name, + "unknown", # Nessun tempo di risposta dal server + "unknown", # Nessuna architettura + f"FAILED: {type(exception).__name__}", + policy, + response_time or 0 + ]) + return + + node_arch = response.headers.get("Serverledge-Node-Arch", "unknown") + serverledge_response_time = "unknown" + + try: + data = response.json() + if "ResponseTime" in data: + serverledge_response_time = data["ResponseTime"] + except Exception: + pass + + with open(CSV_FILE, "a", newline="") as f: + writer = csv.writer(f) + writer.writerow([ + time.time(), + name, + serverledge_response_time, + node_arch, + response.status_code, + policy, + response_time + ]) + +# --- CLASSI UTENTE --- + +class AmdUser(HttpUser): + wait_time = constant(0.0) + weight = 1 # Peso relativo + + @task + def invoke_amd_faster(self): + self.client.post("/invoke/amd_faster", json={"params": {}}, name="amd_faster", timeout=10) + +class ArmUser(HttpUser): + wait_time = constant(0.0) + weight = 1 # Peso relativo + + @task + def invoke_arm_faster(self): + self.client.post("/invoke/arm_faster", json={"params": {}}, name="arm_faster", timeout=10) + +class ThirdFunctionUser(HttpUser): + wait_time = constant(20.0) + weight = 1 + + def on_start(self): + time.sleep(30) + + @task + def invoke_third(self): + self.client.post("/invoke/amd_only", json={"params": {"duration": 30}}, name="amd_only", timeout=50) \ No newline at end of file diff --git a/examples/experiments/plot_results.py b/examples/experiments/plot_results.py new file mode 100644 index 00000000..77008ed7 --- /dev/null +++ b/examples/experiments/plot_results.py @@ -0,0 +1,166 @@ +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import numpy as np + +sns.set_theme(style="whitegrid") +plt.rcParams.update({'font.size': 12}) + +df = pd.read_csv("experiment_results.csv") + +df = df.dropna(subset=['response_time_ms', 'node_arch']) +df = df[df['node_arch'] != 'unknown'] + +# Separate policies +policies = df['policy'].unique() +if len(policies) < 2: + print("WARNING: Only one policy found in the CSV. Cannot make comparisons.") + # Continue anyway for debugging + +# --- STEP 1: Data Alignment (Makespan Logic) --- +# Find the minimum number of requests executed between the two policies to trim the data +min_requests = df.groupby('policy').size().min() +print(f"Trimming data to the first {min_requests} requests to make the comparison fair.") + +df_trimmed = pd.DataFrame() +for p in policies: + # Take the first N requests for each policy + subset = df[df['policy'] == p].sort_values('timestamp').head(min_requests).copy() + # Recalculate a progressive index (Request ID) from 1 to N + subset['request_id'] = range(1, len(subset) + 1) + # Calculate cumulative + subset['cumulative_time_s'] = subset['response_time_ms'].cumsum() # they're already seconds, actually + df_trimmed = pd.concat([df_trimmed, subset]) + +# --- GRAPH 1: Cumulative Time (Monotonic Curves) --- +plt.figure(figsize=(10, 6)) +sns.lineplot(data=df_trimmed, x='request_id', y='cumulative_time_s', hue='policy', linewidth=2.5) + +plt.xlabel('Numero di Richieste Completate') +plt.ylabel('Tempo Totale Cumulativo (secondi)') +plt.title('Confronto Velocità: Baseline vs MAB (A parità di carico)') +plt.legend(title='Strategia') +plt.tight_layout() +plt.savefig('grafico_cumulativo.png') +plt.show() + +# --- GRAPH 2: Architecture Distribution (Bar Chart) --- +# We want to see: For each Function -> For each Policy -> How much x86 vs ARM? + +# Aggregate data: Count requests by (Policy, Function, Arch) +# Use the original (untrimmed) df to see the full behavior +count_data = df.groupby(['policy', 'function', 'node_arch']).size().reset_index(name='count') + +g = sns.catplot( + data=count_data, + kind="bar", + x="function", + y="count", + hue="node_arch", + col="policy", # Creates two separate side-by-side graphs: one for RR, one for MAB + palette="muted", + height=5, + aspect=1 +) + +# Add labels with numbers above the bars +g.set_axis_labels("Function", "Number of Executions") +g.fig.suptitle('Distribution of Architectural Choices (x86 vs ARM)', y=1.05) + +for ax in g.axes.flat: + for p in ax.containers: + ax.bar_label(p, label_type='edge') + +plt.savefig('grafico_distribuzione.png') +plt.show() + +# --- GRAPH 3: Architecture Choice Timeline (by policy, colored by function) --- + +# Ordiniamo temporalmente +df_timeline = df.sort_values('timestamp').copy() + +# Indice temporale separato per policy +df_timeline['decision_step'] = ( + df_timeline + .groupby('policy') + .cumcount() + 1 +) + +# Mappatura architetture reali +arch_mapping = {'arm64': 0, 'amd64': 1} +df_timeline = df_timeline[df_timeline['node_arch'].isin(arch_mapping)] +df_timeline['arch_numeric'] = df_timeline['node_arch'].map(arch_mapping) + +# Grafico: pannelli separati per policy, colori per funzione +g = sns.relplot( + data=df_timeline, + x='decision_step', + y='arch_numeric', + hue='function', + col='policy', + kind='scatter', + height=4, + aspect=1.5, + s=45 +) + +# Etichette e titoli +g.set_axis_labels("Decision step (ordine temporale)", "Architettura scelta") +g.set_titles("Policy: {col_name}") + +for ax in g.axes.flat: + ax.set_yticks([0, 1]) + ax.set_yticklabels(['ARM64', 'AMD64']) + +plt.suptitle("Cronologia delle decisioni architetturali nel tempo", y=1.05) +plt.tight_layout() +plt.savefig("grafico_timeline_decisioni.png") +plt.show() + + +# --- Textual Statistics --- +print("\n=== Final Statistics ===") +for p in policies: + sub = df[df['policy'] == p] + total_time = sub['response_time_ms'].sum() / 1000.0 + count = len(sub) + print(f"Policy: {p}") + print(f" - Total Requests: {count}") + print(f" - Total Cumulative Time: {total_time:.2f} s") + print(f" - Average Time per Request: {sub['response_time_ms'].mean():.2f} ms") + print("-" * 30) + +# --- Average Execution Time per Function, Architecture and Policy --- +print("\n=== Average Execution Time per Function and Architecture (by Policy) ===") + +avg_func_arch_policy = ( + df + .groupby(['policy', 'function', 'node_arch'])['response_time_ms'] + .mean() + .reset_index() + .sort_values(by=['policy', 'function', 'node_arch']) +) + +current_policy = None +current_function = None + +for _, row in avg_func_arch_policy.iterrows(): + policy = row['policy'] + func = row['function'] + arch = row['node_arch'] + avg_time = row['response_time_ms'] + + # Nuova policy + if policy != current_policy: + print(f"\nPolicy: {policy}") + current_policy = policy + current_function = None + + # Nuova funzione (dentro la policy) + if func != current_function: + print(f" Function: {func}") + current_function = func + + print(f" - {func} su {arch.upper()}: {avg_time:.2f} ms") + + diff --git a/examples/experiments/plot_results2.py b/examples/experiments/plot_results2.py new file mode 100644 index 00000000..0af4978e --- /dev/null +++ b/examples/experiments/plot_results2.py @@ -0,0 +1,121 @@ +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import numpy as np + +# Configurazione stile +sns.set_theme(style="whitegrid") +# Palette colori ad alto contrasto per distinguere le policy +custom_palette = {"MAB_LinUCB": "#E63946", "RoundRobin": "#457B9D"} + +def analyze_locust_experiment(file_path): + # 1. Caricamento + df = pd.read_csv(file_path) + df = df.sort_values(by='timestamp') + + # 2. Preprocessing Temporale + # Calcoliamo il tempo relativo per ogni policy in modo indipendente + # Così partono entrambe da t=0 per il confronto + dfs = [] + for policy, group in df.groupby('policy'): + group = group.copy() + start_time = group['timestamp'].min() + group['relative_time'] = group['timestamp'] - start_time + + # Cumulativo GLOBALE per la policy (aggregato) + group['cumulative_total'] = range(1, len(group) + 1) + + # Cumulativo PER FUNZIONE + # Ordiniamo per funzione e tempo per fare il cumcount corretto + group_func = group.sort_values(by=['function', 'relative_time']) + group_func['cumulative_func'] = group_func.groupby('function').cumcount() + 1 + + dfs.append(group_func) + + df_processed = pd.concat(dfs) + + # --------------------------------------------------------- + # GRAFICO 1: Throughput Separato (Fairness per Funzione) + # --------------------------------------------------------- + # Usiamo 'line' plot. Assi Y indipendenti (sharey=False) fondamentali + # perché primenum è molto più veloce di linpack. + g = sns.relplot( + data=df_processed, + x="relative_time", + y="cumulative_func", + hue="policy", + style="policy", + col="function", + kind="line", + palette=custom_palette, + height=5, + aspect=1.2, + linewidth=2.5, + facet_kws={'sharey': False, 'sharex': True} + ) + g.fig.suptitle('Confronto A: Velocità per singola Funzione', y=1.03, fontsize=16) + g.set_axis_labels("Tempo Trascorso (s)", "Richieste Completate") + plt.show() + + # --------------------------------------------------------- + # GRAFICO 2: Throughput Aggregato (La visione d'insieme) + # --------------------------------------------------------- + # Qui ordiniamo per tempo relativo per avere una linea crescente pulita + df_agg = df_processed.sort_values(by=['policy', 'relative_time']) + + plt.figure(figsize=(10, 6)) + sns.lineplot( + data=df_agg, + x='relative_time', + y='cumulative_total', + hue='policy', + style='policy', + palette=custom_palette, + linewidth=2.5 + ) + plt.title('Confronto B: Throughput Aggregato (Sistema Completo)', fontsize=16) + plt.xlabel('Tempo Trascorso (s)') + plt.ylabel('Totale Richieste Completate (Somma delle funzioni)') + plt.legend(title='Policy', loc='upper left') + plt.grid(True, which='both', linestyle='--', linewidth=0.5) + plt.show() + + # --------------------------------------------------------- + # GRAFICO 3: Analisi Architetturale (MAB sta imparando?) + # --------------------------------------------------------- + # Visualizziamo quale architettura viene scelta nel tempo + g = sns.FacetGrid(df_processed, col="function", row="policy", height=4, aspect=1.5, sharex=True) + g.map_dataframe(sns.scatterplot, x="relative_time", y="node_arch", alpha=0.6, s=30) + g.fig.suptitle('Strategia di Assegnazione: Chi esegue cosa?', y=1.02, fontsize=16) + g.set_axis_labels("Tempo (s)", "Architettura") + plt.show() + + # --------------------------------------------------------- + # 4. Statistiche "Fair" + # --------------------------------------------------------- + print("\n" + "="*60) + print("ANALISI PRESTAZIONI (Metrics)") + print("="*60) + + # Calcoliamo il Throughput Medio (Richieste / Durata Totale) + policy_stats = df_processed.groupby('policy').agg( + total_req=('response_time_ms', 'count'), + duration=('relative_time', 'max'), + avg_exec_time=('response_time_ms', 'mean'), + p95_exec_time=('response_time_ms', lambda x: x.quantile(0.95)) + ) + + policy_stats['throughput_req_per_sec'] = policy_stats['total_req'] / policy_stats['duration'] + + print("--- Riepilogo Generale ---") + print(policy_stats[['total_req', 'duration', 'throughput_req_per_sec']].round(2)) + + print("\n--- Dettaglio Latenza (Esecuzione Server) ---") + print(policy_stats[['avg_exec_time', 'p95_exec_time']].round(2)) + + print("\n--- Dettaglio per Funzione ---") + func_stats = df_processed.groupby(['policy', 'function'])['response_time_ms'].mean().unstack().round(2) + print(func_stats) + +if __name__ == "__main__": + analyze_locust_experiment('experiment_results.csv') diff --git a/examples/experiments/plot_results2_withCPUHogger.py b/examples/experiments/plot_results2_withCPUHogger.py new file mode 100644 index 00000000..86cb5896 --- /dev/null +++ b/examples/experiments/plot_results2_withCPUHogger.py @@ -0,0 +1,207 @@ +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import numpy as np +import os +import matplotlib.patches as mpatches + +# Configurazione stile +sns.set_theme(style="whitegrid") +# Palette colori ad alto contrasto per distinguere le policy +custom_palette = {"MAB_LinUCB": "#E63946", "MAB_UCB1": "#457B9D", "RoundRobin": "#26ac26"} + +def analyze_locust_experiment(file_path): + # 1. Caricamento Dati Principali + df = pd.read_csv(file_path) + df = df.sort_values(by='timestamp') + + # 1.1 Caricamento Dati CPU Hogger (Modificato per intervalli multipli) + hog_intervals = [] # Lista di tuple (start, end) + if os.path.exists("cpu_hogger.csv"): + try: + # Leggiamo il CSV che ha colonne start_ts e end_ts + df_hog = pd.read_csv("cpu_hogger.csv") + + # Iteriamo sulle righe per salvare tutti gli intervalli + for _, row in df_hog.iterrows(): + # Assumiamo che le colonne si chiamino 'start_ts' e 'end_ts' + if pd.notna(row['start_ts']) and pd.notna(row['end_ts']): + hog_intervals.append((row['start_ts'], row['end_ts'])) + + except Exception as e: + print(f"Nota: Impossibile leggere o processare cpu_hogger.csv ({e})") + + # 2. Preprocessing Temporale + # Calcoliamo il tempo relativo per ogni policy in modo indipendente + dfs = [] + + # Dizionario per salvare l'inizio assoluto di ogni policy + # Serve per mappare gli intervalli assoluti del hogger nel tempo relativo della policy + policy_start_times = {} + + # Salviamo anche la durata massima per evitare di disegnare hogging fuori dal grafico + policy_durations = {} + + for policy, group in df.groupby('policy'): + group = group.copy() + start_time = group['timestamp'].min() + policy_start_times[policy] = start_time + + group['relative_time'] = group['timestamp'] - start_time + policy_durations[policy] = group['relative_time'].max() + + # Cumulativo GLOBALE per la policy (aggregato) + group['cumulative_total'] = range(1, len(group) + 1) + + # Cumulativo PER FUNZIONE + # Ordiniamo per funzione e tempo per fare il cumcount corretto + group_func = group.sort_values(by=['function', 'relative_time']) + group_func['cumulative_func'] = group_func.groupby('function').cumcount() + 1 + + dfs.append(group_func) + + df_processed = pd.concat(dfs) + + # Funzione helper per disegnare l'area colorata su un asse specifico + def highlight_hog_region(ax): + if not hog_intervals: + return + + # Iteriamo su tutti gli intervalli trovati nel CSV + for h_start_abs, h_end_abs in hog_intervals: + # Controlliamo per ogni policy (per gestire il tempo relativo corretto) + for policy, p_start in policy_start_times.items(): + rel_h_start = h_start_abs - p_start + rel_h_end = h_end_abs - p_start + + # REGOLE DI SOVRAPPOSIZIONE: + # 1. L'intervallo deve finire DOPO l'inizio dell'esperimento (rel_h_end > 0) + # 2. L'intervallo deve iniziare PRIMA della fine dell'esperimento (rel_h_start < duration) + p_duration = policy_durations[policy] + + if rel_h_end > 0 and rel_h_start < p_duration: + # Disegna la banda verticale (gialla semitrasparente) + # max(0, ...) serve per tagliare l'inizio a 0 se l'evento è iniziato prima + ax.axvspan(max(0, rel_h_start), rel_h_end, color='#FFD700', alpha=0.2, lw=0, zorder=0) + + # Creiamo un patch per la legenda personalizzata + hog_patch = mpatches.Patch(color='#FFD700', alpha=0.2, label='CPU Hogging Active') + + # Funzione per aggiornare la legenda includendo il patch del hogging + def update_legend(ax): + if not hog_intervals: + return + # Recupera la legenda esistente + handles, labels = ax.get_legend_handles_labels() + # Aggiunge il nostro patch se non c'è già + if 'CPU Hogging Active' not in labels: + handles.append(hog_patch) + ax.legend(handles=handles, title='Legenda', loc='upper left') + + # --------------------------------------------------------- + # GRAFICO 1: Throughput Separato (Fairness per Funzione) + # --------------------------------------------------------- + g = sns.relplot( + data=df_processed, + x="relative_time", + y="cumulative_func", + hue="policy", + style="policy", + col="function", + kind="line", + palette=custom_palette, + height=5, + aspect=1.2, + linewidth=2.5, + facet_kws={'sharey': False, 'sharex': True} + ) + + # Applichiamo l'evidenziazione su tutti i subplot + for ax in g.axes.flatten(): + highlight_hog_region(ax) + + # Aggiungiamo la voce alla legenda (basta farlo sull'ultimo asse o riconfigurare la legenda del FacetGrid) + # Per Seaborn FacetGrid, spesso è meglio ri-aggiungere la legenda globale + g._legend.remove() # Rimuoviamo la legenda automatica esterna + # Creiamo una legenda unificata prendendo gli handle dal primo asse + handles, labels = g.axes[0,0].get_legend_handles_labels() + if hog_intervals: + handles.append(hog_patch) + g.fig.legend(handles=handles, title="Policy / Status", loc='center right', bbox_to_anchor=(1, 0.5)) + plt.subplots_adjust(right=0.85) # Facciamo spazio per la legenda esterna + + g.fig.suptitle('Confronto A: Velocità per singola Funzione', y=1.03, fontsize=16) + g.set_axis_labels("Tempo Trascorso (s)", "Richieste Completate") + plt.show() + + # --------------------------------------------------------- + # GRAFICO 2: Throughput Aggregato (La visione d'insieme) + # --------------------------------------------------------- + df_agg = df_processed.sort_values(by=['policy', 'relative_time']) + + plt.figure(figsize=(10, 6)) + ax2 = sns.lineplot( + data=df_agg, + x='relative_time', + y='cumulative_total', + hue='policy', + style='policy', + palette=custom_palette, + linewidth=2.5 + ) + + highlight_hog_region(ax2) + update_legend(ax2) # Aggiorna la legenda di questo plot specifico + + plt.title('Confronto B: Throughput Aggregato (Sistema Completo)', fontsize=16) + plt.xlabel('Tempo Trascorso (s)') + plt.ylabel('Totale Richieste Completate (Somma delle funzioni)') + plt.grid(True, which='both', linestyle='--', linewidth=0.5) + plt.show() + + # --------------------------------------------------------- + # GRAFICO 3: Analisi Architetturale (MAB sta imparando?) + # --------------------------------------------------------- + g = sns.FacetGrid(df_processed, col="function", row="policy", height=4, aspect=1.5, sharex=True) + g.map_dataframe(sns.scatterplot, x="relative_time", y="node_arch", alpha=0.6, s=30) + + for ax in g.axes.flatten(): + highlight_hog_region(ax) + + # Gestione Legenda per FacetGrid (qui non c'è hue automatica complessa, quindi possiamo inserirla manualmente in uno dei plot o fuori) + # Creiamo una legenda personalizzata solo per indicare il colore giallo, dato che le policy sono divise per righe + if hog_intervals: + g.add_legend(legend_data={'CPU Hogging Active': hog_patch}, title="Eventi") + + g.fig.suptitle('Strategia di Assegnazione: Chi esegue cosa?', y=1.02, fontsize=16) + g.set_axis_labels("Tempo (s)", "Architettura") + plt.show() + + # --------------------------------------------------------- + # 4. Statistiche "Fair" + # --------------------------------------------------------- + print("\n" + "="*60) + print("ANALISI PRESTAZIONI (Metrics)") + print("="*60) + + policy_stats = df_processed.groupby('policy').agg( + total_req=('response_time_ms', 'count'), + duration=('relative_time', 'max'), + avg_exec_time=('response_time_ms', 'mean'), + p95_exec_time=('response_time_ms', lambda x: x.quantile(0.95)) + ) + + policy_stats['throughput_req_per_sec'] = policy_stats['total_req'] / policy_stats['duration'] + + print("--- Riepilogo Generale ---") + print(policy_stats[['total_req', 'duration', 'throughput_req_per_sec']].round(2)) + + print("\n--- Dettaglio Latenza (Esecuzione Server) ---") + print(policy_stats[['avg_exec_time', 'p95_exec_time']].round(2)) + + print("\n--- Dettaglio per Funzione ---") + func_stats = df_processed.groupby(['policy', 'function'])['response_time_ms'].mean().unstack().round(2) + print(func_stats) + +if __name__ == "__main__": + analyze_locust_experiment('experiment_results.csv') diff --git a/examples/experiments/primenum.py b/examples/experiments/primenum.py new file mode 100644 index 00000000..9663eb12 --- /dev/null +++ b/examples/experiments/primenum.py @@ -0,0 +1,36 @@ +import time +import math + + +CPU_MAX_PRIME = 100000 + +def handler(params, context): + max_prime = CPU_MAX_PRIME + prime_count = 0 + iterations = 0 + + start = time.time() + + + for c in range(3, max_prime + 1): + iterations = iterations + 1 + t = int(math.sqrt(c)) + is_prime = True + + for l in range(2, t + 1): + if c % l == 0: + is_prime = False + break + + if is_prime: + prime_count += 1 + + duration = time.time() - start + + return { + "function": "primes_pure", + "max_prime_limit": max_prime, + "primes_found": prime_count, + "latency_seconds": duration, + "iterations": iterations + } \ No newline at end of file diff --git a/examples/experiments/run_comparison.sh b/examples/experiments/run_comparison.sh new file mode 100644 index 00000000..9b6ba8fa --- /dev/null +++ b/examples/experiments/run_comparison.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +LB_IP="INSERIRE_IP_QUI" +export LB_POLICY=${1:-"RoundRobin"} + +LOCUST_DURATION="10m" +USERS=9 +SPAWN_RATE=9 +RESULT_FILE="experiment_results.csv" + +echo "=============================================" +echo "STARTING EXPERIMENT" +echo "Policy: $LB_POLICY" +echo "Target: http://$LB_IP:1323" +echo "=============================================" + +locust -f locustfile.py \ + --headless \ + --users $USERS \ + --spawn-rate $SPAWN_RATE \ + --run-time $LOCUST_DURATION \ + --host "http://$LB_IP:1323" + +echo "Experiment completed. Data appended to CSV." \ No newline at end of file diff --git a/examples/experiments/worker-config.yaml b/examples/experiments/worker-config.yaml new file mode 100644 index 00000000..ce8fe86b --- /dev/null +++ b/examples/experiments/worker-config.yaml @@ -0,0 +1,15 @@ +registry: + area: "cloud-region" + udp.port: 9877 + + + +etcd: + address: "1.2.3.4:2379" + +container: + pool: + memory: 10000 + +janitor: + interval: 60 diff --git a/examples/experiments/x86-target.yaml b/examples/experiments/x86-target.yaml new file mode 100644 index 00000000..64f1bef6 --- /dev/null +++ b/examples/experiments/x86-target.yaml @@ -0,0 +1,12 @@ +registry: + area: "cloud-region" + udp.port: 9877 + + + +etcd: + address: "5.6.7.8:2379" # set to IP of VM hosting the LB (public one) + +container: + pool: + memory: 4096 diff --git a/examples/go-example/Makefile b/examples/go-example/Makefile new file mode 100644 index 00000000..37fb586e --- /dev/null +++ b/examples/go-example/Makefile @@ -0,0 +1,18 @@ +# EXAMPLE OF MAKEFILE + +AMD64_BIN=handler_amd64 +ARM64_BIN=handler_arm64 + +all: $(AMD64_BIN) $(ARM64_BIN) + +$(AMD64_BIN): + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o $(AMD64_BIN) main.go + +$(ARM64_BIN): + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o $(ARM64_BIN) main.go + +zip: all + zip -j go-function.zip $(AMD64_BIN) $(ARM64_BIN) + +clean: + rm -f $(AMD64_BIN) $(ARM64_BIN) go-function.zip diff --git a/examples/go-example/compiling-examples.md b/examples/go-example/compiling-examples.md new file mode 100644 index 00000000..e88d2765 --- /dev/null +++ b/examples/go-example/compiling-examples.md @@ -0,0 +1,70 @@ +# Building Go Functions for Serverledge + +To ensure your Go function runs correctly on Serverledge's lightweight Alpine containers (avoiding `glibc` vs `musl` issues) and supports multi-architecture clusters (x86 & ARM), follow these build steps. + +## 1. Static Compilation +You must disable CGO to make the executable self-contained and compatible with any Linux distribution (including Alpine and Scratch). +You can check the example Makefile as well. + +### Create executables for both architectures (run **both** commands) +```bash +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o handler_amd64 main.go +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o handler_arm64 main.go +``` +Removing debugging information is not strictly necessary, but may help producing smaller executables. + +## 2. Packaging the fat zip +```bash +zip -j function.zip handler_amd64 handler_arm64 +``` + +## 3. Deploy to Serverledge +```bash +# create the function +bin/serverledge-cli create \ + -f gofunc \ + --memory 300 \ + --src examples/go-example/go-function.zip \ + --runtime go125 + +# and then execute it +bin/serverledge-cli invoke \ +-f gofunc \ +-p "name:Serverledge User" \ +-p "num:127" + +# Expected output +{ + "Success": true, + "Result": "{\"message\":\"Hello Serverledge User\",\"number\":127,\"is_prime\":true}", + "ResponseTime": 0.397966139, + "IsWarmStart": false, + "InitTime": 0.3957812, + "QueueingTime": 0, + "OffloadLatency": 0, + "Duration": 0.002184848000000017, + "Output": "" +} +``` + +### Or with output capture enabled +```bash +bin/serverledge-cli invoke \ +-f gofunc \ +-p "name:Serverledge User" \ +-p "num:127" \ +-o # flag to capture the output + +# Expected output +{ + "Success": true, + "Result": "{\"message\":\"Hello Serverledge User\",\"number\":127,\"is_prime\":true}", + "ResponseTime": 0.001020982, + "IsWarmStart": true, + "InitTime": 0.000054193, + "QueueingTime": 0, + "OffloadLatency": 0, + "Duration": 0.000966691, + "Output": "2025/12/01 11:12:46 Processing request for user: Serverledge User...\n2025/12/01 11:12:46 Checked number: 127 (Prime: true)\n" +} +``` diff --git a/examples/go-example/go-function.zip b/examples/go-example/go-function.zip new file mode 100644 index 00000000..234c09ce Binary files /dev/null and b/examples/go-example/go-function.zip differ diff --git a/examples/go-example/go.mod b/examples/go-example/go.mod new file mode 100644 index 00000000..0936b3ef --- /dev/null +++ b/examples/go-example/go.mod @@ -0,0 +1,8 @@ +module go-example + +go 1.25 + +// TODO: THIS HAS TO BE REMOVED IF THIS IS EVER MERGED TO THE MAIN REPOSITORY +replace github.com/serverledge-faas/serverledge => github.com/FilippoMuschera/serverledge-tesi v0.0.0-20251201113838-6fd3cf13f0a4 + +require github.com/serverledge-faas/serverledge v0.1.0 diff --git a/examples/go-example/main.go b/examples/go-example/main.go new file mode 100644 index 00000000..dd422ddb --- /dev/null +++ b/examples/go-example/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "log" + "math" + "strconv" + + // Import the Serverledge Go SDK + "github.com/serverledge-faas/serverledge/serverledge" +) + +// Response defines the structure of the JSON output. This is just an example. It can be defined by the user as they wish. +type Response struct { + Message string `json:"message"` + Number int `json:"number,omitempty"` + IsPrime bool `json:"is_prime"` +} + +// isPrime is a helper function for this example. It can be any user-defined function for their logic. +func isPrime(n int) bool { + if n <= 1 { + return false + } + for i := 2; i <= int(math.Sqrt(float64(n))); i++ { + if n%i == 0 { + return false + } + } + return true +} + +// myHandler is the main entry point for your function logic. This is also defined by the user, but it has to use this +// signature to implement the interface of the Serverledge library. +// Arguments: +// - params: A map[string]interface{} containing the input parameters provided by the caller. +// +// Returns: +// - interface{}: The result payload (struct, map, string, etc.) to be returned as JSON. +// - error: An error object if the execution failed. +func myHandler(params map[string]interface{}) (interface{}, error) { + + name := "World" + if n, ok := params["name"].(string); ok { + name = n + } + + // If the function is invoked with logging enabled, (e.g.: serveledge-cli with "-o" flag or ReturnOutput=true in the request) + // these lines will be captured and returned in the "Output" field of the response. + log.Printf("Processing request for user: %s...", name) + + resp := Response{Message: "Hello " + name} + + if numStr, ok := params["num"].(string); ok { + if num, err := strconv.Atoi(numStr); err == nil { + resp.Number = num + resp.IsPrime = isPrime(num) + log.Printf("Checked number: %d (Prime: %v)", num, resp.IsPrime) + } + } + + return resp, nil +} + +func main() { + // Start the Serverledge runtime. This is a blocking function since it will start an HTTP server inside the + // container, waiting for the signal to execute the function. + serverledge.Start(myHandler) +} diff --git a/examples/java_build/target/hello-function-1.0.0-jar-with-dependencies.jar b/examples/java_build/target/hello-function-1.0.0-jar-with-dependencies.jar new file mode 100644 index 00000000..c470946a Binary files /dev/null and b/examples/java_build/target/hello-function-1.0.0-jar-with-dependencies.jar differ diff --git a/examples/jsonschema/Dockerfile b/examples/jsonschema/Dockerfile index 62bedf6c..ad368433 100644 --- a/examples/jsonschema/Dockerfile +++ b/examples/jsonschema/Dockerfile @@ -1,4 +1,4 @@ -#FROM grussorusso/serverledge-python310 +#FROM grussorusso/serverledge-python314 FROM python:3.10-alpine3.16 RUN pip3 install jsonschema diff --git a/examples/ml.py b/examples/ml.py new file mode 100644 index 00000000..54a4c2d8 --- /dev/null +++ b/examples/ml.py @@ -0,0 +1,74 @@ +import os + +# Suppress TF logs and oneDNN info +os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0" +os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + +import numpy as np +import pandas as pd +from scipy import stats +from sklearn.linear_model import LinearRegression +from sklearn.metrics import mean_squared_error, r2_score +import tensorflow as tf + + +def full_demo_with_metrics(epochs): + # This is a small test that uses all the available libraries of the python312ml runtime. + + # Generate random data (NumPy) + x = np.linspace(0, 10, 100) + y = 4 * x + 7 + np.random.randn(100) + + # Generate random test data + x_test = np.linspace(0, 10, 20) + y_test = 4 * x_test + 7 + np.random.randn(20) + + # Pandas DataFrame + df = pd.DataFrame({"x": x, "y": y}) + df_test = pd.DataFrame({"x": x_test, "y": y_test}) + + # use SciPy + corr, p_value = stats.pearsonr(df["x"], df["y"]) + + # use Scikit-learn + sklearn_model = LinearRegression() + sklearn_model.fit(df[["x"]], df["y"]) + sklearn_preds = sklearn_model.predict(df_test[["x"]]) + + sklearn_mse = mean_squared_error(df_test["y"], sklearn_preds) + + # Use tenseorflow (cpu only) with keras + tf_model = tf.keras.Sequential([ + tf.keras.Input(shape=(1,)), + tf.keras.layers.Dense(8, activation="relu"), + tf.keras.layers.Dense(1) + ]) + + tf_model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.01), + loss="mse") + + tf_model.fit(df[["x"]], df["y"], epochs=epochs, verbose=0) + tf_preds = tf_model.predict(df_test[["x"]], verbose=0).flatten() + + tf_mse = mean_squared_error(df_test["y"], tf_preds) + + return { + "correlation_train": corr, + "p_value_train": p_value, + "sklearn_sample_pred": sklearn_preds[:5].tolist(), + "sklearn_mse": sklearn_mse, + "tensorflow_sample_pred": tf_preds[:5].tolist(), + "tensorflow_mse": tf_mse, + } + +def handler(params, context): + epochs = 200 + if "n" in params: + epochs = int(params["n"]) + print("Will run with", epochs, "epochs") + + result = full_demo_with_metrics(epochs) + print("=== RESULTS ===") + for key, value in result.items(): + print(f"{key}: {value}") + return result diff --git a/go.mod b/go.mod index 38b86a07..0d732d9b 100644 --- a/go.mod +++ b/go.mod @@ -1,89 +1,127 @@ module github.com/serverledge-faas/serverledge -go 1.24 +go 1.24.0 + +replace github.com/distribution/reference => github.com/distribution/reference v0.5.0 require ( github.com/buger/jsonparser v1.1.1 - github.com/docker/docker v24.0.5+incompatible + github.com/docker/docker v25.0.2+incompatible + github.com/google/go-containerregistry v0.19.0 github.com/hexablock/vivaldi v0.0.0-20180727225019-07adad3f2b5f github.com/labstack/echo/v4 v4.6.1 github.com/labstack/gommon v0.3.0 github.com/lithammer/shortuuid v3.0.0+incompatible github.com/mikoim/go-loadavg v0.0.0-20150917074714-35ece5f6d547 - github.com/prometheus/client_golang v1.14.0 - github.com/prometheus/common v0.37.0 + github.com/prometheus/client_golang v1.20.5 + github.com/prometheus/common v0.62.0 github.com/spf13/cast v1.3.0 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.4.0 - go.etcd.io/etcd/client/v3 v3.5.1 - go.opentelemetry.io/otel v1.28.0 + github.com/stretchr/testify v1.11.1 + go.etcd.io/etcd/client/v3 v3.6.5 + go.etcd.io/etcd/server/v3 v3.6.5 + go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 - go.opentelemetry.io/otel/sdk v1.28.0 - go.opentelemetry.io/otel/trace v1.28.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 + go.uber.org/automaxprocs v1.6.0 golang.org/x/exp v0.0.0-20230905200255-921286631fa9 - golang.org/x/net v0.15.0 - google.golang.org/grpc v1.57.0 + golang.org/x/net v0.43.0 + gonum.org/v1/gonum v0.17.0 + google.golang.org/grpc v1.75.0 ) require ( - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/coreos/go-semver v0.3.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc4 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/rogpeppe/go-internal v1.8.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/afero v1.2.2 // indirect github.com/spf13/jwalterweatherman v1.0.0 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect - go.etcd.io/etcd/api/v3 v3.5.1 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.1 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.17.0 // indirect - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect - golang.org/x/tools v0.13.0 // indirect - google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/vbatts/tar-split v0.11.3 // indirect + github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect + go.etcd.io/bbolt v1.4.3 // indirect + go.etcd.io/etcd/api/v3 v3.6.5 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect + go.etcd.io/etcd/pkg/v3 v3.6.5 // indirect + go.etcd.io/raft/v3 v3.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.0 // indirect - + sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index f6af259a..34701d04 100644 --- a/go.sum +++ b/go.sum @@ -1,49 +1,13 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -51,67 +15,64 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= +github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= -github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v25.0.2+incompatible h1:/OaKeauroa10K4Nqavw4zlhcDq/WBcPMc5DbjOGgozY= +github.com/docker/docker v25.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -122,105 +83,69 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic= +github.com/google/go-containerregistry v0.19.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexablock/vivaldi v0.0.0-20180727225019-07adad3f2b5f h1:vgMos6ed6qNnsswN5hB+l/y/UcuUxyq7OL6pXu8XkaI= github.com/hexablock/vivaldi v0.0.0-20180727225019-07adad3f2b5f/go.mod h1:oicL+P8ej+PDUzurbqC9ln05I+/+LP0YHljnBmv1HZw= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M= github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= @@ -239,10 +164,10 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mikoim/go-loadavg v0.0.0-20150917074714-35ece5f6d547 h1:sKOBS3TQA6gIeu7xDDIJnH1cPmGAa3535gg2/cWrwC4= github.com/mikoim/go-loadavg v0.0.0-20150917074714-35ece5f6d547/go.mod h1:Gv1gEAo58s56eUbsb59IAFnEr6flyFg9lgryVQnKwhM= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -250,72 +175,58 @@ github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= -github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= @@ -333,407 +244,201 @@ github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd/api/v3 v3.5.1 h1:v28cktvBq+7vGyJXF8G+rWJmj+1XUmMtqcLnH8hDocM= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.1 h1:XIQcHCFSG53bJETYeRJtIxdLv2EWRGxcfzR8lSnTH4E= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v3 v3.5.1 h1:oImGuV5LGKjCqXdjkMHCyWa5OO1gYKCnC/1sgdfj1Uk= -go.etcd.io/etcd/client/v3 v3.5.1/go.mod h1:OnjH4M8OnAotwaB2l9bVgZzRFKru7/ZMoS46OtKyd3Q= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= +go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= +go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8= +go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= +go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= +go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.etcd.io/etcd/pkg/v3 v3.6.5 h1:byxWB4AqIKI4SBmquZUG1WGtvMfMaorXFoCcFbVeoxM= +go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= +go.etcd.io/etcd/server/v3 v3.6.5 h1:4RbUb1Bd4y1WkBHmuF+cZII83JNQMuNXzyjwigQ06y0= +go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= +go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= -google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= -google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= +sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/images/base-alpine/Dockerfile b/images/base-alpine/Dockerfile index 8642c20e..b4604c5f 100644 --- a/images/base-alpine/Dockerfile +++ b/images/base-alpine/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine AS build +FROM golang:1.24-alpine AS build WORKDIR /sedge diff --git a/images/go125/Dockerfile b/images/go125/Dockerfile new file mode 100644 index 00000000..18c97ff5 --- /dev/null +++ b/images/go125/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:latest + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +WORKDIR /app + +CMD ["/entrypoint.sh"] \ No newline at end of file diff --git a/images/go125/entrypoint.sh b/images/go125/entrypoint.sh new file mode 100644 index 00000000..91baf2e6 --- /dev/null +++ b/images/go125/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +for f in ./*.zip; do + [ -e "$f" ] || continue + unzip -o -- "$f" +done + + + +ARCH=$(uname -m) # architecture of node executing the function + +# select the correct binary +if [ "$ARCH" = "x86_64" ]; then + BINARY="./handler_amd64" +elif [ "$ARCH" = "aarch64" ]; then + BINARY="./handler_arm64" +else + echo "Error: Unsupported architecture '$ARCH', or wrong names for executables" + echo "Usage: zip file with both: handler_amd64 and handler_arm64 executable files" +fi + +if [ -f "$BINARY" ]; then + chmod +x "$BINARY" + exec "$BINARY" +else + echo "Error: Binary '$BINARY' not found in the uploaded package." + echo "Files available:" + ls -l + exit 1 +fi \ No newline at end of file diff --git a/images/java21/Dockerfile b/images/java21/Dockerfile new file mode 100644 index 00000000..88405d47 --- /dev/null +++ b/images/java21/Dockerfile @@ -0,0 +1,23 @@ +# builder to compile serverledge's executor +FROM eclipse-temurin:21-jdk-alpine AS builder + +WORKDIR /build + +# gson to handle requests/responses json +RUN wget -O gson.jar https://repo1.maven.org/maven2/com/google/code/gson/gson/2.13.2/gson-2.13.2.jar + +COPY Executor.java . + +RUN javac -cp .:gson.jar Executor.java + + +FROM eclipse-temurin:21-jre-alpine + +WORKDIR /app + +RUN mkdir -p /app/ + +COPY --from=builder /build/*.class /app/ +COPY --from=builder /build/gson.jar /app/ + +CMD ["java", "-cp", ".:gson.jar", "Executor"] \ No newline at end of file diff --git a/images/java21/Executor.java b/images/java21/Executor.java new file mode 100644 index 00000000..e2af7dc7 --- /dev/null +++ b/images/java21/Executor.java @@ -0,0 +1,164 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.io.*; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; + +public class Executor { + + private static final int PORT = 8080; + + // GSON to parse serverledge's jsons + private static final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .serializeNulls() + .create(); + + + private static volatile URLClassLoader sharedClassLoader = null; + private static final Object LOADER_LOCK = new Object(); + + public static void main(String[] args) throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0); + server.createContext("/", new InvokeHandler()); + server.setExecutor(Executors.newCachedThreadPool()); + server.start(); + System.out.println("Java Executor Server started on port " + PORT); + } + + static class InvokeHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + + if (!exchange.getRequestMethod().equalsIgnoreCase("POST") || !exchange.getRequestURI().getPath().contains("invoke")) { + exchange.sendResponseHeaders(404, -1); + return; + } + + InputStreamReader reader = new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8); + Map request = gson.fromJson(reader, new TypeToken>(){}.getType()); + + String handlerStr = (String) request.get("Handler"); + String handlerDir = (String) request.get("HandlerDir"); + Object params = request.get("Params"); + boolean returnOutput = request.containsKey("ReturnOutput") && (boolean) request.get("ReturnOutput"); + + String envContext = System.getenv("CONTEXT"); + Map context = envContext != null ? + gson.fromJson(envContext, new TypeToken>(){}.getType()) : new HashMap<>(); + + Map response = new HashMap<>(); + + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + ByteArrayOutputStream baosOut = new ByteArrayOutputStream(); + ByteArrayOutputStream baosErr = new ByteArrayOutputStream(); + + + try { + if (returnOutput) { + System.setOut(new PrintStream(baosOut)); + System.setErr(new PrintStream(baosErr)); + } + + if (sharedClassLoader == null) { + synchronized (LOADER_LOCK) { + if (sharedClassLoader == null) { + File dir = new File(handlerDir); + if (!dir.exists() || !dir.isDirectory()) { + throw new FileNotFoundException("Handler Directory not found: " + handlerDir); + } + // building the classpath + List urls = new ArrayList<>(); + urls.add(dir.toURI().toURL()); + + File[] jarFiles = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".jar")); + if (jarFiles != null) { + for (File jar : jarFiles) { + urls.add(jar.toURI().toURL()); + } + } + + sharedClassLoader = new URLClassLoader(urls.toArray(new URL[0]), Executor.class.getClassLoader()); + System.out.println("Loader created for: " + handlerDir); + } + } + } + + String className; + String methodName; + if (handlerStr.contains("::")) { + String[] parts = handlerStr.split("::"); + className = parts[0]; + methodName = parts[1]; + } else { + className = handlerStr; + methodName = "handler"; // default name if we're only provided with the class name + } + + Class usrClass = Class.forName(className, true, sharedClassLoader); + Object instance = usrClass.getDeclaredConstructor().newInstance(); + + Method method = null; + for (Method m : usrClass.getMethods()) { + if (m.getName().equals(methodName)) { + method = m; + break; + } + } + + if (method == null) throw new NoSuchMethodException("Method " + methodName + " not found in " + className); + + Object result; + if (method.getParameterCount() == 2) { + result = method.invoke(instance, params, context); + } else { + result = method.invoke(instance, params); + } + + response.put("Result", gson.toJson(result)); + response.put("Success", true); + + } catch (Exception e) { + e.printStackTrace(); + response.put("Success", false); + response.put("Error", e.toString()); + response.put("Result", ""); + } finally { + if (returnOutput) { + System.out.flush(); + System.err.flush(); + System.setOut(originalOut); + System.setErr(originalErr); + + String stdoutCaptured = baosOut.toString(StandardCharsets.UTF_8); + String stderrCaptured = baosErr.toString(StandardCharsets.UTF_8); + response.put("Output", stdoutCaptured + "\n" + stderrCaptured); + } else { + response.put("Output", ""); + } + } + + String jsonResponse = gson.toJson(response); + byte[] responseBytes = jsonResponse.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, responseBytes.length); + OutputStream os = exchange.getResponseBody(); + os.write(responseBytes); + os.close(); + } + } +} \ No newline at end of file diff --git a/images/python310/Dockerfile b/images/python310/Dockerfile deleted file mode 100644 index 88d169b1..00000000 --- a/images/python310/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM python:3.10-alpine3.14 - -WORKDIR / - -COPY images/python310/executor.py / -RUN mkdir -p /app - -CMD python3 /executor.py diff --git a/images/python312ml/Dockerfile b/images/python312ml/Dockerfile new file mode 100644 index 00000000..02593014 --- /dev/null +++ b/images/python312ml/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.12-slim + +ARG TARGETARCH + + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* # remove useless cache afterwards for a lighter image + +RUN pip install --no-cache-dir --upgrade pip + +# multi-arch image differences +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + pip install --no-cache-dir tensorflow-cpu; \ + else \ + pip install --no-cache-dir tensorflow; \ + fi + + +RUN pip install --no-cache-dir \ + numpy \ + pandas \ + scikit-learn \ + scipy + +COPY executor.py /executor.py +WORKDIR /app + +CMD ["python3", "/executor.py"] \ No newline at end of file diff --git a/images/python310/executor.py b/images/python312ml/executor.py similarity index 100% rename from images/python310/executor.py rename to images/python312ml/executor.py diff --git a/images/python314/Dockerfile b/images/python314/Dockerfile new file mode 100644 index 00000000..9f8aadca --- /dev/null +++ b/images/python314/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.14-alpine3.22 + +WORKDIR / + +COPY images/python314/executor.py / +RUN mkdir -p /app + +CMD ["python3", "/executor.py"] diff --git a/images/python314/executor.py b/images/python314/executor.py new file mode 100644 index 00000000..1f53b12a --- /dev/null +++ b/images/python314/executor.py @@ -0,0 +1,123 @@ +# Python 3 server example +from socketserver import ThreadingMixIn +from http.server import BaseHTTPRequestHandler, HTTPServer +import time +import os +import sys +import importlib +import json + +hostName = "0.0.0.0" +serverPort = 8080 + +#executed_modules = {} +added_dirs = {} + +from io import StringIO +import sys + +class CaptureOutput: + def __enter__(self): + self._stdout_output = '' + self._stderr_output = '' + + self._stdout = sys.stdout + sys.stdout = StringIO() + + self._stderr = sys.stderr + sys.stderr = StringIO() + + return self + + def __exit__(self, *args): + self._stdout_output = sys.stdout.getvalue() + sys.stdout = self._stdout + + self._stderr_output = sys.stderr.getvalue() + sys.stderr = self._stderr + + def get_stdout(self): + return self._stdout_output + + def get_stderr(self): + return self._stderr_output + + +class ThreadingSimpleServer(ThreadingMixIn, HTTPServer): + pass + +class Executor(BaseHTTPRequestHandler): + def do_POST(self): + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + request = json.loads(post_data.decode('utf-8')) + + if not "invoke" in self.path: + self.send_response(404) + self.end_headers() + return + + handler = request["Handler"] + handler_dir = request["HandlerDir"] + + try: + params = request["Params"] + except: + params = {} + + if "context" in os.environ: + context = json.loads(os.environ["CONTEXT"]) + else: + context = {} + + if not handler_dir in added_dirs: + sys.path.insert(1, handler_dir) + added_dirs[handler_dir] = True + + # Get module name + module,func_name = os.path.splitext(handler) + func_name = func_name[1:] # strip initial dot + loaded_mod = None + + return_output = bool(request["ReturnOutput"]) + + response = {} + + try: + # Call function + if loaded_mod is None: + loaded_mod = importlib.import_module(module) + + if not return_output: + result = getattr(loaded_mod, func_name)(params, context) + response["Output"] = "" + else: + with CaptureOutput() as capturer: + result = getattr(loaded_mod, func_name)(params, context) + response["Output"] = str(capturer.get_stdout()) + "\n" + str(capturer.get_stderr()) + + response["Result"] = json.dumps(result) + response["Success"] = True + except Exception as e: + print(e, file=sys.stderr) + response["Success"] = False + + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + self.wfile.write(bytes(json.dumps(response), "utf-8")) + + + +if __name__ == "__main__": + webServer = ThreadingSimpleServer((hostName, serverPort), Executor) + print("Server started http://%s:%s" % (hostName, serverPort)) + + try: + webServer.serve_forever() + except KeyboardInterrupt: + pass + + webServer.server_close() + print("Server stopped.") + diff --git a/internal/api/api.go b/internal/api/api.go index 9406ee50..6e52a82a 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -8,6 +8,7 @@ import ( "io" "log" "net/http" + "runtime" "sync" "time" @@ -84,12 +85,29 @@ func InvokeFunction(c echo.Context) error { defer span.End() } + setMetricsHeaders := func() { + // These headers will be used by the Load Balancer (if there is one), to get fresh updates on the free + // memory of each node after the execution of every function. + c.Response().Header().Set("Serverledge-Node-Name", node.LocalNode.Key) + freeMem := node.LocalResources.AvailableMemory() + c.Response().Header().Set("Serverledge-Free-Mem", fmt.Sprintf("%d", freeMem)) + c.Response().Header().Set("Serverledge-Timestamp", fmt.Sprintf("%d", time.Now().Unix())) + c.Response().Header().Set("Serverledge-Free-CPU", fmt.Sprintf("%f", node.LocalResources.AvailableCPUs())) + c.Response().Header().Set("Serverledge-Node-Arch", runtime.GOARCH) // used by the MAB to update correct arm + if reqID := c.Request().Header.Get("Serverledge-MAB-Request-ID"); reqID != "" { + c.Response().Header().Set("Serverledge-MAB-Request-ID", reqID) + } + + } + if r.Async { go scheduling.SubmitAsyncRequest(r) + setMetricsHeaders() return c.JSON(http.StatusOK, function.AsyncResponse{ReqId: r.Id()}) } executionReport, err := scheduling.SubmitRequest(r) + setMetricsHeaders() if errors.Is(err, node.OutOfResourcesErr) { return c.String(http.StatusTooManyRequests, "") @@ -158,6 +176,7 @@ func CreateOrUpdateFunction(c echo.Context) error { if !ok { return c.JSON(http.StatusNotFound, "Invalid runtime.") } + f.SupportedArchs = []string{container.X86, container.ARM} if f.MaxConcurrency > 1 && !runtime.ConcurrencySupported { log.Printf("Forcing max concurrency = 1 for runtime %s\n", f.Runtime) f.MaxConcurrency = 1 @@ -167,6 +186,39 @@ func CreateOrUpdateFunction(c echo.Context) error { log.Printf("Forcing max concurrency = 1 for runtime %s\n", f.Runtime) f.MaxConcurrency = 1 } + // If we have a custom runtime, then f.CustomImage will contain the image name + runtime, ok := container.CustomRuntimeToInfo[f.CustomImage] + if !ok { + //If I've never seen this custom runtime before, I'll add it to the map + archs, err := container.GetFactory().GetImageArchitectures(f.CustomImage) + if err != nil { + log.Printf("Failed to get image architectures for image %s: %v\n", f.CustomImage, err) + return c.String(http.StatusInternalServerError, "Failed to get image architectures") + } + + /* CustomRuntimeToInfo value "Image" is the empty string to save (just a little) memory. In fact, f.CustomImage + is the image name, and it's used as the key for the map. In the case of a custom runtime, the attribute f.Runtime + is simply "custom", and so it's not usable as a key for the map, and it's not worth keeping it in memory since it's + useless here (we already know is a custom runtime because the image is in CustomRuntimeToInfo). So what is actually + of interest here is the field Architectures, that will be used to keep track of where this function can be executed + (only x86 nodes, only ARM or both). I chose to use RuntimeInfo to keep it consistent with the map RuntimeToInfo + that we use for default runtimes (and images), without creating an overly specific data structure just for this case. + This way, future lookup for compatible architectures can be made the same way for both default and custom runtimes. + */ + runtime = container.RuntimeInfo{ + Image: "", + InvocationCmd: nil, + ConcurrencySupported: false, + Architectures: archs, + } + + container.CustomRuntimeToInfo[f.CustomImage] = runtime + } + + // Now we know that "runtime" contains info about the runtime of this function, both if it is a new one or one + // we already saw + f.SupportedArchs = runtime.Architectures // also inside the function to leverage etcd for offloading + } if f.MemoryMB < 1 { @@ -217,8 +269,21 @@ func DeleteFunction(c echo.Context) error { // GetServerStatus simple api to check the current server status func GetServerStatus(c echo.Context) error { - node.LocalResources.RLock() - defer node.LocalResources.RUnlock() + + // THE ORDER IN WHICH THESE DATA IS GATHERED AND THE USE OF THE RLock and RUnlock ARE MEANT TO PREVENT A + // DEADLOCK THAT WAS AFFECTING THIS PORTION OF THE CODE: + // As stated in the docs: RLock locks rw for reading. + // It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock. + // Since AcquireWarmContainer uses a full Lock() on LocalResources, asking for a recursive lock here leads to a deadlock in high + // concurrency scenarios. In fact, node.WarmStatus uses a RLock on Local resources, same as what used to do this function + // in the very first lines. So it would ask for the lock a first time from GetServerStatus, and again for in WarmStatus. + // If AcquireWarmContainer would ask for the Lock() after the first RLock here, but before the second RLock in WarmStatus + // no one was able to actually use LocalResources anymore, neither for writing nor for reading, resulting in a deadlock. + + // With this order of execution, we are sure that the lock is never taken recursively, avoiding said deadlock. + + warmStatus := node.WarmStatus() + coords := *registration.VivaldiClient.GetCoordinate() loadAvg, err := loadavg.Parse() loadAvgValues := []float64{-1.0, -1.0, -1.0} @@ -226,15 +291,23 @@ func GetServerStatus(c echo.Context) error { loadAvgValues = []float64{loadAvg.LoadAverage1, loadAvg.LoadAverage5, loadAvg.LoadAverage10} } + node.LocalResources.RLock() + totalMem := node.LocalResources.TotalMemory() + usedMem := node.LocalResources.UsedMemory() + totalCPU := node.LocalResources.TotalCPUs() + usedCPU := node.LocalResources.UsedCPUs() + node.LocalResources.RUnlock() + // TODO: use a different type response := registration.StatusInformation{ - AvailableWarmContainers: node.WarmStatus(), - TotalMemory: node.LocalResources.TotalMemory(), - UsedMemory: node.LocalResources.UsedMemory(), - TotalCPU: node.LocalResources.TotalCPUs(), - UsedCPU: node.LocalResources.UsedCPUs(), - Coordinates: *registration.VivaldiClient.GetCoordinate(), + AvailableWarmContainers: warmStatus, + TotalMemory: totalMem, + UsedMemory: usedMem, + TotalCPU: totalCPU, + UsedCPU: usedCPU, + Coordinates: coords, LoadAvg: loadAvgValues, + LastUpdateTime: time.Now().Unix(), } return c.JSON(http.StatusOK, response) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index b1bd53da..2f318494 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "strings" @@ -326,10 +327,19 @@ func create(cmd *cobra.Command, args []string) { var encoded string if runtime != "custom" { - srcContent, err := ReadSourcesAsTar(src) - if err != nil { - fmt.Printf("%v\n", err) - os.Exit(3) + var srcContent []byte + u, err := url.ParseRequestURI(src) + if err == nil && u.Scheme != "" && u.Host != "" { + // src is a URL + fmt.Println(u) + srcContent = []byte(src) + } else { + // src is a folder; a tar has to be created to be uploaded to etcd + srcContent, err = ReadSourcesAsTar(src) + if err != nil { + fmt.Printf("%v\n", err) + os.Exit(3) + } } encoded = base64.StdEncoding.EncodeToString(srcContent) } else { diff --git a/internal/config/keys.go b/internal/config/keys.go index f72a867a..77e47167 100644 --- a/internal/config/keys.go +++ b/internal/config/keys.go @@ -23,6 +23,9 @@ const POOL_CLEANUP_PERIOD = "janitor.interval" // container expiration time const CONTAINER_EXPIRATION_TIME = "container.expiration" +// offloading cache validity time for EdgeOnlypolicy +const OFFLOADING_CACHE_VALIDITY = "offloading.cache.validity" + // cache capacity const CACHE_SIZE = "cache.size" @@ -50,6 +53,27 @@ const REG_NEARBY_INTERVAL = "registry.nearby.interval" // long period for general monitoring inside the area const REG_MONITORING_INTERVAL = "registry.monitoring.interval" +// ArchitectureAwareLb: number of replicas in the HashRing for each physical node +const REPLICAS = "lb.replicas" + +// LoadBalancer refresh interval (in seconds) +const LB_REFRESH_INTERVAL = "lb.refresh_interval" + +// LoadBalancer working mode (i.e.: "MAB" or "RoundRobin") +const LB_MODE = "lb.mode" + +// Policy for the Multi Armed Bandit (MAB) (i.e.: "LinUcb" or "UCB1") +const MAB_POLICY = "mab.policy" + +// C value for the UCB1 policy +const MAB_UCB1_C = "mab.ucb1.c" + +// Aplha value for the LinUCB policy +const MAB_LINUCB_ALPHA = "mab.linucb.alpha" + +// Lambda value for the LinUCB policy, used for the memory penalty of reward +const MAB_LINUCB_LAMBDA = "mab.linucb.lambda" + // port for udp status listener const LISTEN_UDP_PORT = "registry.udp.port" @@ -69,6 +93,10 @@ const METRICS_RETRIEVER_INTERVAL = "metrics.retriever.interval" // Possible values: "qosaware", "default", "cloudonly" const SCHEDULING_POLICY = "scheduler.policy" +// If "edgeonly" policy is set, tries to execute the offloadable functions locally in the case +// where all neighbors won't support the function's runtime architecture +const SCHEDULING_FALLBACK_LOCAL = "scheduler.fallback.local" + // Capacity of the queue (possibly) used by the scheduler const SCHEDULER_QUEUE_CAPACITY = "scheduler.queue.capacity" diff --git a/internal/container/container.go b/internal/container/container.go index 513c0d0a..236748ed 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -5,12 +5,14 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/serverledge-faas/serverledge/internal/function" "io" "log" "net/http" + "net/url" "time" + "github.com/serverledge-faas/serverledge/internal/function" + "github.com/serverledge-faas/serverledge/internal/executor" ) @@ -56,17 +58,31 @@ func newContainer(image, codeTar string, opts *ContainerOptions) (*Container, er } if len(codeTar) > 0 { - decodedCode, errDecode := base64.StdEncoding.DecodeString(codeTar) - if errDecode != nil { - log.Printf("Failed code decode\n") - return nil, errDecode + var r io.Reader + // Decoding codeTar + decodedCode, _ := base64.StdEncoding.DecodeString(codeTar) + // Check if decoded src is a url + u, err := url.ParseRequestURI(string(decodedCode)) + if err == nil && u.Scheme != "" && u.Host != "" { + // codeTar is an URL; it has to be downloaded + resp, err := http.Get(string(decodedCode)) + if err != nil { + log.Printf("Failed to download code %s", decodedCode) + return nil, err + } + defer resp.Body.Close() + r = resp.Body + } else { + // assuming decodedCode is base64 encoded tar + r = bytes.NewReader(decodedCode) } - err = cf.CopyToContainer(contID, bytes.NewReader(decodedCode), "/app/") + err = cf.CopyToContainer(contID, r, "/app/") if err != nil { log.Printf("Failed code copy\n") return nil, err } } + // Starts the container after copying the code in it err = cf.Start(contID) if err != nil { diff --git a/internal/container/docker.go b/internal/container/docker.go index 42cdf9b8..7fdbc810 100644 --- a/internal/container/docker.go +++ b/internal/container/docker.go @@ -2,17 +2,22 @@ package container import ( "context" + "encoding/json" "fmt" "io" "log" "strings" "sync" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" + regName "github.com/google/go-containerregistry/pkg/name" + regRemote "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/serverledge-faas/serverledge/internal/config" + "github.com/serverledge-faas/serverledge/utils" // "github.com/docker/docker/pkg/stdcopy" ) @@ -167,3 +172,116 @@ func (cf *DockerFactory) GetLog(contID ContainerID) (string, error) { } return string(logs[:]), nil } + +// GetImageArchitectures retrieves the supported CPU architectures for a given container image. +// It first checks etcd for cached information. If not found, it queries the remote registry +// and then caches the result in etcd. As for now, architectures of interest are: x86_64 (amd64) and arm64. +func (cf *DockerFactory) GetImageArchitectures(imageName string) ([]string, error) { + etcdCli, err := utils.GetEtcdClient() + if err != nil { + return nil, fmt.Errorf("failed to get etcd client: %w", err) + } + + const imageArchEtcdPrefix = "/serverledge/image_architectures/" + etcdKey := imageArchEtcdPrefix + imageName + ctx, cancel := context.WithTimeout(cf.ctx, 5*time.Second) + defer cancel() + + // 1. Try to get architectures from etcd + resp, err := etcdCli.Get(ctx, etcdKey) + if err == nil && len(resp.Kvs) > 0 { + var architectures []string + if err := json.Unmarshal(resp.Kvs[0].Value, &architectures); err != nil { + log.Printf("Warning: failed to unmarshal architectures from etcd for image %s: %v. Re-fetching from registry.", imageName, err) + } else { + log.Printf("Architectures for image %s found in etcd: %v", imageName, architectures) + return architectures, nil + } + } else if err != nil { + // Log the error if it's not just "key not found" + if !strings.Contains(err.Error(), "key not found") { // etcd client returns error if key not found + log.Printf("Warning: failed to get architectures from etcd for image %s: %v. Re-fetching from registry.", imageName, err) + } else { + log.Printf("Architectures for image %s not found in etcd. Fetching from registry.", imageName) + } + } else { + log.Printf("Architectures for image %s not found in etcd. Fetching from registry.", imageName) + } + + // 2. If not found in etcd (or unmarshal failed), query the remote registry + ref, err := regName.ParseReference(imageName) + if err != nil { + return nil, fmt.Errorf("failed to parse image name %s: %w", imageName, err) + } + + desc, err := regRemote.Get(ref) + if err != nil { + return nil, fmt.Errorf("failed to get remote image descriptor for %s: %w", imageName, err) + } + + var supportedArchitectures []string // the list we will return + archSet := make(map[string]struct{}) // Use a set to avoid duplicates (it's just temporary, won't be returned) + + if desc.MediaType.IsIndex() { // Multi-platform images have an index manifest for the multiple architectures + idx, err := desc.ImageIndex() + if err != nil { + return nil, fmt.Errorf("failed to get image index for %s: %w", imageName, err) + } + manifests, err := idx.IndexManifest() + if err != nil { + return nil, fmt.Errorf("failed to get index manifest for %s: %w", imageName, err) + } + + for _, manifest := range manifests.Manifests { + if manifest.Platform != nil { + arch := manifest.Platform.Architecture + // We are interested in "amd64" (x86_64) and "arm64", for the moment we don't care if more architectures are supported + if arch == X86 || arch == ARM { + if _, found := archSet[arch]; !found { + supportedArchitectures = append(supportedArchitectures, arch) + archSet[arch] = struct{}{} // to avoid duplicates + } + } + } + } + } else if desc.MediaType.IsImage() { // Single-platform image + img, err := desc.Image() + if err != nil { + return nil, fmt.Errorf("failed to get image for %s: %w", imageName, err) + } + cfg, err := img.ConfigFile() + if err != nil { + return nil, fmt.Errorf("failed to get image config for %s: %w", imageName, err) + } + arch := cfg.Architecture + if arch == X86 || arch == ARM { + supportedArchitectures = append(supportedArchitectures, arch) + } + } else { + return nil, fmt.Errorf("unsupported media type for image %s: %s", imageName, desc.MediaType) + } + + if len(supportedArchitectures) == 0 { + return nil, fmt.Errorf("no architecture supported by Serverledge found for image %s", imageName) + + } + + // 3. Cache the result in etcd for future lookup (e.g.: another function executed in the same custom runtime, or can + // be used by another node if/when the function is offloaded) without having to hit the registry again. + // TODO maybe TTL for cache? + archBytes, err := json.Marshal(supportedArchitectures) + if err != nil { + log.Printf("Warning: failed to marshal architectures for image %s: %v. Not caching in etcd.", imageName, err) + // Return what we found, even if not cached, is an etcd problem, not a problem retrieving architectures + return supportedArchitectures, nil + } + + _, err = etcdCli.Put(ctx, etcdKey, string(archBytes)) + if err != nil { + log.Printf("Warning: failed to put architectures to etcd for image %s: %v", imageName, err) + } else { + log.Printf("Architectures for image %s cached in etcd: %v", imageName, supportedArchitectures) + } + + return supportedArchitectures, nil +} diff --git a/internal/container/docker_test.go b/internal/container/docker_test.go new file mode 100644 index 00000000..a75874bc --- /dev/null +++ b/internal/container/docker_test.go @@ -0,0 +1,156 @@ +package container + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "testing" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/server/v3/embed" + + "github.com/stretchr/testify/assert" +) + +var etcdClient *clientv3.Client + +// TestMain will be executed before all tests in this package. +// It is used to set up a global state, like an embedded etcd server. +func TestMain(m *testing.M) { + // Create a temporary directory for etcd data + tempDir, err := os.MkdirTemp("", "etcd-data-*") + if err != nil { + log.Fatalf("Failed to create temp dir for etcd: %v", err) + } + // Schedule cleanup for the end of the test run + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + log.Printf("Failed to remove temp dir for etcd: %v", err) + } + }(tempDir) + + // 1. Setup: Start an embedded etcd server + cfg := embed.NewConfig() + cfg.Dir = tempDir // Use the temporary directory + cfg.LogOutputs = []string{"/dev/null"} // Silence etcd output + etcd, err := embed.StartEtcd(cfg) + if err != nil { + log.Fatalf("Failed to start embedded etcd: %v", err) + } + defer etcd.Close() + + select { + case <-etcd.Server.ReadyNotify(): + // Silently continue + case <-time.After(60 * time.Second): + etcd.Server.Stop() // trigger a shutdown + log.Fatal("Embedded etcd server took too long to start") + } + + // Create a client connected to the embedded server + etcdClient, err = clientv3.New(clientv3.Config{ + Endpoints: []string{etcd.Clients[0].Addr().String()}, + DialTimeout: 5 * time.Second, + }) + if err != nil { + log.Fatalf("Failed to create etcd client: %v", err) + } + defer func(etcdClient *clientv3.Client) { + err := etcdClient.Close() + if err != nil { + log.Printf("Failed to close etcd client: %v", err) + } + }(etcdClient) + + InitDockerContainerFactory() + + // 2. Run all tests + code := m.Run() + + // 3. Teardown: The deferred calls to Close() and RemoveAll() will handle this. + os.Exit(code) +} + +// TestGetImageArchitectures requires a running Docker daemon. +func TestGetImageArchitectures(t *testing.T) { + // Pull a known multi-arch image for testing + _ = pullImage("busybox:latest") // So we can test both with images available locally and later we'll use + // images not yet pulled, to test different scenarios + + t.Run("multi-arch image", func(t *testing.T) { + archs, err := cf.GetImageArchitectures("busybox:latest") + assert.NoError(t, err) + assert.Contains(t, archs, "amd64") + assert.Contains(t, archs, "arm64") + assert.Equal(t, len(archs), 2, "Busybox should have exactly 2 architectures") + }) + + t.Run("single-arch image", func(t *testing.T) { + image := "amd64/hello-world" + //Let's make sure is not available locally + cmd := exec.Command("docker", "image", "rm", image) + _ = cmd.Run() // error here means it was already non-available locally, so it's ok + + archs, err := cf.GetImageArchitectures(image) + assert.NoError(t, err) + assert.Equal(t, []string{"amd64"}, archs) + assert.Equal(t, len(archs), 1, "amd64/hello-world should have exactly 1 architecture") + }) + + t.Run("non-existent image", func(t *testing.T) { + _, err := cf.GetImageArchitectures("non-existent-image-serverledge-test:latest") + assert.Error(t, err) + }) +} + +func TestCache(t *testing.T) { + image := "memcached:latest" + + // This is the key that the GetImageArchitectures function will use in etcd. + // Make sure it matches the implementation in docker.go + cacheKey := fmt.Sprintf("/serverledge/image_architectures/%s", image) + + // 1. First call: Cache Miss + // Before the call, the key must not exist in etcd. + resp, err := etcdClient.Get(context.Background(), cacheKey) + assert.NoError(t, err) + assert.Zero(t, resp.Count, "Key should not exist before first call") + + start := time.Now() + archs1, err := cf.GetImageArchitectures(image) + noCacheTime := time.Since(start) + assert.NoError(t, err) + assert.NotEmpty(t, archs1) + + // After the call, the key must exist. + resp, err = etcdClient.Get(context.Background(), cacheKey) + assert.NoError(t, err) + assert.Equal(t, int64(1), resp.Count, "Key should exist after first call") + + // 2. Second call: Cache Hit + // For this test, we rely on the fact that the key exists and the function returns the correct value. + start = time.Now() + archs2, err := cf.GetImageArchitectures(image) + cacheTime := time.Since(start) + assert.NoError(t, err) + assert.Equal(t, archs1, archs2, "Architectures from cache should match original ones") + log.Printf("noCacheTime: %v, cacheTime: %v", noCacheTime, cacheTime) +} + +// pullImage is a helper function to ensure the image is available locally. +func pullImage(image string) error { + cmd := exec.Command("docker", "pull", image) + + // Run esegue il comando e attende la fine. + err := cmd.Run() + if err != nil { + log.Printf("Failed pulling image: %v\n", err) + return err + } + return nil + +} diff --git a/internal/container/factory.go b/internal/container/factory.go index 75156b72..7032c0be 100644 --- a/internal/container/factory.go +++ b/internal/container/factory.go @@ -15,6 +15,7 @@ type Factory interface { GetIPAddress(ContainerID) (string, error) GetMemoryMB(id ContainerID) (int64, error) GetLog(id ContainerID) (string, error) + GetImageArchitectures(string) ([]string, error) } // ContainerOptions contains options for container creation. @@ -35,3 +36,7 @@ type Container struct { // cf is the container factory for the node var cf Factory + +func GetFactory() Factory { + return cf +} diff --git a/internal/container/runtimes.go b/internal/container/runtimes.go index 1fa83710..0827d6d2 100644 --- a/internal/container/runtimes.go +++ b/internal/container/runtimes.go @@ -5,13 +5,23 @@ type RuntimeInfo struct { Image string InvocationCmd []string ConcurrencySupported bool + Architectures []string } const CUSTOM_RUNTIME = "custom" +const X86 = "amd64" +const ARM = "arm64" var refreshedImages = map[string]bool{} var RuntimeToInfo = map[string]RuntimeInfo{ - "python310": {"grussorusso/serverledge-python310", []string{"python", "/entrypoint.py"}, true}, - "nodejs17ng": {"grussorusso/serverledge-nodejs17ng", []string{}, false}, + "python314": {"fmuschera/serverledge-python314", []string{"python", "/entrypoint.py"}, true, []string{X86, ARM}}, + "nodejs17ng": {"fmuschera/serverledge-nodejs17ng", []string{}, false, []string{X86, ARM}}, + "java21": {"fmuschera/serverledge-java21", []string{}, false, []string{X86, ARM}}, + "go125": {"fmuschera/serverledge-go125", []string{"/entrypoint.sh"}, true, []string{X86, ARM}}, + "python312ml": {"fmuschera/serverledge-python312ml", []string{"python", "/entrypoint.py"}, true, []string{X86, ARM}}, } + +// CustomRuntimeToInfo Map to keep track of architectures compatible with each custom runtime image associated with a function registered +// by users +var CustomRuntimeToInfo = map[string]RuntimeInfo{} diff --git a/internal/function/function.go b/internal/function/function.go index 3eab997d..dba63b1e 100644 --- a/internal/function/function.go +++ b/internal/function/function.go @@ -3,6 +3,7 @@ package function import ( "encoding/json" "fmt" + "slices" "time" @@ -15,13 +16,14 @@ import ( // Function describes a serverless function. type Function struct { Name string - Runtime string // example: python310 - MemoryMB int64 // MB - CPUDemand float64 // 1.0 -> 1 core - MaxConcurrency int16 // intra-container maximum concurrency - Handler string // example: "module.function_name" - TarFunctionCode string // input is .tar - CustomImage string // used if custom runtime is chosen + Runtime string // example: python314 + MemoryMB int64 // MB + CPUDemand float64 // 1.0 -> 1 core + MaxConcurrency int16 // intra-container maximum concurrency + Handler string // example: "module.function_name" + TarFunctionCode string // input is .tar + CustomImage string // used if custom runtime is chosen + SupportedArchs []string // list of supported architectures by the runtime Signature *Signature } @@ -33,6 +35,10 @@ func getEtcdKey(funcName string) string { return fmt.Sprintf("/function/%s", funcName) } +func (f *Function) SupportsArch(arch string) bool { + return slices.Contains(f.SupportedArchs, arch) +} + // GetFunction retrieves a Function given its name. If it doesn't exist, returns false func GetFunction(name string) (*Function, bool) { diff --git a/internal/lb/architectureAwareLb.go b/internal/lb/architectureAwareLb.go new file mode 100644 index 00000000..e32647b4 --- /dev/null +++ b/internal/lb/architectureAwareLb.go @@ -0,0 +1,258 @@ +package lb + +import ( + "fmt" + "log" + "math/rand" + "strings" + "sync" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/lithammer/shortuuid" + "github.com/serverledge-faas/serverledge/internal/config" + "github.com/serverledge-faas/serverledge/internal/container" + "github.com/serverledge-faas/serverledge/internal/function" + "github.com/serverledge-faas/serverledge/internal/mab" +) + +type ArchitectureAwareBalancer struct { + mu sync.Mutex + + // instead of classic lists we will use hashRings (see hashRing.go) to implement a consistent hashing technique + armRing *HashRing + x86Ring *HashRing + + mode string + rrIndices map[string]int +} + +// NewArchitectureAwareBalancer Constructor +func NewArchitectureAwareBalancer(targets []*middleware.ProxyTarget) *ArchitectureAwareBalancer { + + // REPLICAS is the number of times each physical node will appear in the hash ring. This is done to improve how + // virtual nodes (i.e.: replicas of each physical node) are distributed over the ring, to reduce variation. + REPLICAS := config.GetInt(config.REPLICAS, 128) + log.Printf("Running ArchitectureAwareLB with %d replicas per node in the hash rings\n", REPLICAS) + + b := &ArchitectureAwareBalancer{ + armRing: NewHashRing(REPLICAS), + x86Ring: NewHashRing(REPLICAS), + rrIndices: make(map[string]int), + } + + b.mode = config.GetString(config.LB_MODE, RR) + log.Printf("LB mode set to %s\n", b.mode) + + // to stay consistent with the old RoundRobinLoadBalancer, we'll still a single target list, that will contain all nodes, + // both ARM and x86. We will now sort them into the respective hashRings. + for _, t := range targets { + arch := t.Meta["arch"] + if arch == container.ARM || arch == container.X86 { + b.AddTarget(t) + } else { + log.Printf("Unknown architecture for node %s\n", t.Name) + } + } + + return b +} + +// Next Used by Echo Proxy middleware to select the next target dynamically +func (b *ArchitectureAwareBalancer) Next(c echo.Context) *middleware.ProxyTarget { + b.mu.Lock() + defer b.mu.Unlock() + + funcName := extractFunctionName(c) // get function's name from request's URL + fun, ok := function.GetFunction(funcName) // we use this to leverage cache before asking etcd + if !ok { + log.Printf("Dropping request for unknown fun '%s'\n", funcName) + return nil + } + + reqID := shortuuid.New() + c.Request().Header.Set("Serverledge-MAB-Request-ID", reqID) + + targetArch := "" + + var ctx *mab.Context = nil + if b.mode == MAB { + ctx = b.calculateSystemContext() // memory snapshot for the MAB LinUCB + mab.GlobalContextStorage.Store(reqID, ctx) // Cache it for LinUCB update + } + + if len(fun.SupportedArchs) == 1 { // If only one architecture is supported skip the MAB and just use that + targetArch = fun.SupportedArchs[0] + } else if b.mode == MAB { // if both are supported, then use the MAB to select it + bandit := mab.GlobalBanditManager.GetBandit(funcName) + targetArch = bandit.SelectArm(ctx) + } else if b.mode == RR { // RoundRobin + targetArch = b.selectArchitectureRR(funcName) // here the load balancer decides what architecture to use for this function + } else { // Random + targetArch = b.selectArchitectureRandom() // random load balancer for testing purposes + } + + // once we selected an architecture, we'll use consistent hashing to select what node to use + // The Get function will cycle through the hashRing to find a suitable node. If none is find we try to check if in + // the other ring there is a suitable node for the function, to maximize chances of execution. + var candidate *middleware.ProxyTarget + if targetArch == container.ARM { // Prioritize ARM if selected + candidate = b.armRing.Get(fun) + if candidate == nil && fun.SupportsArch(container.X86) { // If no ARM node, try x86 if supported + candidate = b.x86Ring.Get(fun) + } + } else { // Prioritize x86 if selected + candidate = b.x86Ring.Get(fun) + if candidate == nil && fun.SupportsArch(container.ARM) { // If no x86 node, try ARM if supported + candidate = b.armRing.Get(fun) + } + } + if candidate != nil { + freeMemoryMB := NodeMetrics.GetFreeMemory(candidate.Name) - fun.MemoryMB + // Remove the memory that this function will use (this will then be updated again once the function is executed) + freeCpu := NodeMetrics.metrics[candidate.Name].FreeCPU - fun.CPUDemand + NodeMetrics.Update(candidate.Name, freeMemoryMB, 0, time.Now().Unix(), freeCpu) + + } + return candidate +} + +// extractFunctionName retrieves the function's name by parsing the request's URL. +func extractFunctionName(c echo.Context) string { + path := c.Request().URL.Path + + const prefix = "/invoke/" + if !strings.HasPrefix(path, prefix) { + return "" // not an invocation + } + + return path[len(prefix):] +} + +// Deprecated +// This should only be used for tests or as a baseline in experiments. +// selectArchitecture checks the function's runtime to see what architecture it can support. Then it checks if any +// available node of the corresponding architecture is available. If the runtime supports both architecture, then we +// have a tie-break and select a node from the chosen list (arm or x86). +func (b *ArchitectureAwareBalancer) selectArchitecture(fun *function.Function) (string, error) { + supportsArm := fun.SupportsArch(container.ARM) + supportsX86 := fun.SupportsArch(container.X86) + + if supportsArm && supportsX86 { + cacheValidity := 30 * time.Second // may be fine-tuned + cacheEntry, ok := ArchitectureCacheLB.cache[fun.Name] + + // If we have a valid cache entry, we try to use it + expiry := time.Unix(cacheEntry.Timestamp, 0).Add(cacheValidity) + if ok && time.Now().Before(expiry) { + // Check if the cached architecture has available nodes + if (cacheEntry.Arch == container.ARM && b.armRing.Size() > 0) || + (cacheEntry.Arch == container.X86 && b.x86Ring.Size() > 0) { + // If the cached architecture is still valid and has available nodes, use it + cacheEntry.Timestamp = time.Now().Unix() // Update timestamp + ArchitectureCacheLB.cache[fun.Name] = cacheEntry + return cacheEntry.Arch, nil + } + + } + + // Tie-breaking: if both architectures are supported, prefer ARM if available (less energy consumption), otherwise x86. + // This will also be the fallback if the cached decision is not usable. + var chosenArch string + if b.armRing.Size() > 0 { + chosenArch = container.ARM + } else if b.x86Ring.Size() > 0 { + chosenArch = container.X86 + } else { + return "", fmt.Errorf("no available nodes for either ARM or x86") + } + + // Update cache + newCacheEntry := ArchitectureCacheEntry{ + Arch: chosenArch, + Timestamp: time.Now().Unix(), + } + ArchitectureCacheLB.cache[fun.Name] = newCacheEntry + + return chosenArch, nil + } + + if supportsArm { + if b.armRing.Size() > 0 { + return container.ARM, nil + } + return "", fmt.Errorf("no ARM nodes available") + } + + if supportsX86 { + if b.x86Ring.Size() > 0 { + return container.X86, nil + } + return "", fmt.Errorf("no x86 nodes available") + } + + return "", fmt.Errorf("function does not support any available architecture") +} + +// selectArchitectureRR selects the architecture using a Round Robin policy. +func (b *ArchitectureAwareBalancer) selectArchitectureRR(funcName string) string { + + // This is just a function to use as a baseline for the LB. It should actually implement checks over the rings dimension. + // i.e.: it cannot select ARM/X86 "blindly", it should check if we have at least one node for that architecture. + archs := []string{container.ARM, container.X86} + index := b.rrIndices[funcName] + selected := archs[index] + b.rrIndices[funcName] = (index + 1) % len(archs) + return selected +} + +// AddTarget Echo requires this method for dynamic load-balancing. It simply inserts a new node in the respective ring. +func (b *ArchitectureAwareBalancer) AddTarget(t *middleware.ProxyTarget) bool { + b.mu.Lock() + defer b.mu.Unlock() + + nodeInfo := GetSingleTargetInfo(t) + // Every time we add a node, we set the information about its available memory + if nodeInfo != nil { + totalMemoryMb := nodeInfo.TotalMemory + freeMemoryMB := totalMemoryMb - nodeInfo.UsedMemory + freeCpu := nodeInfo.TotalCPU - nodeInfo.UsedCPU + // Update will update the freeMemory only if the information in nodeInfo is fresher than what we + // already have in the NodeMetrics cache. + NodeMetrics.Update(t.Name, freeMemoryMB, totalMemoryMb, nodeInfo.LastUpdateTime, freeCpu) + } + // Decide if target belongs to ARM or x86 + if t.Meta["arch"] == container.ARM { + b.armRing.Add(t) + } else { + b.x86Ring.Add(t) + } + + return true +} + +// RemoveTarget Echo requires this method to remove a target by name +func (b *ArchitectureAwareBalancer) RemoveTarget(name string) bool { + b.mu.Lock() + defer b.mu.Unlock() + + delete(NodeMetrics.metrics, name) // this is no longer needed + + if b.armRing.RemoveByName(name) { + return true + } + if b.x86Ring.RemoveByName(name) { + return true + } + return false + +} + +func (b *ArchitectureAwareBalancer) selectArchitectureRandom() string { + archs := []string{container.ARM, container.X86} + // Seed the random number generator if needed, though global rand is usually fine for simple LB + // rand.Seed(time.Now().UnixNano()) + index := rand.Intn(len(archs)) + return archs[index] +} diff --git a/internal/lb/architectureAwareLb_test.go b/internal/lb/architectureAwareLb_test.go new file mode 100644 index 00000000..6fde0c65 --- /dev/null +++ b/internal/lb/architectureAwareLb_test.go @@ -0,0 +1,289 @@ +package lb + +import ( + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/serverledge-faas/serverledge/internal/cache" + "github.com/serverledge-faas/serverledge/internal/container" + "github.com/serverledge-faas/serverledge/internal/function" + "github.com/serverledge-faas/serverledge/internal/mab" + "github.com/stretchr/testify/assert" +) + +func newTarget(name string, arch string) *middleware.ProxyTarget { + return &middleware.ProxyTarget{ + Name: name, + URL: &url.URL{Host: name}, + Meta: echo.Map{"arch": arch}, + } +} + +func TestNewArchitectureAwareBalancer(t *testing.T) { + targets := []*middleware.ProxyTarget{ + newTarget("arm1", container.ARM), + newTarget("x86_1", container.X86), + newTarget("arm2", container.ARM), + } + + b := getNewLb(targets) + + assert.Equal(t, 2, b.armRing.Size()) + assert.Equal(t, 1, b.x86Ring.Size()) +} + +func TestAddTarget(t *testing.T) { + b := getNewLb([]*middleware.ProxyTarget{}) + b.AddTarget(newTarget("arm1", container.ARM)) + b.AddTarget(newTarget("x86_1", container.X86)) + + assert.Equal(t, 1, b.armRing.Size()) + assert.Equal(t, 1, b.x86Ring.Size()) + + b.AddTarget(newTarget("x86_2", container.X86)) + b.AddTarget(newTarget("arm2", container.ARM)) + + assert.Equal(t, 2, b.armRing.Size()) + assert.Equal(t, 2, b.x86Ring.Size()) +} + +func TestRemoveTarget(t *testing.T) { + targets := []*middleware.ProxyTarget{ + newTarget("arm1", container.ARM), + newTarget("x86_1", container.X86), + } + b := getNewLb(targets) + + assert.Equal(t, 1, b.armRing.Size()) + assert.Equal(t, 1, b.x86Ring.Size()) + + assert.True(t, b.RemoveTarget("arm1")) + assert.False(t, b.RemoveTarget("unknown")) + assert.Equal(t, 0, b.armRing.Size()) + assert.Equal(t, 1, b.x86Ring.Size()) +} + +func TestSelectArchitecture(t *testing.T) { + targets := []*middleware.ProxyTarget{ + newTarget("arm1", container.ARM), + newTarget("x86_1", container.X86), + } + b := getNewLb(targets) + + // Test case 1: Function supports both ARM and x86 + funBoth := &function.Function{Name: "bothArchs", SupportedArchs: []string{container.X86, container.ARM}} + arch, err := b.selectArchitecture(funBoth) + assert.NoError(t, err) + + // Test case 2: Function supports only ARM + funArm := &function.Function{Name: "onlyArm", SupportedArchs: []string{container.ARM}} + arch, err = b.selectArchitecture(funArm) + assert.NoError(t, err) + assert.Equal(t, container.ARM, arch) + + // Test case 3: Function supports only x86 + funX86 := &function.Function{Name: "onlyX86", SupportedArchs: []string{container.X86}} + arch, err = b.selectArchitecture(funX86) + assert.NoError(t, err) + assert.Equal(t, container.X86, arch) + + // Test case 4: No available nodes for supported architecture + b.RemoveTarget("arm1") + b.RemoveTarget("x86_1") + _, err = b.selectArchitecture(funBoth) + assert.Error(t, err) +} + +func TestConsistentNodeMapping(t *testing.T) { + targets := []*middleware.ProxyTarget{ + newTarget("arm1", container.ARM), + newTarget("x86_1", container.X86), + newTarget("arm2", container.ARM), + newTarget("x86_2", container.X86), + } + b := getNewLb(targets) + + fun := &function.Function{ + Name: "testFunc", + SupportedArchs: []string{container.ARM, container.X86}, + } + + // Add the function to the cache to avoid etcd dependency + cache.GetCacheInstance().Set(fun.Name, fun, 30*time.Second) + defer cache.GetCacheInstance().Delete(fun.Name) + + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/invoke/testFunc", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // First call + firstTarget := b.Next(c) + assert.NotNil(t, firstTarget) + + // Subsequent calls should return the same target + for i := 0; i < 10; i++ { + nextTarget := b.Next(c) + assert.NotNil(t, nextTarget) + } +} + +func TestGetNodeFromRing(t *testing.T) { + targets := []*middleware.ProxyTarget{ + newTarget("arm1", container.ARM), + newTarget("arm2", container.ARM), + } + b := getNewLb(targets) + nodeMap := map[string]struct{}{} + nodeMap["arm2"] = struct{}{} + mockMemChecker := &MockMemChecker{nodesWithEnoughMemory: nodeMap} + b.armRing.memChecker = mockMemChecker + + fun := &function.Function{ + Name: "testGetNodeFromRingFunc", + SupportedArchs: []string{container.ARM}, + } + + // Add the function to the cache to avoid etcd dependency + cache.GetCacheInstance().Set(fun.Name, fun, 30*time.Second) + defer cache.GetCacheInstance().Delete(fun.Name) + + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/invoke/testGetNodeFromRingFunc", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // First call + firstTarget := b.Next(c) + assert.NotNil(t, firstTarget) + assert.Equal(t, firstTarget.Name, "arm2") + + // Subsequent calls should return the same target + for i := 0; i < 10; i++ { + nextTarget := b.Next(c) + assert.Equal(t, firstTarget, nextTarget) + } + + delete(mockMemChecker.nodesWithEnoughMemory, "arm2") + nextTarget := b.Next(c) + assert.Nil(t, nextTarget) + + mockMemChecker.nodesWithEnoughMemory["arm1"] = struct{}{} + nextTarget = b.Next(c) + assert.NotNil(t, nextTarget) + assert.Equal(t, nextTarget.Name, "arm1") + +} + +func TestGetArchFallback(t *testing.T) { + targets := []*middleware.ProxyTarget{ + newTarget("arm1", container.ARM), + newTarget("arm2", container.ARM), + newTarget("x86_1", container.X86), + newTarget("x86_2", container.X86), + } + b := getNewLb(targets) + nodeMap := map[string]struct{}{} + nodeMap["x86_2"] = struct{}{} + mockMemChecker := &MockMemChecker{nodesWithEnoughMemory: nodeMap} + b.armRing.memChecker = mockMemChecker + b.x86Ring.memChecker = mockMemChecker + + fun := &function.Function{ + Name: "testGetArchFallbackFunc", + SupportedArchs: []string{container.ARM, container.X86}, + } + + // Add the function to the cache to avoid etcd dependency + cache.GetCacheInstance().Set(fun.Name, fun, 30*time.Second) + defer cache.GetCacheInstance().Delete(fun.Name) + + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/invoke/testGetArchFallbackFunc", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // First call + firstTarget := b.Next(c) + assert.NotNil(t, firstTarget) + assert.Equal(t, firstTarget.Name, "x86_2") + + // Subsequent calls should return the same target + for i := 0; i < 10; i++ { + nextTarget := b.Next(c) + assert.Equal(t, firstTarget, nextTarget) + } + + delete(mockMemChecker.nodesWithEnoughMemory, "x86_2") + nextTarget := b.Next(c) + assert.Nil(t, nextTarget) + + mockMemChecker.nodesWithEnoughMemory["arm2"] = struct{}{} + nextTarget = b.Next(c) + assert.NotNil(t, nextTarget) + assert.Equal(t, nextTarget.Name, "arm2") + +} + +func TestGetArchFallbackNotPossible(t *testing.T) { + targets := []*middleware.ProxyTarget{ + newTarget("arm1", container.ARM), + newTarget("arm2", container.ARM), + newTarget("x86_1", container.X86), + } + b := getNewLb(targets) + nodeMap := map[string]struct{}{} + nodeMap["x86_1"] = struct{}{} // has enough memory but should still not be used because incompatible architecture + mockMemChecker := &MockMemChecker{nodesWithEnoughMemory: nodeMap} + b.armRing.memChecker = mockMemChecker + b.x86Ring.memChecker = mockMemChecker + + fun := &function.Function{ + Name: "testGetArchFallbackNotPossibleFunc", + SupportedArchs: []string{container.ARM}, + } + + // Add the function to the cache to avoid etcd dependency + cache.GetCacheInstance().Set(fun.Name, fun, 30*time.Second) + defer cache.GetCacheInstance().Delete(fun.Name) + + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/invoke/testGetArchFallbackNotPossibleFunc", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // First call + firstTarget := b.Next(c) + assert.Nil(t, firstTarget) + + // Subsequent calls should return the same target + for i := 0; i < 10; i++ { + nextTarget := b.Next(c) + assert.Equal(t, firstTarget, nextTarget) + } + + mockMemChecker.nodesWithEnoughMemory["arm1"] = struct{}{} + nextTarget := b.Next(c) + assert.NotNil(t, nextTarget) + assert.Equal(t, nextTarget.Name, "arm1") + +} + +type MockMemChecker struct { + nodesWithEnoughMemory map[string]struct{} +} + +func (m *MockMemChecker) HasEnoughMemory(target *middleware.ProxyTarget, fun *function.Function) bool { + _, ret := m.nodesWithEnoughMemory[target.Name] + return ret +} + +func getNewLb(targets []*middleware.ProxyTarget) *ArchitectureAwareBalancer { + mab.InitBanditManager() + return NewArchitectureAwareBalancer(targets) +} diff --git a/internal/lb/hashRing.go b/internal/lb/hashRing.go new file mode 100644 index 00000000..37983744 --- /dev/null +++ b/internal/lb/hashRing.go @@ -0,0 +1,148 @@ +package lb + +import ( + "fmt" + "hash/fnv" + "log" + "sort" + + "github.com/labstack/echo/v4/middleware" + "github.com/serverledge-faas/serverledge/internal/function" +) + +type HashRing struct { + replicas int + ring []uint32 // actual ring with hash of nodes + targets map[uint32]*middleware.ProxyTarget // mapping hash(es) <-> node. Each node will have #replicas entries in the ring + // function to check if the node selected has enough memory to execute the function + targetList []*middleware.ProxyTarget // list of target. Cached instead of iterating on targets every time. + memChecker MemoryChecker // implemented this way to make the code testable by mocking this struct/function. + +} + +func NewHashRing(replicas int) *HashRing { + + return &HashRing{ + replicas: replicas, + ring: make([]uint32, 0), + targets: make(map[uint32]*middleware.ProxyTarget), + targetList: make([]*middleware.ProxyTarget, 0), + memChecker: &DefaultMemoryChecker{}, + } +} + +func (r *HashRing) Add(t *middleware.ProxyTarget) { + // put replicas in the ring. To do so we'll hash the node's name + an incrementing number + for i := 0; i < r.replicas; i++ { + key := fmt.Sprintf("%s#%d", t.Name, i) + h := hash(key) + r.ring = append(r.ring, h) + r.targets[h] = t + } + sort.Slice(r.ring, func(i, j int) bool { return r.ring[i] < r.ring[j] }) // sort the ring by hash + r.targetList = append(r.targetList, t) +} + +func (r *HashRing) Get(fun *function.Function) *middleware.ProxyTarget { + if len(r.ring) == 0 { + return nil + } + + h := hash(fun.Name) + // we'll return the node whose hash is the next in the ring, starting from the hash of the function's name + idx := sort.Search(len(r.ring), func(i int) bool { return r.ring[i] >= h }) + if idx == len(r.ring) { + idx = 0 + } + candidate := r.targets[r.ring[idx]] // here we use the map to get the node corresponding to the hash + + if r.memChecker.HasEnoughMemory(candidate, fun) { + return candidate + } + + // If the node found by consistent hashing doesn't have enough memory, we'll try to find another node to execute the function + // by navigating the ring, so that the lookup order is consistent between different executions (if the nodes' pool doesn't change + // of course). + startingIdx := idx // idx is still set to the candidate index in the ring + idx = (idx + 1) % len(r.ring) // next node in the ring + + // since there are multiple replicas of every physical node, I'll keep track of nodes already considered as candiadates + // to skip them and make the lookup faster. + // E.g.: if the first candidate was node "Node-A", found by its replica "Node-A#1", there is no point in trying to see + // if "Node-A" has enough memory once I find it through its replica "Node-A#2", for example, that I may find while + // traversing the ring. + seen := make(map[string]struct{}) + seen[candidate.Name] = struct{}{} // I already tried it + + for idx != startingIdx { // as long as I have not completed a full circle + candidate = r.targets[r.ring[idx]] // new candidate: idx is the replica's index. candidate is the corresponding physical node + _, alreadySeen := seen[candidate.Name] // I check if it's in the map (meaning I already tried it) + + if !alreadySeen && r.memChecker.HasEnoughMemory(candidate, fun) { + return candidate + } else { + seen[candidate.Name] = struct{}{} // it's a map, it doesn't really matter if alreadySeen was true or not, there are no duplicates + idx = (idx + 1) % len(r.ring) + } + } + + return nil // no suitable node found + +} + +func (r *HashRing) RemoveByName(name string) bool { + removed := false + newRing := make([]uint32, 0) + + // We'll delete all entries for this node from the targets' map, and generate a new ring without them. + + for _, h := range r.ring { + if r.targets[h].Name == name { + delete(r.targets, h) + removed = true + } else { + newRing = append(newRing, h) + } + } + + if removed { + r.ring = newRing + sort.Slice(r.ring, func(i, j int) bool { return r.ring[i] < r.ring[j] }) + r.removeFromTargetList(name) + + } + + return removed +} + +func (r *HashRing) removeFromTargetList(targetName string) { + newList := r.targetList[:0] + for _, t := range r.targetList { + if t.Name != targetName { + newList = append(newList, t) + } + } + r.targetList = newList +} + +// Size returns the number of UNIQUE nodes in the ring, not the numbers of total nodes (which is = nUniqueNodes * Replicas) +func (r *HashRing) Size() int { + + return len(r.targetList) + +} + +func (r *HashRing) GetAllTargets() []*middleware.ProxyTarget { + return r.targetList +} + +// hash function uses the FNV-1a function. It has good distribution and is fast to compute. It's not cryptographically safe, +// but should be good enough for our purposes (consistent-hashing). +func hash(s string) uint32 { + h := fnv.New32a() + _, err := h.Write([]byte(s)) + if err != nil { + log.Printf("error hashing %s: %v", s, err) + } + return h.Sum32() +} diff --git a/internal/lb/lb.go b/internal/lb/lb.go index 08de382f..abbc70f5 100644 --- a/internal/lb/lb.go +++ b/internal/lb/lb.go @@ -1,24 +1,30 @@ package lb import ( + "bytes" + "encoding/json" "errors" "fmt" + "io" "log" "net/http" "net/url" "os" + "strconv" "time" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/serverledge-faas/serverledge/internal/config" + "github.com/serverledge-faas/serverledge/internal/mab" "github.com/serverledge-faas/serverledge/internal/registration" ) var currentTargets []*middleware.ProxyTarget func newBalancer(targets []*middleware.ProxyTarget) middleware.ProxyBalancer { - return middleware.NewRoundRobinBalancer(targets) + // old Load Balancer: return middleware.NewRoundRobinBalancer(targets) + return NewArchitectureAwareBalancer(targets) } func StartReverseProxy(e *echo.Echo, region string) { @@ -31,8 +37,81 @@ func StartReverseProxy(e *echo.Echo, region string) { log.Printf("Initializing with %d targets.\n", len(targets)) balancer := newBalancer(targets) currentTargets = targets - e.Use(middleware.Proxy(balancer)) + // Custom ProxyConfig to process custom headers and update available memory of each targets after they + // executed a function. + // These headers are set after the execution of the function on the target node, so the free memory already + // includes the memory freed by the function, once it's executed. + proxyConfig := middleware.ProxyConfig{ + Balancer: balancer, + + // We use ModifyResponse to process these headers + ModifyResponse: func(res *http.Response) error { + + // Here we read the body, and then we restore it. This is done to avoid a potential race condition: + // the main thread of this LB will send the body back to the original user/caller, since it's acting as a + // reverse proxy. In the meantime UpdateBandit will try to read the same stream of data to get the + // stats about the execution, to update the bandit. Even if the goroutine tries to restore the response after + // reading it, chaches are that that won't happen quick eough (and it will not be a reliable solution anyway), + // so the solution here is the following: + // 1. We read the response body + // 2. We extract the fileds needed to update the bandit, and we pass those to UpdateBandit + // 3. We restore the response body so that it can be read by the ProxyBalancer in order to send it back to the user. + // All of this is because we don't want to call UpdateBandit synchronously, since that would add more + // latency for the final user. + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return err + } + _ = res.Body.Close() + + res.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // reset the body, as discussed earlier + + // Extract the necessary data for UpdateBandit + nodeArch := res.Header.Get("Serverledge-Node-Arch") + reqPath := res.Request.URL.Path + reqID := res.Request.Header.Get("Serverledge-MAB-Request-ID") + + go func(data []byte, path string, arch string, reqID string) { + err := mab.UpdateBandit(data, path, arch, reqID) + if err != nil { + log.Printf("Failed to update bandit: %v", err) + } + }(bodyBytes, reqPath, nodeArch, reqID) + + nodeName := res.Header.Get("Serverledge-Node-Name") + freeMemStr := res.Header.Get("Serverledge-Free-Mem") + freeCpuStr := res.Header.Get("Serverledge-Free-CPU") + timestampStr := res.Header.Get("Serverledge-Timestamp") + + if nodeName != "" && freeMemStr != "" { + freeMem, err := strconv.ParseInt(freeMemStr, 10, 64) + freeCpu, err2 := strconv.ParseFloat(freeCpuStr, 64) + timestamp, err3 := strconv.ParseInt(timestampStr, 10, 64) + + if err == nil && err2 == nil && err3 == nil { + NodeMetrics.Update(nodeName, freeMem, 0, timestamp, freeCpu) + + log.Printf("[LB-Update] Node %s reported %d MB free", nodeName, freeMem) + } else { + log.Printf("ERROR updating node stats: MEM error: %v, CPU error: %v", err, err2) + } + } + + // Remove the no-longer-needed headers + res.Header.Del("Serverledge-Node-Name") + res.Header.Del("Serverledge-Free-Mem") + res.Header.Del("Serverledge-Free-CPU") + res.Header.Del("Serverledge-MAB-Request-ID") + + // for experiments: we need to know which node ran the function + res.Header.Set("Serverledge-Node-Arch", nodeArch) + + return nil + }, + } + + e.Use(middleware.ProxyWithConfig(proxyConfig)) go updateTargets(balancer, region) portNumber := config.GetInt(config.API_PORT, 1323) @@ -55,7 +134,8 @@ func getTargets(region string) ([]*middleware.ProxyTarget, error) { if err != nil { return nil, err } - targets = append(targets, &middleware.ProxyTarget{Name: target.Key, URL: parsedUrl}) + archMap := echo.Map{"arch": target.Arch} + targets = append(targets, &middleware.ProxyTarget{Name: target.Key, URL: parsedUrl, Meta: archMap}) } log.Printf("Found %d targets\n", len(targets)) @@ -64,12 +144,15 @@ func getTargets(region string) ([]*middleware.ProxyTarget, error) { } func updateTargets(balancer middleware.ProxyBalancer, region string) { + var sleepTime = config.GetInt(config.LB_REFRESH_INTERVAL, 30) for { - time.Sleep(30 * time.Second) // TODO: configure + time.Sleep(time.Duration(sleepTime) * time.Second) + log.Printf("[LB]: Periodic targets update\n") targets, err := getTargets(region) if err != nil { log.Printf("Cannot update targets: %v\n", err) + continue // otherwise we update everything with a nil target array, removing all targets from the LB list! } toKeep := make([]bool, len(currentTargets)) @@ -82,6 +165,15 @@ func updateTargets(balancer middleware.ProxyBalancer, region string) { if curr.Name == t.Name { toKeep[i] = true toAdd = false + // Since we're keeping this node, we'll update it's free memory info. + nodeInfo := GetSingleTargetInfo(curr) + if nodeInfo != nil { + totalMemory := nodeInfo.TotalMemory + freeMemoryMB := totalMemory - nodeInfo.UsedMemory + freeCpu := nodeInfo.TotalCPU - nodeInfo.UsedCPU + NodeMetrics.Update(curr.Name, freeMemoryMB, totalMemory, nodeInfo.LastUpdateTime, freeCpu) + } + } } if toAdd { @@ -95,6 +187,15 @@ func updateTargets(balancer middleware.ProxyBalancer, region string) { if !toKeep[i] { log.Printf("Removing %s\n", curr.Name) toRemove = append(toRemove, curr.Name) + } else { + // If we keep this node, then we'll update its info about free memory + nodeInfo := GetSingleTargetInfo(curr) + if nodeInfo != nil { + totalMemory := nodeInfo.TotalMemory + freeMemoryMB := totalMemory - nodeInfo.UsedMemory + freeCpu := nodeInfo.TotalCPU - nodeInfo.UsedCPU + NodeMetrics.Update(curr.Name, freeMemoryMB, totalMemory, nodeInfo.LastUpdateTime, freeCpu) + } } } for _, curr := range toRemove { @@ -104,3 +205,31 @@ func updateTargets(balancer middleware.ProxyBalancer, region string) { currentTargets = targets } } + +func GetSingleTargetInfo(target *middleware.ProxyTarget) *registration.StatusInformation { + + // Build the status URL and GET request to the target (not using UDP best-effort implementation) + targetUrl := fmt.Sprintf("%s/status", target.URL) + + resp, err := http.Get(targetUrl) + if err != nil { + log.Printf("Failed to get status from target %s: %v", target.Name, err) + return nil + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + log.Printf("Failed to close response body: %v", err) + } + }(resp.Body) + + // Decode the JSON response to obtain the StatusInfo data structure + var statusInfo registration.StatusInformation + err = json.NewDecoder(resp.Body).Decode(&statusInfo) + if err != nil { + log.Printf("Failed to decode status response from target %s: %v", target.Name, err) + return nil + } + + return &statusInfo +} diff --git a/internal/lb/types.go b/internal/lb/types.go new file mode 100644 index 00000000..8b195783 --- /dev/null +++ b/internal/lb/types.go @@ -0,0 +1,146 @@ +package lb + +import ( + "log" + "sync" + + "github.com/labstack/echo/v4/middleware" + "github.com/serverledge-faas/serverledge/internal/container" + "github.com/serverledge-faas/serverledge/internal/function" + "github.com/serverledge-faas/serverledge/internal/mab" +) + +const MAB = "MAB" +const RR = "RoundRobin" + +var AllMemoryAvailable = int64(10_000_000) // A high value to symbolize all memory is free + +// MemoryChecker is the function that checks if the node selected has enough memory to execute the function. +// it is an interface, and it's put in HashRing to make unit-tests possible by mocking it +type MemoryChecker interface { + HasEnoughMemory(target *middleware.ProxyTarget, fun *function.Function) bool +} + +type DefaultMemoryChecker struct{} + +func (m *DefaultMemoryChecker) HasEnoughMemory(candidate *middleware.ProxyTarget, fun *function.Function) bool { + freeMemoryMB := NodeMetrics.GetFreeMemory(candidate.Name) + freeCpu := NodeMetrics.metrics[candidate.Name].FreeCPU + log.Printf("Candidate has: %d MB free memory. Function needs: %d MB", freeMemoryMB, fun.MemoryMB) + return freeMemoryMB >= fun.MemoryMB && freeCpu >= fun.CPUDemand + +} + +var NodeMetrics = &NodeMetricCache{ + metrics: make(map[string]NodeMetric), +} + +// ArchitectureCacheLB This map will cache the architecture chosen previously to try and maximize the use of warm containers of targets +var ArchitectureCacheLB = &ArchitectureCache{ + cache: make(map[string]ArchitectureCacheEntry), +} + +type NodeMetric struct { + TotalMemoryMB int64 + FreeMemoryMB int64 + LastUpdate int64 + TotalCPU float64 + FreeCPU float64 +} + +type NodeMetricCache struct { + mu sync.RWMutex + metrics map[string]NodeMetric +} + +type ArchitectureCacheEntry struct { + Arch string + Timestamp int64 +} + +type ArchitectureCache struct { + mu sync.RWMutex + cache map[string]ArchitectureCacheEntry +} + +// Update info about memory of a specific node. If totalMemMB = 0, then we keep the previous value. +func (c *NodeMetricCache) Update(nodeName string, freeMemMB int64, totalMemMB int64, updateTime int64, freeCpu float64) { + c.mu.Lock() + defer c.mu.Unlock() + curr, ok := c.metrics[nodeName] + if ok && (updateTime < curr.LastUpdate) { + return // if this branch is taken, we do not update. The info we already have is "fresher" than the one we received now + } + + if totalMemMB == 0 && ok { + totalMemMB = curr.TotalMemoryMB + } + + c.metrics[nodeName] = NodeMetric{ + TotalMemoryMB: totalMemMB, + FreeMemoryMB: freeMemMB, + LastUpdate: updateTime, + FreeCPU: freeCpu, + } +} + +func (c *NodeMetricCache) GetFreeMemory(nodeName string) int64 { + c.mu.RLock() + defer c.mu.RUnlock() + + val, ok := c.metrics[nodeName] + if !ok { + // This can probably only happen in the first phases of execution of Serverledge; we have the list of neighbors + // but we haven't completed yet the first polling round for status information. This means the full system has + // already started and there should be enough free memory. + // Plus, these are cloud nodes, so the total memory should be sufficient to execute any function. + return AllMemoryAvailable + } + + return val.FreeMemoryMB +} + +// Calculate the avg utilization of each architecture +func (b *ArchitectureAwareBalancer) calculateSystemContext() *mab.Context { + + archs := []string{container.ARM, container.X86} + usageMap := make(map[string]float64) + + for _, arch := range archs { + var totalFree int64 = 0 + var totalCap int64 = 0 + + var nodes []*middleware.ProxyTarget + if arch == container.ARM { + nodes = b.armRing.GetAllTargets() + } else { + nodes = b.x86Ring.GetAllTargets() + } + + if len(nodes) == 0 { + usageMap[arch] = 100.0 // If no node let's assume it's full. This architecture will not be used anyway. + continue + } + + for _, node := range nodes { + + NodeMetrics.mu.RLock() // Assumo tu abbia reso pubblico il mutex o aggiunto un metodo Getter completo + metric, ok := NodeMetrics.metrics[node.Name] + NodeMetrics.mu.RUnlock() + + if ok && metric.TotalMemoryMB > 0 { + totalFree += metric.FreeMemoryMB + totalCap += metric.TotalMemoryMB + } else { + log.Printf("[AALB] Node %s has a TotalMemoryMB attribute = 0. It wasn't initialized correctly?\n", node.Name) + panic(0) // it should never happen + } + } + + used := float64(totalCap - totalFree) + usageMap[arch] = used / float64(totalCap) // % utilization (0.0 - 1.0) fort this specific architecture + + } + + return &mab.Context{ArchMemUsage: usageMap} +} diff --git a/internal/mab/bandit_linucb.go b/internal/mab/bandit_linucb.go new file mode 100644 index 00000000..d417410e --- /dev/null +++ b/internal/mab/bandit_linucb.go @@ -0,0 +1,187 @@ +package mab + +import ( + "log" + "math" + "sync" + + "github.com/serverledge-faas/serverledge/internal/config" + "gonum.org/v1/gonum/mat" // for matrix operations +) + +// LinUCBDisjointPolicy implements the LinUCB algorithm with disjoint linear models. +// Reference: Li et al., "A Contextual-Bandit Approach to Personalized News Article Recommendation", Algorithm 1. +type LinUCBDisjointPolicy struct { + Alpha float64 // Exploration parameter + + // Maps each arm to its features (A, b) + Arms map[string]*LinUCBArmState + mu sync.RWMutex + + // Dimension of the feature vector (d) + // Bias (1) + MemoryFeature (1) = 2 + Dim int +} + +// LinUCBArmState holds the matrix A and vector b for a specific arm. +// A represents the design matrix (d x d) +// b represents the reward mapping (d x 1) +type LinUCBArmState struct { + A *mat.Dense + b *mat.VecDense +} + +// NewLinUCBDisjointPolicy creates a new instance of the policy. +func NewLinUCBDisjointPolicy(alpha float64) *LinUCBDisjointPolicy { + return &LinUCBDisjointPolicy{ + Alpha: alpha, + Arms: make(map[string]*LinUCBArmState), + Dim: 2, // Currently: Bias + Memory Usage + } +} + +// InitArm initializes the matrices for a new architecture. +func (p *LinUCBDisjointPolicy) InitArm(arm string) { + p.mu.Lock() + defer p.mu.Unlock() + + if _, exists := p.Arms[arm]; exists { + return + } + + // Initialize A as Identity Matrix (d x d) as per the paper + A := mat.NewDense(p.Dim, p.Dim, nil) + for i := 0; i < p.Dim; i++ { + A.Set(i, i, 1.0) + } + + // Initialize b as Zero Vector (d) + b := mat.NewVecDense(p.Dim, nil) + + p.Arms[arm] = &LinUCBArmState{ + A: A, + b: b, + } +} + +// SelectArm calculates the UCB score for each arm using the context and returns the best one. +func (p *LinUCBDisjointPolicy) SelectArm(ctx *Context) string { + p.mu.Lock() + defer p.mu.Unlock() + + bestArm := "" + bestScore := -math.MaxFloat64 + + for arm, state := range p.Arms { + // Construct Feature Vector x_t for this arm + // We need the memory usage specifically for THIS arm from the context + memUsage, ok := ctx.ArchMemUsage[arm] + if !ok { + // If no info let's assume it is a new arm and therefore it has no functions running. + memUsage = 0.0 + } + + x := p.computeFeatures(memUsage) + + // Compute Inverse of A + var AInv mat.Dense + err := AInv.Inverse(state.A) + if err != nil { + log.Printf("[LinUCB] Error inverting matrix for arm %s: %v", arm, err) + panic(1) // it should never happen + } + + // Compute Theta_hat = A_inv * b (line 8 of Algorithm 1 in the aforementioned paper + var theta mat.VecDense + theta.MulVec(&AInv, state.b) + + // Compute x^T * theta + expectedReward := mat.Dot(x, &theta) + + // Compute alpha * sqrt(x^T * A_inv * x) + var tempVec mat.VecDense + tempVec.MulVec(&AInv, x) + variance := mat.Dot(x, &tempVec) + confidence := p.Alpha * math.Sqrt(variance) + + // Final UCB Score for this arm + score := expectedReward + confidence + + log.Printf("[LinUCB] Arm: %s, Mem: %.2f, Exp: %.4f, Conf: %.4f, Score: %.4f", arm, memUsage, expectedReward, confidence, score) + + if score > bestScore { + bestScore = score + bestArm = arm + } + } + + if bestArm == "" { + log.Println("[LinUCB] Warning: No arms available/configured. Panic.") + panic(2) // should never happen if initialized correctly + } + + return bestArm +} + +// UpdateReward updates A and b for the chosen arm. Context is necessary to keep track of the memory usage AT THE MOMENT +// the decision was taken. So it has to be a "snapshot" of memory at that given time. +func (p *LinUCBDisjointPolicy) UpdateReward(arm string, ctx *Context, isWarmStart bool, durationMs float64) { + p.mu.Lock() + defer p.mu.Unlock() + + if !isWarmStart { + return // likely an outlier, skip update + } + + state, ok := p.Arms[arm] + if !ok { + log.Printf("[LinUCB] Warning: Trying to update unknown arm %s", arm) + panic(3) // should never happen if correctly used + } + + // Reconstruct the feature vector x_t used at decision time + memUsage := 0.0 + if ctx != nil { + memUsage = ctx.ArchMemUsage[arm] + } else { + log.Printf("[LinUCB] Warning: Context is nil for arm %s", arm) + panic(4) // should never happen + } + lambda := config.GetFloat(config.MAB_LINUCB_LAMBDA, 0.0) + // reward as negative Log to handle better very slow and very fast exec times plus eventual memory penalty + reward := -math.Log(durationMs) - (lambda * memPenalty(memUsage)) + x := p.computeFeatures(memUsage) + + // Update A: A = A + x * x^T + var outerProduct mat.Dense + outerProduct.Outer(1.0, x, x) + state.A.Add(state.A, &outerProduct) + + // Update b: b = b + reward * x + var scaledX mat.VecDense + scaledX.ScaleVec(reward, x) + state.b.AddVec(state.b, &scaledX) +} + +func memPenalty(memUsage float64) float64 { + // Grows from 0 at 0.75 utilization to 1 at 1.0 utilization + penalty := (memUsage - 0.75) / 0.25 // (memUsage - 0.75) / (1 - 0.75) + return max(0.0, penalty) +} + +// computeFeatures transforms raw context data into the feature vector [1, sigma(u)]. +func (p *LinUCBDisjointPolicy) computeFeatures(memUsage float64) *mat.VecDense { + // Bias term + bias := 1.0 + + // Non-linear penalty (sigma) as suggested: 1 / (1 - u + epsilon) + // epsilon prevents division by zero if usage is 100% + epsilon := 0.01 + sigma := 1.0 / (1.0 - memUsage + epsilon) + + return mat.NewVecDense(p.Dim, []float64{bias, sigma}) +} + +func (p *LinUCBDisjointPolicy) GetType() BanditType { + return LinUCB +} diff --git a/internal/mab/bandit_ucb1.go b/internal/mab/bandit_ucb1.go new file mode 100644 index 00000000..067a73bf --- /dev/null +++ b/internal/mab/bandit_ucb1.go @@ -0,0 +1,119 @@ +package mab + +import ( + "log" + "math" + "sync" +) + +// NOTE: Since nomenclature may be confusing: 'ARM' is the architecture, 'arm' is the arm of the Multi-Armed Bandit (MAB) + +// ArmStats maintains information about a single arm dedicated to a single function +type ArmStats struct { + Count int64 // UCB needs to know hom many times we chose that arm/architecture + SumRewards float64 // Sum of rewards + AvgReward float64 // Avg Reward (Q value in the formula) +} + +// UCB1Bandit is the bandit that handles decision for ONE function +type UCB1Bandit struct { + TotalCounts int64 // number of total executions (t) + Arms map[string]*ArmStats // Map "amd64" -> Stats, "arm64" -> Stats for each arm + mu sync.RWMutex // Mutex per thread-safety + c float64 // Exploration parameter C (usually sqrt(2) ~= 1.41, but can be tuned) + // Higher values lead to more exploration. Lower values lead to more exploitation. +} + +// InitArm adds a new arm to the bandit +func (b *UCB1Bandit) InitArm(arm string) { + b.mu.Lock() + defer b.mu.Unlock() + if _, exists := b.Arms[arm]; !exists { + b.Arms[arm] = &ArmStats{Count: 0, SumRewards: 0, AvgReward: 0} + } +} + +// SelectArm implements UCB-1 formulas +// Returns the suggested architecture to use ("amd64" o "arm64"). +// ctx *Ctx is necessary even if not used to be compliant with the interface. +func (b *UCB1Bandit) SelectArm(ctx *Context) string { + b.mu.Lock() + defer b.mu.Unlock() + + ctx = nil // not used, favor garbage collection + minSampleCount := int64(1) + currentMinSample := int64(math.MaxInt64) + leastTriedArch := "" + + // 1. If an arm hasn't tried at least minSampleCount times, it has to be tried. If both haven't reached this threshold, + // we choose the one with fewer tries. + for arch, stats := range b.Arms { + if stats.Count < minSampleCount && stats.Count < currentMinSample { + currentMinSample = stats.Count + leastTriedArch = arch + } + } + if leastTriedArch != "" { + log.Printf("Using (forced) least tried arch: %s", leastTriedArch) + b.TotalCounts++ + b.Arms[leastTriedArch].Count = b.Arms[leastTriedArch].Count + 1 + return leastTriedArch + } + + bestScore := -math.MaxFloat64 // Initialize with a very low score + bestArch := "" + + // 2. Calculate UCB1 score for each architecture + for arch, stats := range b.Arms { + // Formula: Q(a) + c * sqrt( ln(t) / N(a) ) where Q(a) is AvgReward, t is TotalCounts, N(a) is stats.Count + explorationBonus := b.c * math.Sqrt(math.Log(float64(b.TotalCounts))/float64(stats.Count)) + score := stats.AvgReward + explorationBonus + log.Printf("Score for %s: %f\n", arch, score) + + if score > bestScore { + bestScore = score // Update best score + bestArch = arch + } + } + if bestArch == "" { + log.Printf("Couldn't select any ARM. Panic\n") + panic(1) + } + b.TotalCounts++ + b.Arms[bestArch].Count = b.Arms[bestArch].Count + 1 + + return bestArch +} + +// UpdateReward updates bandit stats after execution. For now reward is 1.0 / executionTime (not considering setup time). +// It may be fine-tuned in the future. ctx *Context is need even if it's unused to be compliant with the interface. +func (b *UCB1Bandit) UpdateReward(arch string, ctx *Context, isWarmStart bool, durationMs float64) { + b.mu.Lock() + defer b.mu.Unlock() + + if ctx != nil { + ctx = nil // is not used here but will still be set in MAB mode, help garbage collection to get rid of this + } + if !isWarmStart { // redact this run if it was not a warm start. Likely to be an outlier. + b.TotalCounts-- + b.Arms[arch].Count-- + return + } + + if _, ok := b.Arms[arch]; !ok { + return // Should not happen + } + + stats := b.Arms[arch] + + // reward calculation + reward := -math.Log(durationMs) // reward as negative Log to handle better very slow and very fast exec times + + // Update average reward + stats.SumRewards += reward + stats.AvgReward = stats.SumRewards / float64(stats.Count) +} + +func (b *UCB1Bandit) GetType() BanditType { + return UCB1 +} diff --git a/internal/mab/mab_utils.go b/internal/mab/mab_utils.go new file mode 100644 index 00000000..ba95b8b8 --- /dev/null +++ b/internal/mab/mab_utils.go @@ -0,0 +1,46 @@ +package mab + +import ( + "encoding/json" + "fmt" + "log" + "strings" + + "github.com/serverledge-faas/serverledge/internal/function" +) + +func UpdateBandit(body []byte, reqPath string, arch string, reqID string) error { // Read the body + // Parse the body to a Response object + var response function.Response + if err := json.Unmarshal(body, &response); err != nil { + return fmt.Errorf("failed to unmarshal response body: %v", err) + } + // get the url of the request, to extract the function name, so that we can update the related MAB. + pathParts := strings.Split(reqPath, "/") + if len(pathParts) < 3 || pathParts[len(pathParts)-2] != "invoke" { + return fmt.Errorf("could not extract function name from URL: %s", reqPath) + } + functionName := pathParts[len(pathParts)-1] + + bandit := GlobalBanditManager.GetBandit(functionName) + ctx := GlobalContextStorage.RetrieveAndDelete(reqID) + + if arch == "" { + log.Println("Serverledge-Node-Arch header missing") + panic(0) // should never happen + } + + // Calculate the reward for this execution + if response.ExecutionReport.Duration <= 0 { + log.Printf("invalid execution duration: %f", response.ExecutionReport.Duration) + panic(1) // should never happen + } + + // Reward = 1 / Duration (we don't consider cold start delay, since we want to focus on architectures' performance) + durationMs := response.ExecutionReport.Duration * 1000.0 // s to ms + + // finally update the reward for the bandit. This is thread safe since internally it has a mutex + bandit.UpdateReward(arch, ctx, response.IsWarmStart, durationMs) + + return nil +} diff --git a/internal/mab/manager.go b/internal/mab/manager.go new file mode 100644 index 00000000..3746f87b --- /dev/null +++ b/internal/mab/manager.go @@ -0,0 +1,60 @@ +package mab + +import ( + "log" + "sync" + + "github.com/serverledge-faas/serverledge/internal/config" +) + +// BanditManager contains all the existing bandits (one for each known function) +type BanditManager struct { + bandits map[string]Policy // Nota: ora è map[string]Policy, non *UCB1Bandit + mu sync.RWMutex +} + +var GlobalBanditManager *BanditManager + +// InitBanditManager sets up the bandit manager +func InitBanditManager() { + GlobalBanditManager = &BanditManager{ + bandits: make(map[string]Policy), + } +} + +// GetBandit returns (or creates) the bandit for a given function +func (bm *BanditManager) GetBandit(functionName string) Policy { + bm.mu.Lock() + defer bm.mu.Unlock() + + if _, exists := bm.bandits[functionName]; !exists { + // Read policy from config + policyType := config.GetString(config.MAB_POLICY, "UCB1") + log.Printf("BanditManager GetBandit: policy type: %s\n", policyType) + + var newBandit Policy + + switch policyType { + case "LinUCB": + // Alpha param could also be in config + alpha := config.GetFloat(config.MAB_LINUCB_ALPHA, 0.1) + newBandit = NewLinUCBDisjointPolicy(alpha) + log.Printf("Initialized LinUCB bandit for %s", functionName) + default: + // Default to UCB1 (Legacy) + newBandit = &UCB1Bandit{ + TotalCounts: 0, + Arms: map[string]*ArmStats{}, + c: config.GetFloat(config.MAB_UCB1_C, 0.8), + } + log.Printf("Initialized UCB1 bandit for %s", functionName) + } + + // Ideally, this list is not hardcoded, but comes from the LB or Discovery or the config + newBandit.InitArm("amd64") + newBandit.InitArm("arm64") + + bm.bandits[functionName] = newBandit + } + return bm.bandits[functionName] +} diff --git a/internal/mab/policy.go b/internal/mab/policy.go new file mode 100644 index 00000000..88ac0e8d --- /dev/null +++ b/internal/mab/policy.go @@ -0,0 +1,32 @@ +package mab + +type BanditType string + +const ( + UCB1 BanditType = "UCB1" + LinUCB BanditType = "LinUCB" +) + +// Context carries the state of the system at the time of decision. +// Currently, it holds memory usage, but can be extended (i.e.: we could also add % of cpu load) +// It is used only by contextual MABs, obviously. The UCB1 doesn't need this since it works without context. +type Context struct { + // "archName" -> memory usage % + ArchMemUsage map[string]float64 +} + +// Policy is the interface that any Bandit algorithm must implement. +type Policy interface { + // SelectArm chooses the best arm based on the policy logic and optional context. + SelectArm(ctx *Context) string + + // UpdateReward updates the internal model of the policy based on the feedback. + // It requires the context that was present when the decision was made (if the MAB has a context). + UpdateReward(arm string, ctx *Context, isWarmStart bool, durationMs float64) + + // InitArm initializes a new arm before it is used. So it will be easier to implement more than 2 arms for new architectures. + InitArm(arm string) + + // GetType returns the type of the bandit policy. + GetType() BanditType +} diff --git a/internal/mab/storage.go b/internal/mab/storage.go new file mode 100644 index 00000000..f894b4ed --- /dev/null +++ b/internal/mab/storage.go @@ -0,0 +1,27 @@ +package mab + +import ( + "sync" +) + +// ContextStorage is a temporary cache to store the context (state) +// used for a specific request ID until the request completes. +// It is needed to update context LinUCB +type ContextStorage struct { + // sync.Map is safe for concurrent use. So we use it since the LB can handle multiple request simultaneously. + data sync.Map +} + +var GlobalContextStorage = &ContextStorage{} + +func (s *ContextStorage) Store(reqID string, ctx *Context) { + s.data.Store(reqID, ctx) +} + +func (s *ContextStorage) RetrieveAndDelete(reqID string) *Context { + val, ok := s.data.LoadAndDelete(reqID) + if !ok { + return nil + } + return val.(*Context) +} diff --git a/internal/node/node.go b/internal/node/node.go index 34e7d1b8..b9943416 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -3,12 +3,13 @@ package node import ( "errors" "fmt" - "github.com/lithammer/shortuuid" - "github.com/serverledge-faas/serverledge/internal/config" "runtime" "strconv" "sync" "time" + + "github.com/lithammer/shortuuid" + "github.com/serverledge-faas/serverledge/internal/config" ) var OutOfResourcesErr = errors.New("not enough resources for function execution") @@ -16,6 +17,7 @@ var OutOfResourcesErr = errors.New("not enough resources for function execution" type NodeID struct { Area string Key string + Arch string } var LocalNode NodeID @@ -26,7 +28,8 @@ func (n NodeID) String() string { func NewIdentifier(area string) NodeID { id := shortuuid.New() + strconv.FormatInt(time.Now().UnixNano(), 10) - return NodeID{Area: area, Key: id} + arch := runtime.GOARCH + return NodeID{Area: area, Key: id, Arch: arch} } type Resources struct { diff --git a/internal/node/pool.go b/internal/node/pool.go index 86775ab1..a3e2b5de 100644 --- a/internal/node/pool.go +++ b/internal/node/pool.go @@ -1,7 +1,6 @@ package node import ( - "container/list" "errors" "log" "time" @@ -12,8 +11,9 @@ import ( ) type ContainerPool struct { - busy *list.List - idle *list.List + // for better efficiently we now use slices here instead of linked lists + busy []*container.Container + idle []*container.Container } var NoWarmFoundErr = errors.New("no warm container is available") @@ -30,20 +30,22 @@ func GetContainerPool(f *function.Function) *ContainerPool { } func (fp *ContainerPool) popIdleContainer() (*container.Container, bool) { - // TODO: picking most-recent / least-recent container might be better? - elem := fp.idle.Front() - if elem == nil { + n := len(fp.idle) + if n == 0 { return nil, false } + // LIFO (maybe better for cache locality) + c := fp.idle[n-1] - c := fp.idle.Remove(elem).(*container.Container) + fp.idle[n-1] = nil // to favor garbage collection + fp.idle = fp.idle[:n-1] // pop the slice return c, true } func (fp *ContainerPool) getReusableContainer(maxConcurrency int16) (*container.Container, bool) { - for elem := fp.busy.Front(); elem != nil; elem = elem.Next() { - c := elem.Value.(*container.Container) + for _, elem := range fp.busy { + c := elem if c.RequestsCount < maxConcurrency { return c, true } @@ -53,11 +55,10 @@ func (fp *ContainerPool) getReusableContainer(maxConcurrency int16) (*container. } func newContainerPool() *ContainerPool { - fp := &ContainerPool{} - fp.busy = list.New() - fp.idle = list.New() - - return fp + return &ContainerPool{ + busy: make([]*container.Container, 0, 10), + idle: make([]*container.Container, 0, 10), + } } func acquireNewMemory(mem int64, forWarmPool bool) bool { @@ -119,7 +120,7 @@ func acquireWarmContainer(f *function.Function) (*container.Container, error) { // add container to the busy pool c.RequestsCount = 1 - fp.busy.PushBack(c) + fp.busy = append(fp.busy, c) log.Printf("Using warm %s for %s. Now: %v", c.ID, f, &LocalResources) return c, nil @@ -153,24 +154,30 @@ func HandleCompletion(cont *container.Container, f *function.Function) { if cont.RequestsCount == 0 { // the container is now idle and must be moved to the warm pool fp := GetContainerPool(f) - // we must update the busy list by removing this element - var deleted interface{} - elem := fp.busy.Front() - for ok := elem != nil; ok; ok = elem != nil { - if elem.Value.(*container.Container) == cont { - deleted = fp.busy.Remove(elem) // delete the element from the busy list + // Search the container index in the slice + idx := -1 + for i, c := range fp.busy { + if c == cont { // with slices, we can compare pointers + idx = i break } - elem = elem.Next() } - if deleted == nil { - log.Println("Failed to release a container!") + + if idx == -1 { + log.Println("Failed to release a container! Not found in busy pool.") return } + // swap then pop from the slice. This way we don't have to + lastIdx := len(fp.busy) - 1 + fp.busy[idx] = fp.busy[lastIdx] // swap between last element and the one we want to delete + fp.busy[lastIdx] = nil // nil to favor garbage collection + fp.busy = fp.busy[:lastIdx] // pop the slice + + // finally, we add the container to the idle pool d := time.Duration(config.GetInt(config.CONTAINER_EXPIRATION_TIME, 600)) * time.Second cont.ExpirationTime = time.Now().Add(d).UnixNano() - fp.idle.PushBack(cont) + fp.idle = append(fp.idle, cont) LocalResources.usedCPUs -= f.CPUDemand LocalResources.busyPoolUsedMem -= f.MemoryMB @@ -216,10 +223,10 @@ func NewContainerWithAcquiredResources(fun *function.Function, startAsIdle bool, fp := GetContainerPool(fun) if startAsIdle { - fp.idle.PushBack(cont) + fp.idle = append(fp.idle, cont) } else { cont.RequestsCount = 1 - fp.busy.PushBack(cont) // We immediately mark it as busy + fp.busy = append(fp.busy, cont) // We immediately mark it as busy } return cont, nil @@ -231,6 +238,7 @@ func NewContainerWithAcquiredResourcesAsync(fun *function.Function, okCallback f if err != nil { log.Printf("Failed container creation: %v\n", err) errCallback(err) + return } LocalResources.Lock() @@ -243,61 +251,78 @@ func NewContainerWithAcquiredResourcesAsync(fun *function.Function, okCallback f fp := GetContainerPool(fun) cont.RequestsCount = 1 - fp.busy.PushBack(cont) // We immediately mark it as busy + fp.busy = append(fp.busy, cont) // We immediately mark it as busy okCallback(cont) }() } type itemToDismiss struct { - contID container.ContainerID + cont *container.Container pool *ContainerPool - elem *list.Element memory int64 } -// dismissContainer ... this function is used to get free memory used for a new container -// 2-phases: first, we find idle container and collect them as a slice, second (cleanup phase) we delete the container only and only if -// the sum of their memory is >= requiredMemoryMB is +// removeContainerFromIdle removes a specific container from the idle pool. +func (fp *ContainerPool) removeContainerFromIdle(target *container.Container) bool { + for i, c := range fp.idle { + if c == target { // pointer comparison + // swap with the last element + lastIdx := len(fp.idle) - 1 + fp.idle[i] = fp.idle[lastIdx] + + fp.idle[lastIdx] = nil // for better garbage collection we delete the last element content before slicing + + fp.idle = fp.idle[:lastIdx] + return true + } + } + return false +} + +// dismissContainer attempts to free memory by dismissing idle containers. +// It works in 2 phases: research (collect candidates) and cleanup (destroy them). +// Containers are actually cleaned only if they free enough memory for the new function/container func dismissContainer(requiredMemoryMB int64) (bool, error) { log.Printf("Trying to dismiss containers to free up at least %d MB", requiredMemoryMB) var cleanedMB int64 = 0 var containerToDismiss []itemToDismiss - //first phase, research + // Phase 1: Research + // We iterate through all pools to find idle containers to (potentially) remove. for _, funPool := range LocalResources.containerPools { - if funPool.idle.Len() > 0 { - // every container into the funPool has the same memory (same function) - //so it is not important which one you destroy - elem := funPool.idle.Front() - contID := elem.Value.(*container.Container).ID - // container in the same pool need same memory - memory, _ := container.GetMemoryMB(contID) - for ok := true; ok; ok = elem != nil { + + if len(funPool.idle) > 0 { + for _, cont := range funPool.idle { + memory, _ := container.GetMemoryMB(cont.ID) + + // We collect the pointer to the container and the pool reference containerToDismiss = append(containerToDismiss, - itemToDismiss{contID: contID, pool: funPool, elem: elem, memory: memory}) + itemToDismiss{cont: cont, pool: funPool, memory: memory}) + cleanedMB += memory if cleanedMB >= requiredMemoryMB { goto cleanup } - //go on to the next one - elem = elem.Next() } } } -cleanup: // second phase, cleanup - // memory check - if cleanedMB >= requiredMemoryMB { +cleanup: // Phase 2: Cleanup + if cleanedMB >= requiredMemoryMB { // if we'd actually free enough memory we do it, otherwise there's no point for _, item := range containerToDismiss { - item.pool.idle.Remove(item.elem) // remove the container from the funPool - err := container.Destroy(item.contID) // destroy the container - if err != nil { - return false, err + if item.pool.removeContainerFromIdle(item.cont) { + // Destroy the actual container resources (Docker/Containerd) + err := container.Destroy(item.cont.ID) + if err != nil { + return false, err + } + LocalResources.warmPoolUsedMem -= item.memory } - LocalResources.warmPoolUsedMem -= item.memory } } else { log.Printf("Not enough containers to free up at least %d MB (avail to dismiss: %d)", requiredMemoryMB, cleanedMB) + return false, errors.New("not enough containers to free up memory") + } return true, nil } @@ -311,28 +336,45 @@ func DeleteExpiredContainer() { defer LocalResources.Unlock() for _, pool := range LocalResources.containerPools { - elem := pool.idle.Front() - for ok := elem != nil; ok; ok = elem != nil { - warm := elem.Value.(*container.Container) + // Index to track the position of kept elements + // Basically, since we now have slices, we want to avoid modifying the slice length while we're iterating + // over it. So, we keep track of the element (containers) we want to keep, then we move them to the front of + // the slice, and finally we cut the slice. + // E.g.: if we have 8 containers, but 3 of them are expired, we cycle over the 8 containers, we put the 5 we need + // to keep from the index 0 to 4, and then we cut the slice after the fifth element. + // Finally, we "nil-out" the last 3 elements (in this example), to favor garbage collection. + kept := 0 + + for _, warm := range pool.idle { if now > warm.ExpirationTime { - temp := elem - elem = elem.Next() - //log.Printf("cleaner: Removing container %s\n", warm.contID) - pool.idle.Remove(temp) // remove the expired element + // remove the expired container + // Update resources memory, _ := container.GetMemoryMB(warm.ID) LocalResources.warmPoolUsedMem -= memory + + // Destroy the actual container err := container.Destroy(warm.ID) if err != nil { log.Printf("Error while destroying container %s: %s\n", warm.ID, err) } - // log.Printf("Released resources. Now: %v\n", &LocalResources) + } else { - elem = elem.Next() + // container is still valid: Keep it + // Rewrite the element at the 'kept' position + pool.idle[kept] = warm + kept++ // position to write the (eventual) next container we need to keep } } - } + // finally, we set as nil the references to the containers we deleted + for i := kept; i < len(pool.idle); i++ { + pool.idle[i] = nil + } + + // Reslice to the new length + pool.idle = pool.idle[:kept] + } } // ShutdownWarmContainersFor destroys warm containers of a given function @@ -346,21 +388,21 @@ func ShutdownWarmContainersFor(f *function.Function) { return } - containersToDelete := make([]container.ContainerID, 0) + containersToDelete := make([]container.ContainerID, 0, len(fp.idle)) // we already know how long it'll need to be, so no need for reallocation - elem := fp.idle.Front() - for ok := elem != nil; ok; ok = elem != nil { - warmed := elem.Value.(*container.Container) - temp := elem - elem = elem.Next() + // Iterate over the idle slice directly + for i, warmed := range fp.idle { log.Printf("Removing container with ID %s\n", warmed.ID) - fp.idle.Remove(temp) memory, _ := container.GetMemoryMB(warmed.ID) LocalResources.warmPoolUsedMem -= memory containersToDelete = append(containersToDelete, warmed.ID) + fp.idle[i] = nil } + // clear the slice + fp.idle = fp.idle[:0] + go func(contIDs []container.ContainerID) { for _, contID := range contIDs { // No need to update available resources here @@ -385,34 +427,38 @@ func ShutdownAllContainers() { continue // should not happen } - for elem := pool.idle.Front(); elem != nil; elem = elem.Next() { - warmed := elem.Value.(*container.Container) - temp := elem + for i, warmed := range pool.idle { log.Printf("Removing container with ID %s\n", warmed.ID) - pool.idle.Remove(temp) err := container.Destroy(warmed.ID) if err != nil { log.Printf("Error while destroying container %s: %s", warmed.ID, err) } LocalResources.warmPoolUsedMem -= functionDescriptor.MemoryMB + + // nil to help garbage collection + pool.idle[i] = nil } + // Reset the idle slice + pool.idle = pool.idle[:0] - for elem := pool.busy.Front(); elem != nil; elem = elem.Next() { - contID := elem.Value.(*container.Container).ID - temp := elem - log.Printf("Removing container with ID %s\n", contID) - pool.idle.Remove(temp) + // now we do the same but for busy containers + for i, busyCont := range pool.busy { + log.Printf("Removing container with ID %s\n", busyCont.ID) - err := container.Destroy(contID) + err := container.Destroy(busyCont.ID) if err != nil { - log.Printf("failed to destroy container %s: %v\n", contID, err) + log.Printf("failed to destroy container %s: %v\n", busyCont.ID, err) continue } LocalResources.usedCPUs -= functionDescriptor.CPUDemand LocalResources.busyPoolUsedMem -= functionDescriptor.MemoryMB + + pool.busy[i] = nil } + // Reset the busy slice capacity + pool.busy = pool.busy[:0] } } @@ -422,7 +468,7 @@ func WarmStatus() map[string]int { defer LocalResources.RUnlock() warmPool := make(map[string]int) for funcName, pool := range LocalResources.containerPools { - warmPool[funcName] = pool.idle.Len() + warmPool[funcName] = len(pool.idle) } return warmPool diff --git a/internal/registration/registry.go b/internal/registration/registry.go index cc06299d..0ea179a3 100644 --- a/internal/registration/registry.go +++ b/internal/registration/registry.go @@ -2,18 +2,20 @@ package registration import ( "fmt" - "github.com/hexablock/vivaldi" - "github.com/serverledge-faas/serverledge/internal/node" - "golang.org/x/exp/maps" "log" "net" "path" + "runtime" "sort" "strconv" "strings" "sync" "time" + "github.com/hexablock/vivaldi" + "github.com/serverledge-faas/serverledge/internal/node" + "golang.org/x/exp/maps" + "github.com/serverledge-faas/serverledge/internal/config" "github.com/serverledge-faas/serverledge/utils" "go.etcd.io/etcd/client/v3" @@ -41,9 +43,9 @@ var etcdLease clientv3.LeaseID func (r *NodeRegistration) toEtcdKey() (key string) { if r.IsLoadBalancer { - return fmt.Sprintf("%s/%s/%s/%s", registryBaseDirectory, r.Area, registryLoadBalancerDirectory, r.Key) + return fmt.Sprintf("%s/%s/%s/%s/%s", registryBaseDirectory, r.Area, registryLoadBalancerDirectory, r.NodeID.Arch, r.Key) } else { - return fmt.Sprintf("%s/%s/%s", registryBaseDirectory, r.Area, r.Key) + return fmt.Sprintf("%s/%s/%s/%s", registryBaseDirectory, r.Area, r.NodeID.Arch, r.Key) } } @@ -82,8 +84,9 @@ func registerToEtcd(asLoadBalancer bool) error { registeredLocalIP := config.GetString(config.API_IP, defaultAddressStr) apiPort := config.GetInt(config.API_PORT, 1323) udpPort := config.GetInt(config.LISTEN_UDP_PORT, 9876) + arch := runtime.GOARCH - payload := fmt.Sprintf("%s;%d;%d", registeredLocalIP, apiPort, udpPort) + payload := fmt.Sprintf("%s;%d;%d;%s", registeredLocalIP, apiPort, udpPort, arch) SelfRegistration = &NodeRegistration{NodeID: node.LocalNode, IPAddress: registeredLocalIP, APIPort: apiPort, UDPPort: udpPort, IsLoadBalancer: asLoadBalancer} @@ -126,7 +129,7 @@ func keepAliveLease() { func parseEtcdRegisteredNode(area string, key string, payload []byte) (NodeRegistration, error) { payloadStr := string(payload) split := strings.Split(payloadStr, ";") - if len(split) < 3 { + if len(split) < 4 { return NodeRegistration{}, fmt.Errorf("invalid payload: %s", payloadStr) } @@ -142,7 +145,9 @@ func parseEtcdRegisteredNode(area string, key string, payload []byte) (NodeRegis return NodeRegistration{}, err } - return NodeRegistration{NodeID: node.NodeID{Area: area, Key: key}, IPAddress: ipAddress, APIPort: apiPort, UDPPort: udpPort}, nil + arch := split[3] + + return NodeRegistration{NodeID: node.NodeID{Area: area, Key: key, Arch: arch}, IPAddress: ipAddress, APIPort: apiPort, UDPPort: udpPort}, nil } // GetNodesInArea is used to obtain the list of other server's addresses under a specific local Area @@ -194,7 +199,8 @@ func GetOneNodeInArea(area string, includeSelf bool) (NodeRegistration, error) { } func GetLBInArea(area string) (map[string]NodeRegistration, error) { - baseDir := areaEtcdKey(area) + "/" + registryLoadBalancerDirectory + //baseDir := areaEtcdKey(area) + "/" + registryLoadBalancerDirectory // bug? + baseDir := areaEtcdKey(area) + registryLoadBalancerDirectory // fix? ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) @@ -395,6 +401,7 @@ func nearbyMonitoring(vivaldiClient *vivaldi.Client) { mutex.Lock() neighborInfo[registeredNode.Key] = newInfo + neighborInfo[registeredNode.Key].LastUpdateTime = time.Now().Unix() _, err := vivaldiClient.Update("node", &newInfo.Coordinates, rtt) if err != nil { diff --git a/internal/registration/types.go b/internal/registration/types.go index b41755df..41470670 100644 --- a/internal/registration/types.go +++ b/internal/registration/types.go @@ -2,6 +2,7 @@ package registration import ( "errors" + "github.com/hexablock/vivaldi" "github.com/serverledge-faas/serverledge/internal/node" ) @@ -25,4 +26,5 @@ type StatusInformation struct { UsedCPU float64 Coordinates vivaldi.Coordinate LoadAvg []float64 + LastUpdateTime int64 // timestamp of last update of this information } diff --git a/internal/scheduling/cloudonly_policy.go b/internal/scheduling/cloudonly_policy.go index 2504a085..d29c5c19 100644 --- a/internal/scheduling/cloudonly_policy.go +++ b/internal/scheduling/cloudonly_policy.go @@ -14,7 +14,7 @@ func (p *CloudOnlyPolicy) OnCompletion(_ *function.Function, _ *function.Executi func (p *CloudOnlyPolicy) OnArrival(r *scheduledRequest) { if r.CanDoOffloading { - handleCloudOffload(r) + handleCloudOffload(r) // This will also check for architecture compatibility } else { dropRequest(r) } diff --git a/internal/scheduling/edgeCloudPolicy.go b/internal/scheduling/edgeCloudPolicy.go index ae24c26f..9ab0ffc7 100644 --- a/internal/scheduling/edgeCloudPolicy.go +++ b/internal/scheduling/edgeCloudPolicy.go @@ -1,6 +1,8 @@ package scheduling import ( + "log" + "github.com/serverledge-faas/serverledge/internal/function" "github.com/serverledge-faas/serverledge/internal/node" ) @@ -18,12 +20,20 @@ func (p *CloudEdgePolicy) OnCompletion(_ *function.Function, _ *function.Executi } func (p *CloudEdgePolicy) OnArrival(r *scheduledRequest) { - containerID, warm, err := node.AcquireContainer(r.Fun, false) - if err == nil { - execLocally(r, containerID, warm) - } else if r.CanDoOffloading { + + canRunLocally := r.Fun.SupportsArch(node.LocalNode.Arch) + if canRunLocally { + containerID, warm, err := node.AcquireContainer(r.Fun, false) + if err == nil { + execLocally(r, containerID, warm) + return + } + } + + if r.CanDoOffloading { handleCloudOffload(r) } else { + log.Printf("Dropping request because cannot exec locally (architecutre is supported: %t) and cannot offload", canRunLocally) dropRequest(r) } } diff --git a/internal/scheduling/edgeOnlyPolicy.go b/internal/scheduling/edgeOnlyPolicy.go index 77b1656e..4c10ffd9 100644 --- a/internal/scheduling/edgeOnlyPolicy.go +++ b/internal/scheduling/edgeOnlyPolicy.go @@ -1,6 +1,10 @@ package scheduling import ( + "errors" + "log" + + "github.com/serverledge-faas/serverledge/internal/config" "github.com/serverledge-faas/serverledge/internal/function" "github.com/serverledge-faas/serverledge/internal/node" ) @@ -8,7 +12,11 @@ import ( // EdgePolicy supports only Edge-Edge offloading. Always does offloading to an edge node if enabled. When offloading is not enabled executes the request locally. type EdgePolicy struct{} +var fallBackLocally bool + func (p *EdgePolicy) Init() { + fallBackLocally = config.GetBool(config.SCHEDULING_FALLBACK_LOCAL, false) + log.Printf("[INFO] Initializing EdgePolicy. Fallback to local execution set to: %t\n", fallBackLocally) } func (p *EdgePolicy) OnCompletion(_ *function.Function, _ *function.ExecutionReport) { @@ -16,19 +24,38 @@ func (p *EdgePolicy) OnCompletion(_ *function.Function, _ *function.ExecutionRep } func (p *EdgePolicy) OnArrival(r *scheduledRequest) { + if r.CanDoOffloading { - url := pickEdgeNodeForOffloading(r) + url, err := pickEdgeNodeForOffloading(r) // this will now take into account the node architecture in the offloading process if url != "" { handleOffload(r, url) return + } else if errors.Is(err, NoSuitableNode) && fallBackLocally { + // This is the case where offloading could've been possible (I had available neighbors) + // but they ALL were of a mismatching architecture. + // E.g.: r.Fun.SupportedArchs = {"amd64"}, but all nNeighbors are arm-based. + + tryLocalExecution(r) } } else { - containerID, warm, err := node.AcquireContainer(r.Fun, false) - if err == nil { - execLocally(r, containerID, warm) - return - } + tryLocalExecution(r) } + dropRequest(r) // r.CanDoOffloading == true, NoSuitableNode == true && fallBackLocally == false leads here, so we drop + // the request in that case +} + +func tryLocalExecution(r *scheduledRequest) { + if !r.Fun.SupportsArch(node.LocalNode.Arch) { + // If the current node architecture is not supported by the function's runtime, we can only drop it, since + // offloading was already tried unsuccessfully, or it was disabled for this request. + dropRequest(r) + return - dropRequest(r) + } + + containerID, warm, err := node.AcquireContainer(r.Fun, false) + if err == nil { + execLocally(r, containerID, warm) + return + } } diff --git a/internal/scheduling/execution.go b/internal/scheduling/execution.go index fc8e030c..768f0019 100644 --- a/internal/scheduling/execution.go +++ b/internal/scheduling/execution.go @@ -7,6 +7,7 @@ import ( "github.com/serverledge-faas/serverledge/internal/container" "github.com/serverledge-faas/serverledge/internal/executor" + "github.com/serverledge-faas/serverledge/internal/node" ) const HANDLER_DIR = "/app" @@ -65,6 +66,8 @@ func Execute(cont *container.Container, r *scheduledRequest, isWarm bool) error // initializing containers may require invocation retries, adding // latency r.InitTime = initTime + invocationWait.Seconds() + node.HandleCompletion(cont, r.Fun) + // notify scheduler completions <- &completionNotification{r: r, cont: cont, failed: false} diff --git a/internal/scheduling/offloading.go b/internal/scheduling/offloading.go index d76442f0..84359e95 100644 --- a/internal/scheduling/offloading.go +++ b/internal/scheduling/offloading.go @@ -3,34 +3,71 @@ package scheduling import ( "bytes" "encoding/json" + "errors" "fmt" "io" "log" - "math/rand" "net/http" "time" "github.com/serverledge-faas/serverledge/internal/client" + "github.com/serverledge-faas/serverledge/internal/config" "github.com/serverledge-faas/serverledge/internal/function" "github.com/serverledge-faas/serverledge/internal/node" "github.com/serverledge-faas/serverledge/internal/registration" ) -func pickEdgeNodeForOffloading(r *scheduledRequest) (url string) { - // TODO: better to cache choice for a while - // TODO: check available mem as well +// Offloading choice caching +var offloadingCache = make(map[string]*registration.NodeRegistration) +var cacheExpiration = make(map[string]time.Time) +var CacheValidity = 60 * time.Second +var NoSuitableNode = errors.New("no node supporting the function's runtime found") +var NoNeighbors = errors.New("the list of neighbors is empty") + +func pickEdgeNodeForOffloading(r *scheduledRequest) (url string, err error) { + // check cache first + cached, ok := offloadingCache[r.Fun.Name] + if ok && time.Now().Before(cacheExpiration[r.Fun.Name]) { + return cached.APIUrl(), nil + } + + // select best node nearestNeighbors := registration.GetNearestNeighbors() if nearestNeighbors == nil { - return "" + return "", NoNeighbors + } + + neighborStatus := registration.GetFullNeighborInfo() + + var bestNode *registration.NodeRegistration + maxMem := int64(0) + + for _, nodeReg := range nearestNeighbors { + status, ok := neighborStatus[nodeReg.Key] + if !ok { + continue + } + availableMemory := status.TotalMemory - status.UsedMemory + if r.Fun.SupportsArch(nodeReg.Arch) && availableMemory > maxMem { + maxMem = availableMemory + bestNode = &nodeReg + } + } + + if bestNode != nil { + cacheValidityInt := config.GetInt(config.OFFLOADING_CACHE_VALIDITY, 60) + CacheValidity = time.Duration(cacheValidityInt) * time.Second + offloadingCache[r.Fun.Name] = bestNode + cacheExpiration[r.Fun.Name] = time.Now().Add(CacheValidity) + return bestNode.APIUrl(), nil } - randomItem := nearestNeighbors[rand.Intn(len(nearestNeighbors))] - return randomItem.APIUrl() + return "", NoSuitableNode } func Offload(r *scheduledRequest, serverUrl string) error { // Prepare request - request := client.InvocationRequest{Params: r.Params, QoSClass: r.Class, QoSMaxRespT: r.MaxRespT} + request := client.InvocationRequest{Params: r.Params, QoSClass: r.Class, QoSMaxRespT: r.MaxRespT, ReturnOutput: r.ReturnOutput} invocationBody, err := json.Marshal(request) if err != nil { log.Print(err) diff --git a/internal/scheduling/policy_default.go b/internal/scheduling/policy_default.go index d93bfe37..11d37821 100644 --- a/internal/scheduling/policy_default.go +++ b/internal/scheduling/policy_default.go @@ -4,10 +4,9 @@ import ( "errors" "log" + "github.com/serverledge-faas/serverledge/internal/config" "github.com/serverledge-faas/serverledge/internal/container" "github.com/serverledge-faas/serverledge/internal/function" - - "github.com/serverledge-faas/serverledge/internal/config" "github.com/serverledge-faas/serverledge/internal/node" ) @@ -75,6 +74,14 @@ func (p *DefaultLocalPolicy) OnCompletion(_ *function.Function, _ *function.Exec // OnArrival for default policy is executed every time a function is invoked, before invoking the function func (p *DefaultLocalPolicy) OnArrival(r *scheduledRequest) { + + if !r.Fun.SupportsArch(node.LocalNode.Arch) { + // If the current node architecture is not supported by the function's runtime, we can only drop it, since in + // this policy there is no offloading. + dropRequest(r) + return + + } containerID, warm, err := node.AcquireContainer(r.Fun, false) if err == nil { execLocally(r, containerID, warm) // decides to execute locally diff --git a/internal/scheduling/scheduler.go b/internal/scheduling/scheduler.go index 9a6cfad6..ed187b00 100644 --- a/internal/scheduling/scheduler.go +++ b/internal/scheduling/scheduler.go @@ -2,11 +2,12 @@ package scheduling import ( "fmt" - "github.com/serverledge-faas/serverledge/internal/registration" "log" "net/http" "time" + "github.com/serverledge-faas/serverledge/internal/registration" + "github.com/serverledge-faas/serverledge/internal/container" "github.com/serverledge-faas/serverledge/internal/function" "github.com/serverledge-faas/serverledge/internal/metrics" @@ -53,7 +54,6 @@ func Run(p Policy) { case r = <-requests: // receive request go p.OnArrival(r) case c = <-completions: - node.HandleCompletion(c.cont, c.r.Fun) p.OnCompletion(c.r.Fun, c.r.ExecutionReport) if metrics.Enabled && !c.failed && c.r.ExecutionReport != nil { @@ -165,7 +165,10 @@ func handleCloudOffload(r *scheduledRequest) { if offloadingTarget == nil { log.Printf("No remote offloading target available; dropping request") r.decisionChannel <- schedDecision{action: DROP} - } else { + // TODO check if this is a correct assumption to make + } else if offloadingTarget.IsLoadBalancer || r.Fun.SupportsArch(node.LocalNode.Arch) { handleOffload(r, offloadingTarget.APIUrl()) + } else { + dropRequest(r) } } diff --git a/internal/test/api_test.go b/internal/test/api_test.go index 523e8a7a..4d81d44d 100644 --- a/internal/test/api_test.go +++ b/internal/test/api_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/spf13/cast" + "runtime" "strings" "testing" "time" @@ -12,6 +12,7 @@ import ( "github.com/serverledge-faas/serverledge/internal/function" "github.com/serverledge-faas/serverledge/internal/workflow" "github.com/serverledge-faas/serverledge/utils" + "github.com/spf13/cast" ) // TestContainerPool executes repeatedly different functions (**not compositions**) to verify the container pool @@ -20,8 +21,8 @@ func TestContainerPool(t *testing.T) { t.Skip("Skipping integration test") } // creating inc and double functions - funcs := []string{"inc", "double"} - for _, name := range funcs { + pyFuncs := []string{"inc", "double"} + for _, name := range pyFuncs { fn, err := InitializePyFunction(name, "handler", function.NewSignature(). AddInput("input", function.Int{}). AddOutput("result", function.Int{}). @@ -30,31 +31,49 @@ func TestContainerPool(t *testing.T) { createApiIfNotExistsTest(t, fn, HOST, PORT) } + + // creating java function + javaFn, err := InitializeJavaFunction("hello-java", "com.test.HelloFunction", function.NewSignature(). + AddInput("name", function.Text{}). + AddOutput("greeting", function.Text{}). + Build()) + utils.AssertNil(t, err) + createApiIfNotExistsTest(t, javaFn, HOST, PORT) + // executing all functions channel := make(chan error) const n = 3 for i := 0; i < n; i++ { - for _, name := range funcs { + for _, name := range pyFuncs { x := make(map[string]interface{}) x["input"] = 1 fnName := name go func() { - time.Sleep(50 * time.Millisecond) + time.Sleep(5 * time.Second) err := invokeApiTest(fnName, x, HOST, PORT) channel <- err }() } + // invoke java func + x := make(map[string]interface{}) + x["name"] = "World" + go func() { + time.Sleep(5 * time.Second) + err := invokeApiTest(javaFn.Name, x, HOST, PORT) + channel <- err + }() } // wait for all functions to complete and checking the errors - for i := 0; i < len(funcs)*n; i++ { + for i := 0; i < (len(pyFuncs)+1)*n; i++ { err := <-channel utils.AssertNil(t, err) } // delete each function - for _, name := range funcs { + for _, name := range pyFuncs { deleteApiTest(t, name, HOST, PORT) } + deleteApiTest(t, javaFn.Name, HOST, PORT) //utils.AssertTrueMsg(t, workflow.IsEmptyPartialDataCache(), "partial data cache is not empty") } @@ -259,3 +278,35 @@ func TestAsyncInvokeWorkflow(t *testing.T) { err = wflow.Delete() utils.AssertNilMsg(t, err, "failed to delete composition") } + +// TestMismatchingArch tests that the execution fails if the node's architecture doesn't support the function's one +// (and offloading is disabled) +func TestMismatchingArchNoOffload(t *testing.T) { + + name := "double" + fn, err := InitializePyFunction(name, "handler", function.NewSignature(). + AddInput("input", function.Int{}). + AddOutput("result", function.Int{}). + Build()) + utils.AssertNil(t, err) + currentArch := runtime.GOARCH + for i, arch := range fn.SupportedArchs { + if arch == currentArch { + fn.SupportedArchs = append(fn.SupportedArchs[:i], fn.SupportedArchs[i+1:]...) + } + } + + createApiIfNotExistsTest(t, fn, HOST, PORT) + + // executing all functions + x := make(map[string]interface{}) + x["input"] = 1 + fnName := name + + time.Sleep(50 * time.Millisecond) + err = invokeApiTestSetOffloading(fnName, x, HOST, PORT, false) // no offloading + utils.AssertNonNil(t, err) // Expecting an error due to mismatching architecture + + // delete function + deleteApiTest(t, name, HOST, PORT) +} diff --git a/internal/test/main_test.go b/internal/test/main_test.go index 4da706e4..5a72d702 100644 --- a/internal/test/main_test.go +++ b/internal/test/main_test.go @@ -2,15 +2,18 @@ package test import ( "context" + "errors" "flag" "fmt" - "github.com/serverledge-faas/serverledge/internal/config" "log" + "net/http" "os" "os/exec" "testing" "time" + "github.com/serverledge-faas/serverledge/internal/config" + "github.com/labstack/echo/v4" "github.com/serverledge-faas/serverledge/internal/api" "github.com/serverledge-faas/serverledge/internal/metrics" @@ -95,6 +98,8 @@ func TestMain(m *testing.M) { os.Exit(int(codes.Internal)) } + waitForServerReady() + // run all test independently code := m.Run() @@ -106,8 +111,36 @@ func TestMain(m *testing.M) { os.Exit(code) } +func waitForServerReady() { + // Wait for the server to be ready by polling the /status endpoint. There was a race condition in the test + // especially noticeable for less powerful hardware. + for i := 0; i < 50; i++ { + resp, err := http.Get(fmt.Sprintf("http://%s:%d/status", HOST, PORT)) + if err == nil && resp.StatusCode == http.StatusOK { + log.Println("Server is ready.") + return + } + time.Sleep(100 * time.Millisecond) + } + log.Fatal("Server not ready after timeout") +} + // startReliably can start the containers, or restart them if needed func startReliably(startScript string) error { + // make sure etcd is not already running, this will cause issues. Also clean etcd status for clean start + cmd1 := exec.CommandContext(context.Background(), getShell(), "../../scripts/stop-etcd"+getShellExt()) + if err := cmd1.Run(); err != nil { + // If the container doesn't exist, `docker stop` and `docker rm` will fail. + // We can safely ignore this error and proceed. + var exitError *exec.ExitError + if errors.As(err, &exitError) { + // 1 indicates that the container does not exist. + if exitError.ExitCode() != 1 { + log.Printf("failed to stop etcd: %v", err) + } + } + } + cmd := exec.CommandContext(context.Background(), getShell(), startScript) err := cmd.Run() if err != nil { diff --git a/internal/test/util.go b/internal/test/util.go index dc2aa1ab..d2957b29 100644 --- a/internal/test/util.go +++ b/internal/test/util.go @@ -4,11 +4,12 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/serverledge-faas/serverledge/internal/node" "net/http" "os" "testing" + "github.com/serverledge-faas/serverledge/internal/node" + "github.com/serverledge-faas/serverledge/internal/cli" "github.com/serverledge-faas/serverledge/internal/client" "github.com/serverledge-faas/serverledge/internal/function" @@ -18,6 +19,9 @@ import ( const PY_MEMORY = 20 const JS_MEMORY = 50 +const JAVA_MEMORY = 100 +const X86 = "amd64" +const ARM = "arm64" func initializeExamplePyFunction() (*function.Function, error) { oldF, found := function.GetFunction("inc") @@ -35,10 +39,11 @@ func initializeExamplePyFunction() (*function.Function, error) { encoded := base64.StdEncoding.EncodeToString(srcContent) f := function.Function{ Name: "inc", - Runtime: "python310", + Runtime: "python314", MemoryMB: PY_MEMORY, - CPUDemand: 1.0, + CPUDemand: 0.1, Handler: "inc.handler", // on python, for now is needed file name and handler name!! + SupportedArchs: []string{X86, ARM}, TarFunctionCode: encoded, Signature: function.NewSignature(). AddInput("input", function.Int{}). @@ -68,8 +73,9 @@ func initializeExampleJSFunction() (*function.Function, error) { Name: "inc", Runtime: "nodejs17ng", MemoryMB: JS_MEMORY, - CPUDemand: 1.0, + CPUDemand: 0.1, Handler: "inc", // for js, only the file name is needed!! + SupportedArchs: []string{X86, ARM}, TarFunctionCode: encoded, Signature: function.NewSignature(). AddInput("input", function.Int{}). @@ -97,10 +103,39 @@ func InitializePyFunction(name string, handler string, sign *function.Signature) encoded := base64.StdEncoding.EncodeToString(srcContent) f := function.Function{ Name: name, - Runtime: "python310", + Runtime: "python314", MemoryMB: PY_MEMORY, - CPUDemand: 0.25, + CPUDemand: 0.1, Handler: fmt.Sprintf("%s.%s", name, handler), // on python, for now is needed file name and handler name!! + SupportedArchs: []string{X86, ARM}, + TarFunctionCode: encoded, + Signature: sign, + } + err = f.SaveToEtcd() + return &f, err +} + +func InitializeJavaFunction(name string, handler string, sign *function.Signature) (*function.Function, error) { + oldF, found := function.GetFunction(name) + if found { + // the function already exists; we delete it + oldF.Delete() + node.ShutdownWarmContainersFor(oldF) + } + + srcPath := "../../examples/java_build/target/hello-function-1.0.0-jar-with-dependencies.jar" + srcContent, err := cli.ReadSourcesAsTar(srcPath) + if err != nil { + return nil, fmt.Errorf("cannot read java sources %s as tar: %v", srcPath, err) + } + encoded := base64.StdEncoding.EncodeToString(srcContent) + f := function.Function{ + Name: name, + Runtime: "java21", + MemoryMB: JAVA_MEMORY, + CPUDemand: 0.1, + Handler: handler, + SupportedArchs: []string{X86, ARM}, TarFunctionCode: encoded, Signature: sign, } @@ -126,8 +161,9 @@ func initializeJsFunction(name string, sign *function.Signature) (*function.Func Name: name, Runtime: "nodejs17ng", MemoryMB: JS_MEMORY, - CPUDemand: 0.25, + CPUDemand: 0.1, Handler: name, // on js only file name is needed!! + SupportedArchs: []string{X86, ARM}, TarFunctionCode: encoded, Signature: sign, } @@ -225,10 +261,14 @@ func createApiIfNotExistsTest(t *testing.T, fn *function.Function, host string, } func invokeApiTest(fn string, params map[string]interface{}, host string, port int) error { + return invokeApiTestSetOffloading(fn, params, host, port, true) +} + +func invokeApiTestSetOffloading(fn string, params map[string]interface{}, host string, port int, offloading bool) error { request := client.InvocationRequest{ Params: params, QoSMaxRespT: 250, - CanDoOffloading: true, + CanDoOffloading: offloading, Async: false, } invocationBody, err1 := json.Marshal(request) diff --git a/scripts/start-etcd.sh b/scripts/start-etcd.sh index e058b6aa..ca4ba573 100755 --- a/scripts/start-etcd.sh +++ b/scripts/start-etcd.sh @@ -1,8 +1,17 @@ #!/bin/sh -docker run -d --rm --name Etcd-server \ - --publish 2379:2379 \ - --publish 2380:2380 \ - --cpus="1" \ - --env ALLOW_NONE_AUTHENTICATION=yes \ - --env ETCD_ADVERTISE_CLIENT_URLS=http://localhost:2379 \ - bitnami/etcd:3.5.14-debian-12-r1 + +docker run -d \ + -p 2379:2379 \ + -p 2380:2380 \ + --name Etcd-server \ + gcr.io/etcd-development/etcd:v3.5.13 \ + /usr/local/bin/etcd \ + --name s1 \ + --data-dir /etcd-data \ + --listen-client-urls http://0.0.0.0:2379 \ + --advertise-client-urls http://0.0.0.0:2379 \ + --listen-peer-urls http://0.0.0.0:2380 \ + --initial-advertise-peer-urls http://0.0.0.0:2380 \ + --initial-cluster s1=http://0.0.0.0:2380 \ + --initial-cluster-token tkn \ + --initial-cluster-state new diff --git a/scripts/start-global-registry.sh b/scripts/start-global-registry.sh new file mode 100755 index 00000000..7224059b --- /dev/null +++ b/scripts/start-global-registry.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# Automatically find Internal IP +INTERNAL_IP=$(hostname -I | awk '{print $1}') + +# Find External IP (via Google Cloud metadata server) +PUBLIC_IP=$(curl -s --connect-timeout 2 --max-time 2 -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip 2>/dev/null) +# Fallback if not on GCP or curl fails +if [ -z "$PUBLIC_IP" ]; then + echo "Could not determine Public IP. Using Internal as fallback." + PUBLIC_IP=$INTERNAL_IP +fi + +echo "Internal IP: $INTERNAL_IP" +echo "Public IP: $PUBLIC_IP" + + +docker run -d \ + -p 2379:2379 \ + -p 2380:2380 \ + --name Etcd-server \ + gcr.io/etcd-development/etcd:v3.5.13 \ + /usr/local/bin/etcd \ + --name s1 \ + --data-dir /etcd-data \ + --listen-client-urls http://0.0.0.0:2379 \ + --advertise-client-urls http://${INTERNAL_IP}:2379,http://${PUBLIC_IP}:2379 \ + --listen-peer-urls http://0.0.0.0:2380 \ + --initial-advertise-peer-urls http://${INTERNAL_IP}:2380 \ + --initial-cluster s1=http://${INTERNAL_IP}:2380 \ + --initial-cluster-token tkn \ + --initial-cluster-state new \ No newline at end of file diff --git a/scripts/stop-etcd.sh b/scripts/stop-etcd.sh index 0c700a38..377769b6 100755 --- a/scripts/stop-etcd.sh +++ b/scripts/stop-etcd.sh @@ -1,2 +1,4 @@ #!/bin/sh -docker kill Etcd-server +docker stop Etcd-server +docker rm Etcd-server +rm -rf default.etcd/ \ No newline at end of file diff --git a/serverledge/runtime.go b/serverledge/runtime.go new file mode 100644 index 00000000..a6bafb3b --- /dev/null +++ b/serverledge/runtime.go @@ -0,0 +1,136 @@ +package serverledge + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "sync" +) + +// Mutex to use if a request wants to capture output. Since it's shared amongst all the functions running in the container +// we have to use a mutex. If functions don't need the StdOut and StdErr to be captured, this will be ignored. +var outputMu sync.Mutex + +// HandlerFunc is the signature of the handler that has to be provided to serverledge +type HandlerFunc func(params map[string]interface{}) (interface{}, error) + +// executionResult is a data structure to capture execution output +type executionResult struct { + Result interface{} + Output string + Err error +} + +// executeWithCapture is the wrapper used to run the function and capture its output. +func executeWithCapture(handler HandlerFunc, params map[string]interface{}) executionResult { + // Lock beacuse stdout/stderr are shared amongst all the functions + outputMu.Lock() + defer outputMu.Unlock() + + // old values to be restored after execution + oldStdout := os.Stdout + oldStderr := os.Stderr + oldLog := log.Writer() + + // pipe used to read the stdout/err + r, w, _ := os.Pipe() + + os.Stdout = w + os.Stderr = w + log.SetOutput(w) + + // channel to pass the output to the wrapper + outC := make(chan string) + + // this goroutine will read from the pipe and wirte to the channel + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + outC <- buf.String() + }() + + // running the user's handler now + var res interface{} + var err error + + // This function will close the pipe after we have executed the user's function + func() { + defer func() { + _ = w.Close() + os.Stdout = oldStdout + os.Stderr = oldStderr + log.SetOutput(oldLog) + }() + res, err = handler(params) // actual execution of the handler + }() + + // wait until we read the output from the channel + output := <-outC + + return executionResult{Result: res, Output: output, Err: err} +} + +// Start is used to execute the given handler. Has to be used by the user of the library in their source code to launch +// their function (e.g.: serverledge.Start(myHandler)) +func Start(handler HandlerFunc) { + http.HandleFunc("/invoke", func(w http.ResponseWriter, r *http.Request) { + // here we put the params obtained from the http request and the boolean to check if we need to also capture + // the output of the function + var req struct { + Params map[string]interface{} `json:"Params"` + ReturnOutput bool `json:"ReturnOutput"` + } + + body, _ := io.ReadAll(r.Body) + if err := json.Unmarshal(body, &req); err != nil { + http.Error(w, "Invalid Request", 500) + return + } + + var execRes executionResult + + // check if we need to wrap the execution to capture output, or if we can directly execute the handler + if req.ReturnOutput { + execRes = executeWithCapture(handler, req.Params) + } else { + res, err := handler(req.Params) + execRes = executionResult{Result: res, Output: "", Err: err} + } + + // Response to be sent outside the container back to the client + resp := map[string]interface{}{ + "Success": true, + "Result": "", + "Output": execRes.Output, + } + + if execRes.Err != nil { // failure + resp["Success"] = false + if resp["Output"].(string) != "" { + resp["Output"] = resp["Output"].(string) + "\n" + execRes.Err.Error() // concatenate the error, no overwriting + } else { + resp["Output"] = execRes.Err.Error() + } + } else { // success + resBytes, _ := json.Marshal(execRes.Result) + resp["Result"] = string(resBytes) + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(resp) + if err != nil { + log.Printf("Error encoding response: %v", err) + return + } + }) + + // start the http server inside the container + + port := 8080 // standard port for all containers + log.Printf("Go Runtime listening on :%d", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) +}