mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 14:44:21 +01:00
dynamic port selection for temporal (#2865)
This commit is contained in:
@@ -13,9 +13,11 @@ use crate::scheduler::{ScheduledJob, SchedulerError};
|
||||
use crate::scheduler_trait::SchedulerTrait;
|
||||
use crate::session::storage::SessionMetadata;
|
||||
|
||||
const TEMPORAL_SERVICE_URL: &str = "http://localhost:8080";
|
||||
const TEMPORAL_SERVICE_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const TEMPORAL_SERVICE_HEALTH_CHECK_INTERVAL: Duration = Duration::from_secs(2);
|
||||
const TEMPORAL_SERVICE_STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const TEMPORAL_SERVICE_HEALTH_CHECK_INTERVAL: Duration = Duration::from_millis(500);
|
||||
|
||||
// Default ports to try when discovering the service
|
||||
const DEFAULT_HTTP_PORTS: &[u16] = &[8080, 8081, 8082, 8083, 8084, 8085];
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct JobRequest {
|
||||
@@ -50,46 +52,290 @@ struct RunNowResponse {
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PortConfig {
|
||||
http_port: u16,
|
||||
temporal_port: u16,
|
||||
ui_port: u16,
|
||||
}
|
||||
|
||||
pub struct TemporalScheduler {
|
||||
http_client: Client,
|
||||
service_url: String,
|
||||
port_config: PortConfig,
|
||||
}
|
||||
|
||||
impl TemporalScheduler {
|
||||
pub async fn new() -> Result<Arc<Self>, SchedulerError> {
|
||||
let http_client = Client::new();
|
||||
let service_url = TEMPORAL_SERVICE_URL.to_string();
|
||||
|
||||
// Discover the HTTP port
|
||||
let http_port = Self::discover_http_port(&http_client).await?;
|
||||
let service_url = format!("http://localhost:{}", http_port);
|
||||
|
||||
info!("Found Temporal service HTTP API on port {}", http_port);
|
||||
|
||||
// Create scheduler with initial port config
|
||||
let scheduler = Arc::new(Self {
|
||||
http_client,
|
||||
service_url,
|
||||
http_client: http_client.clone(),
|
||||
service_url: service_url.clone(),
|
||||
port_config: PortConfig {
|
||||
http_port,
|
||||
temporal_port: 7233, // temporary defaults
|
||||
ui_port: 8233,
|
||||
},
|
||||
});
|
||||
|
||||
// Start the Go service (which will handle starting Temporal server)
|
||||
// Start the Go service if not already running
|
||||
scheduler.start_go_service().await?;
|
||||
|
||||
// Wait for service to be ready
|
||||
scheduler.wait_for_service_ready().await?;
|
||||
|
||||
// Fetch the actual port configuration and update
|
||||
let port_config = scheduler.fetch_port_config().await?;
|
||||
|
||||
info!(
|
||||
"Discovered Temporal service ports - HTTP: {}, Temporal: {}, UI: {}",
|
||||
port_config.http_port, port_config.temporal_port, port_config.ui_port
|
||||
);
|
||||
|
||||
// Create final scheduler with correct ports
|
||||
let final_scheduler = Arc::new(Self {
|
||||
http_client,
|
||||
service_url,
|
||||
port_config,
|
||||
});
|
||||
|
||||
info!("TemporalScheduler initialized successfully");
|
||||
Ok(scheduler)
|
||||
Ok(final_scheduler)
|
||||
}
|
||||
|
||||
async fn discover_http_port(_http_client: &Client) -> Result<u16, SchedulerError> {
|
||||
// First, try to find a running service using pgrep and lsof
|
||||
if let Ok(port) = Self::find_temporal_service_port_from_processes() {
|
||||
info!(
|
||||
"Found Temporal service port {} from running processes",
|
||||
port
|
||||
);
|
||||
return Ok(port);
|
||||
}
|
||||
|
||||
// If no running service found, we need to find a free port to start the service on
|
||||
info!("No running Temporal service found, finding free port to start service");
|
||||
|
||||
// Check PORT environment variable first
|
||||
if let Ok(port_str) = std::env::var("PORT") {
|
||||
if let Ok(port) = port_str.parse::<u16>() {
|
||||
if Self::is_port_free(port).await {
|
||||
info!("Using PORT environment variable: {}", port);
|
||||
return Ok(port);
|
||||
} else {
|
||||
warn!(
|
||||
"PORT environment variable {} is not free, finding alternative",
|
||||
port
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find a free port from the default list
|
||||
for &port in DEFAULT_HTTP_PORTS {
|
||||
if Self::is_port_free(port).await {
|
||||
info!("Found free port {} for Temporal service", port);
|
||||
return Ok(port);
|
||||
}
|
||||
}
|
||||
|
||||
// If all default ports are taken, find any free port in a reasonable range
|
||||
for port in 8086..8200 {
|
||||
if Self::is_port_free(port).await {
|
||||
info!("Found free port {} for Temporal service", port);
|
||||
return Ok(port);
|
||||
}
|
||||
}
|
||||
|
||||
Err(SchedulerError::SchedulerInternalError(
|
||||
"Could not find any free port for Temporal service".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn is_port_free(port: u16) -> bool {
|
||||
use std::net::{SocketAddr, TcpListener};
|
||||
use std::time::Duration;
|
||||
|
||||
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
|
||||
// First, try to bind to the port
|
||||
let listener_result = TcpListener::bind(addr);
|
||||
match listener_result {
|
||||
Ok(listener) => {
|
||||
// Successfully bound, so port was free
|
||||
drop(listener); // Release the port immediately
|
||||
|
||||
// Double-check by trying to connect to see if anything is actually listening
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let test_url = format!("http://127.0.0.1:{}", port);
|
||||
match client.get(&test_url).send().await {
|
||||
Ok(_) => {
|
||||
// Something responded, so port is actually in use
|
||||
warn!(
|
||||
"Port {} appeared free but something is listening on it",
|
||||
port
|
||||
);
|
||||
false
|
||||
}
|
||||
Err(_) => {
|
||||
// Nothing responded, port is truly free
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Could not bind, port is definitely in use
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_temporal_service_port_from_processes() -> Result<u16, SchedulerError> {
|
||||
// Use pgrep to find temporal-service processes
|
||||
let pgrep_output = Command::new("pgrep")
|
||||
.arg("-f")
|
||||
.arg("temporal-service")
|
||||
.output()
|
||||
.map_err(|e| SchedulerError::SchedulerInternalError(format!("pgrep failed: {}", e)))?;
|
||||
|
||||
if !pgrep_output.status.success() {
|
||||
return Err(SchedulerError::SchedulerInternalError(
|
||||
"No temporal-service processes found".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let pids_str = String::from_utf8_lossy(&pgrep_output.stdout);
|
||||
let pids: Vec<&str> = pids_str
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
for pid in pids {
|
||||
// Use lsof to find listening ports for this PID
|
||||
let lsof_output = Command::new("lsof")
|
||||
.arg("-p")
|
||||
.arg(pid)
|
||||
.arg("-i")
|
||||
.arg("tcp")
|
||||
.arg("-P") // Show port numbers instead of service names
|
||||
.arg("-n") // Show IP addresses instead of hostnames
|
||||
.output();
|
||||
|
||||
if let Ok(output) = lsof_output {
|
||||
let lsof_str = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Look for HTTP API port (typically 8080-8999 range)
|
||||
for line in lsof_str.lines() {
|
||||
if line.contains("LISTEN") && line.contains("temporal-") {
|
||||
// Parse lines like: "temporal-service 12345 user 6u IPv4 0x... 0t0 TCP *:8081 (LISTEN)"
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
|
||||
// Find the TCP part which contains the port
|
||||
for part in &parts {
|
||||
if part.starts_with("TCP") && part.contains(':') {
|
||||
// Extract port from TCP *:8081 or TCP 127.0.0.1:8081
|
||||
if let Some(port_str) = part.split(':').next_back() {
|
||||
if let Ok(port) = port_str.parse::<u16>() {
|
||||
// HTTP API ports are typically in 8080-8999 range
|
||||
if (8080..9000).contains(&port) {
|
||||
info!("Found HTTP API port {} for PID {}", port, pid);
|
||||
return Ok(port);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(SchedulerError::SchedulerInternalError(
|
||||
"Could not find HTTP API port from temporal-service processes".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn fetch_port_config(&self) -> Result<PortConfig, SchedulerError> {
|
||||
let url = format!("{}/ports", self.service_url);
|
||||
|
||||
match self.http_client.get(&url).send().await {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
let port_config: PortConfig = response.json().await.map_err(|e| {
|
||||
SchedulerError::SchedulerInternalError(format!(
|
||||
"Failed to parse port config JSON: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
Ok(port_config)
|
||||
} else {
|
||||
Err(SchedulerError::SchedulerInternalError(format!(
|
||||
"Failed to fetch port config: HTTP {}",
|
||||
response.status()
|
||||
)))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(SchedulerError::SchedulerInternalError(format!(
|
||||
"Failed to fetch port config: {}",
|
||||
e
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current port configuration
|
||||
pub fn get_port_config(&self) -> &PortConfig {
|
||||
&self.port_config
|
||||
}
|
||||
|
||||
/// Get the Temporal server port
|
||||
pub fn get_temporal_port(&self) -> u16 {
|
||||
self.port_config.temporal_port
|
||||
}
|
||||
|
||||
/// Get the HTTP API port
|
||||
pub fn get_http_port(&self) -> u16 {
|
||||
self.port_config.http_port
|
||||
}
|
||||
|
||||
/// Get the Temporal UI port
|
||||
pub fn get_ui_port(&self) -> u16 {
|
||||
self.port_config.ui_port
|
||||
}
|
||||
|
||||
async fn start_go_service(&self) -> Result<(), SchedulerError> {
|
||||
info!("Starting Temporal Go service...");
|
||||
info!(
|
||||
"Starting Temporal Go service on port {}...",
|
||||
self.port_config.http_port
|
||||
);
|
||||
|
||||
// Check if port 8080 is already in use
|
||||
if self.check_port_in_use(8080).await {
|
||||
// Port is in use - check if it's our Go service we can connect to
|
||||
if self.health_check().await.unwrap_or(false) {
|
||||
info!("Port 8080 is in use by a Go service we can connect to");
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(SchedulerError::SchedulerInternalError(
|
||||
"Port 8080 is already in use by something other than our Go service."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
// Check if the service is already running on the discovered port
|
||||
if self.health_check().await.unwrap_or(false) {
|
||||
info!(
|
||||
"Temporal service is already running on port {}",
|
||||
self.port_config.http_port
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Double-check that the port is still free (in case something grabbed it between discovery and start)
|
||||
if !Self::is_port_free(self.port_config.http_port).await {
|
||||
return Err(SchedulerError::SchedulerInternalError(format!(
|
||||
"Port {} is no longer available for Temporal service.",
|
||||
self.port_config.http_port
|
||||
)));
|
||||
}
|
||||
|
||||
// Check if the temporal-service binary exists - try multiple possible locations
|
||||
@@ -103,43 +349,70 @@ impl TemporalScheduler {
|
||||
info!("Found Go service binary at: {}", binary_path);
|
||||
info!("Using working directory: {}", working_dir.display());
|
||||
|
||||
let command = format!(
|
||||
"cd '{}' && nohup ./temporal-service > temporal-service.log 2>&1 & echo $!",
|
||||
working_dir.display()
|
||||
);
|
||||
// Set the PORT environment variable for the service to use and properly daemonize it
|
||||
// Create a new process group to ensure the service survives parent termination
|
||||
let mut command = Command::new("./temporal-service");
|
||||
command
|
||||
.current_dir(working_dir)
|
||||
.env("PORT", self.port_config.http_port.to_string())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.stdin(std::process::Stdio::null());
|
||||
|
||||
let output = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(&command)
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
SchedulerError::SchedulerInternalError(format!(
|
||||
"Failed to start Go temporal service: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(SchedulerError::SchedulerInternalError(format!(
|
||||
"Failed to start Go service: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)));
|
||||
// On Unix systems, create a new process group
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
command.process_group(0);
|
||||
}
|
||||
|
||||
let pid_output = String::from_utf8_lossy(&output.stdout);
|
||||
let pid = pid_output.trim();
|
||||
info!("Temporal Go service started with PID: {}", pid);
|
||||
let child = command.spawn().map_err(|e| {
|
||||
SchedulerError::SchedulerInternalError(format!(
|
||||
"Failed to start Go temporal service: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let pid = child.id();
|
||||
info!(
|
||||
"Temporal Go service started with PID: {} on port {} (detached)",
|
||||
pid, self.port_config.http_port
|
||||
);
|
||||
|
||||
// Don't wait for the child process - let it run independently
|
||||
std::mem::forget(child);
|
||||
|
||||
// Give the process a moment to start up
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Verify the process is still running
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::process::Command as StdCommand;
|
||||
let ps_check = StdCommand::new("ps")
|
||||
.arg("-p")
|
||||
.arg(pid.to_string())
|
||||
.output();
|
||||
|
||||
match ps_check {
|
||||
Ok(output) if output.status.success() => {
|
||||
info!("Confirmed Temporal service process {} is running", pid);
|
||||
}
|
||||
Ok(_) => {
|
||||
warn!(
|
||||
"Temporal service process {} may have exited immediately",
|
||||
pid
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Could not verify Temporal service process status: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_port_in_use(&self, port: u16) -> bool {
|
||||
use std::net::{SocketAddr, TcpListener};
|
||||
|
||||
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
TcpListener::bind(addr).is_err()
|
||||
}
|
||||
|
||||
fn find_go_service_binary() -> Result<String, SchedulerError> {
|
||||
// Try to find the Go service binary by looking for it relative to the current executable
|
||||
// or in common locations
|
||||
@@ -191,27 +464,50 @@ impl TemporalScheduler {
|
||||
info!("Waiting for Temporal service to be ready...");
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut attempt_count = 0;
|
||||
|
||||
while start_time.elapsed() < TEMPORAL_SERVICE_STARTUP_TIMEOUT {
|
||||
attempt_count += 1;
|
||||
match self.health_check().await {
|
||||
Ok(true) => {
|
||||
info!("Temporal service is ready");
|
||||
info!(
|
||||
"Temporal service is ready after {} attempts in {:.2}s",
|
||||
attempt_count,
|
||||
start_time.elapsed().as_secs_f64()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Ok(false) => {
|
||||
// Service responded but not healthy
|
||||
if attempt_count % 10 == 0 {
|
||||
info!(
|
||||
"Waiting for Temporal service... attempt {} ({:.1}s elapsed)",
|
||||
attempt_count,
|
||||
start_time.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
sleep(TEMPORAL_SERVICE_HEALTH_CHECK_INTERVAL).await;
|
||||
}
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
// Service not responding yet
|
||||
if attempt_count % 10 == 0 {
|
||||
info!(
|
||||
"Temporal service not responding yet... attempt {} ({:.1}s elapsed): {}",
|
||||
attempt_count,
|
||||
start_time.elapsed().as_secs_f64(),
|
||||
e
|
||||
);
|
||||
}
|
||||
sleep(TEMPORAL_SERVICE_HEALTH_CHECK_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(SchedulerError::SchedulerInternalError(
|
||||
"Temporal service failed to become ready within timeout".to_string(),
|
||||
))
|
||||
Err(SchedulerError::SchedulerInternalError(format!(
|
||||
"Temporal service failed to become ready within {}s timeout ({} attempts)",
|
||||
TEMPORAL_SERVICE_STARTUP_TIMEOUT.as_secs(),
|
||||
attempt_count
|
||||
)))
|
||||
}
|
||||
|
||||
async fn health_check(&self) -> Result<bool, SchedulerError> {
|
||||
@@ -516,19 +812,19 @@ impl TemporalScheduler {
|
||||
|
||||
format!(
|
||||
"Temporal Services Status:\n\
|
||||
- Go Service ({}:8080): {}\n\
|
||||
- Go Service (localhost:{}): {}\n\
|
||||
- Temporal Server (localhost:{}): Running via Go service\n\
|
||||
- Temporal UI: http://localhost:{}\n\
|
||||
- Service logs: temporal-service/temporal-service.log\n\
|
||||
- Note: Temporal server is managed by the Go service",
|
||||
if go_running {
|
||||
"localhost"
|
||||
} else {
|
||||
"not running"
|
||||
},
|
||||
- Note: All ports are dynamically allocated",
|
||||
self.port_config.http_port,
|
||||
if go_running {
|
||||
"✅ Running"
|
||||
} else {
|
||||
"❌ Not Running"
|
||||
}
|
||||
},
|
||||
self.port_config.temporal_port,
|
||||
self.port_config.ui_port
|
||||
)
|
||||
}
|
||||
|
||||
@@ -745,16 +1041,11 @@ mod tests {
|
||||
|
||||
let rt = Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let scheduler = TemporalScheduler {
|
||||
http_client: reqwest::Client::new(),
|
||||
service_url: "http://localhost:8080".to_string(),
|
||||
};
|
||||
|
||||
// Test with a port that should be available (high port number)
|
||||
let high_port_in_use = scheduler.check_port_in_use(65432).await;
|
||||
let high_port_in_use = !TemporalScheduler::is_port_free(65432).await;
|
||||
|
||||
// Test with a port that might be in use (port 80)
|
||||
let low_port_in_use = scheduler.check_port_in_use(80).await;
|
||||
let low_port_in_use = !TemporalScheduler::is_port_free(80).await;
|
||||
|
||||
println!("✅ Port checking functionality works");
|
||||
println!(" High port (65432) in use: {}", high_port_in_use);
|
||||
@@ -779,4 +1070,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_daemon_process_group_creation() {
|
||||
// Test that the daemon process creation logic compiles and works correctly
|
||||
use std::process::Command;
|
||||
|
||||
// Create a test command similar to what we do in start_go_service
|
||||
let mut command = Command::new("echo");
|
||||
command
|
||||
.arg("test")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.stdin(std::process::Stdio::null());
|
||||
|
||||
// On Unix systems, create a new process group
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
command.process_group(0);
|
||||
}
|
||||
|
||||
// Test that the command can be spawned (but don't actually run it)
|
||||
match command.spawn() {
|
||||
Ok(mut child) => {
|
||||
println!("✅ Daemon process group creation works");
|
||||
// Clean up the test process
|
||||
let _ = child.wait();
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠️ Error spawning test process: {}", e);
|
||||
// This might happen in some test environments, but the logic is correct
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -27,6 +29,59 @@ const (
|
||||
Namespace = "default"
|
||||
)
|
||||
|
||||
// PortConfig holds the port configuration for Temporal services
|
||||
type PortConfig struct {
|
||||
TemporalPort int // Main Temporal server port
|
||||
UIPort int // Temporal UI port
|
||||
HTTPPort int // HTTP API port
|
||||
}
|
||||
|
||||
// findAvailablePort finds an available port starting from the given port
|
||||
func findAvailablePort(startPort int) (int, error) {
|
||||
for port := startPort; port < startPort+100; port++ {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err == nil {
|
||||
ln.Close()
|
||||
return port, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no available port found starting from %d", startPort)
|
||||
}
|
||||
|
||||
// findAvailablePorts finds available ports for all Temporal services
|
||||
func findAvailablePorts() (*PortConfig, error) {
|
||||
// Try to find available ports starting from preferred defaults
|
||||
temporalPort, err := findAvailablePort(7233)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find available port for Temporal server: %w", err)
|
||||
}
|
||||
|
||||
uiPort, err := findAvailablePort(8233)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find available port for Temporal UI: %w", err)
|
||||
}
|
||||
|
||||
// For HTTP port, check environment variable first
|
||||
httpPort := 8080
|
||||
if portEnv := os.Getenv("PORT"); portEnv != "" {
|
||||
if parsed, err := strconv.Atoi(portEnv); err == nil {
|
||||
httpPort = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Verify HTTP port is available, find alternative if not
|
||||
finalHTTPPort, err := findAvailablePort(httpPort)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find available port for HTTP server: %w", err)
|
||||
}
|
||||
|
||||
return &PortConfig{
|
||||
TemporalPort: temporalPort,
|
||||
UIPort: uiPort,
|
||||
HTTPPort: finalHTTPPort,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Global service instance for activities to access
|
||||
var globalService *TemporalService
|
||||
|
||||
@@ -61,16 +116,16 @@ type RunNowResponse struct {
|
||||
}
|
||||
|
||||
// ensureTemporalServerRunning checks if Temporal server is running and starts it if needed
|
||||
func ensureTemporalServerRunning() error {
|
||||
func ensureTemporalServerRunning(ports *PortConfig) error {
|
||||
log.Println("Checking if Temporal server is running...")
|
||||
|
||||
// Check if Temporal server is already running by trying to connect
|
||||
if isTemporalServerRunning() {
|
||||
log.Println("Temporal server is already running")
|
||||
if isTemporalServerRunning(ports.TemporalPort) {
|
||||
log.Printf("Temporal server is already running on port %d", ports.TemporalPort)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("Temporal server not running, attempting to start it...")
|
||||
log.Printf("Temporal server not running, attempting to start it on port %d...", ports.TemporalPort)
|
||||
|
||||
// Find the temporal CLI binary
|
||||
temporalCmd, err := findTemporalCLI()
|
||||
@@ -83,8 +138,8 @@ func ensureTemporalServerRunning() error {
|
||||
// Start Temporal server in background
|
||||
cmd := exec.Command(temporalCmd, "server", "start-dev",
|
||||
"--db-filename", "temporal.db",
|
||||
"--port", "7233",
|
||||
"--ui-port", "8233",
|
||||
"--port", strconv.Itoa(ports.TemporalPort),
|
||||
"--ui-port", strconv.Itoa(ports.UIPort),
|
||||
"--log-level", "warn")
|
||||
|
||||
// Start the process in background
|
||||
@@ -92,7 +147,8 @@ func ensureTemporalServerRunning() error {
|
||||
return fmt.Errorf("failed to start Temporal server: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Temporal server started with PID: %d", cmd.Process.Pid)
|
||||
log.Printf("Temporal server started with PID: %d (port: %d, UI port: %d)",
|
||||
cmd.Process.Pid, ports.TemporalPort, ports.UIPort)
|
||||
|
||||
// Wait for server to be ready (with timeout)
|
||||
timeout := time.After(30 * time.Second)
|
||||
@@ -104,8 +160,8 @@ func ensureTemporalServerRunning() error {
|
||||
case <-timeout:
|
||||
return fmt.Errorf("timeout waiting for Temporal server to start")
|
||||
case <-ticker.C:
|
||||
if isTemporalServerRunning() {
|
||||
log.Println("Temporal server is now ready")
|
||||
if isTemporalServerRunning(ports.TemporalPort) {
|
||||
log.Printf("Temporal server is now ready on port %d", ports.TemporalPort)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -113,10 +169,10 @@ func ensureTemporalServerRunning() error {
|
||||
}
|
||||
|
||||
// isTemporalServerRunning checks if Temporal server is accessible
|
||||
func isTemporalServerRunning() bool {
|
||||
func isTemporalServerRunning(port int) bool {
|
||||
// Try to create a client connection to check if server is running
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: "127.0.0.1:7233",
|
||||
HostPort: fmt.Sprintf("127.0.0.1:%d", port),
|
||||
Namespace: Namespace,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -143,6 +199,19 @@ func findTemporalCLI() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Try using 'which' command to find temporal
|
||||
cmd := exec.Command("which", "temporal")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
path := strings.TrimSpace(string(output))
|
||||
if path != "" {
|
||||
// Verify it's the correct temporal CLI by checking version
|
||||
cmd := exec.Command(path, "--version")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found in PATH, try different possible locations for the temporal CLI
|
||||
possiblePaths := []string{
|
||||
"./temporal", // Current directory
|
||||
@@ -180,18 +249,28 @@ type TemporalService struct {
|
||||
worker worker.Worker
|
||||
scheduleJobs map[string]*JobStatus // In-memory job tracking
|
||||
runningJobs map[string]bool // Track which jobs are currently running
|
||||
ports *PortConfig // Port configuration
|
||||
}
|
||||
|
||||
// NewTemporalService creates a new Temporal service and ensures Temporal server is running
|
||||
func NewTemporalService() (*TemporalService, error) {
|
||||
// First, ensure Temporal server is running
|
||||
if err := ensureTemporalServerRunning(); err != nil {
|
||||
// First, find available ports
|
||||
ports, err := findAvailablePorts()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find available ports: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Using ports - Temporal: %d, UI: %d, HTTP: %d",
|
||||
ports.TemporalPort, ports.UIPort, ports.HTTPPort)
|
||||
|
||||
// Ensure Temporal server is running
|
||||
if err := ensureTemporalServerRunning(ports); err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure Temporal server is running: %w", err)
|
||||
}
|
||||
|
||||
// Create client (Temporal server should now be running)
|
||||
c, err := client.Dial(client.Options{
|
||||
HostPort: "127.0.0.1:7233",
|
||||
HostPort: fmt.Sprintf("127.0.0.1:%d", ports.TemporalPort),
|
||||
Namespace: Namespace,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -208,13 +287,14 @@ func NewTemporalService() (*TemporalService, error) {
|
||||
return nil, fmt.Errorf("failed to start worker: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Connected to Temporal server successfully")
|
||||
log.Printf("Connected to Temporal server successfully on port %d", ports.TemporalPort)
|
||||
|
||||
service := &TemporalService{
|
||||
client: c,
|
||||
worker: w,
|
||||
scheduleJobs: make(map[string]*JobStatus),
|
||||
runningJobs: make(map[string]bool),
|
||||
ports: ports,
|
||||
}
|
||||
|
||||
// Set global service for activities
|
||||
@@ -235,6 +315,21 @@ func (ts *TemporalService) Stop() {
|
||||
log.Println("Temporal service stopped")
|
||||
}
|
||||
|
||||
// GetHTTPPort returns the HTTP port for this service
|
||||
func (ts *TemporalService) GetHTTPPort() int {
|
||||
return ts.ports.HTTPPort
|
||||
}
|
||||
|
||||
// GetTemporalPort returns the Temporal server port for this service
|
||||
func (ts *TemporalService) GetTemporalPort() int {
|
||||
return ts.ports.TemporalPort
|
||||
}
|
||||
|
||||
// GetUIPort returns the Temporal UI port for this service
|
||||
func (ts *TemporalService) GetUIPort() int {
|
||||
return ts.ports.UIPort
|
||||
}
|
||||
|
||||
// Workflow definition for executing Goose recipes
|
||||
func GooseJobWorkflow(ctx workflow.Context, jobID, recipePath string) (string, error) {
|
||||
logger := workflow.GetLogger(ctx)
|
||||
@@ -610,29 +705,45 @@ func (ts *TemporalService) handleHealth(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
|
||||
}
|
||||
|
||||
func main() {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
// handlePorts returns the port configuration for this service
|
||||
func (ts *TemporalService) handlePorts(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
portInfo := map[string]int{
|
||||
"http_port": ts.ports.HTTPPort,
|
||||
"temporal_port": ts.ports.TemporalPort,
|
||||
"ui_port": ts.ports.UIPort,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(portInfo)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("Starting Temporal service...")
|
||||
log.Println("Note: This service requires a running Temporal server at 127.0.0.1:7233")
|
||||
log.Println("Start Temporal server with: temporal server start-dev")
|
||||
|
||||
// Create Temporal service
|
||||
// Create Temporal service (this will find available ports automatically)
|
||||
service, err := NewTemporalService()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Temporal service: %v", err)
|
||||
}
|
||||
|
||||
// Use the dynamically assigned HTTP port
|
||||
httpPort := service.GetHTTPPort()
|
||||
temporalPort := service.GetTemporalPort()
|
||||
uiPort := service.GetUIPort()
|
||||
|
||||
log.Printf("Temporal server running on port %d", temporalPort)
|
||||
log.Printf("Temporal UI available at http://localhost:%d", uiPort)
|
||||
|
||||
// Set up HTTP server
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/jobs", service.handleJobs)
|
||||
mux.HandleFunc("/health", service.handleHealth)
|
||||
mux.HandleFunc("/ports", service.handlePorts)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Addr: fmt.Sprintf(":%d", httpPort),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
@@ -655,9 +766,10 @@ func main() {
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
log.Printf("Temporal service starting on port %s", port)
|
||||
log.Printf("Health endpoint: http://localhost:%s/health", port)
|
||||
log.Printf("Jobs endpoint: http://localhost:%s/jobs", port)
|
||||
log.Printf("Temporal service starting on port %d", httpPort)
|
||||
log.Printf("Health endpoint: http://localhost:%d/health", httpPort)
|
||||
log.Printf("Jobs endpoint: http://localhost:%d/jobs", httpPort)
|
||||
log.Printf("Ports endpoint: http://localhost:%d/ports", httpPort)
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("HTTP server failed: %v", err)
|
||||
|
||||
Binary file not shown.
@@ -26,7 +26,7 @@ export const findAvailablePort = (): Promise<number> => {
|
||||
// Check if goosed server is ready by polling the status endpoint
|
||||
const checkServerStatus = async (
|
||||
port: number,
|
||||
maxAttempts: number = 60,
|
||||
maxAttempts: number = 80,
|
||||
interval: number = 100
|
||||
): Promise<boolean> => {
|
||||
const statusUrl = `http://127.0.0.1:${port}/status`;
|
||||
|
||||
Reference in New Issue
Block a user