2
0

kvmanager.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package kvmanager
  2. import (
  3. "fmt"
  4. "net"
  5. "os"
  6. "os/exec"
  7. "strconv"
  8. "strings"
  9. "syscall"
  10. "time"
  11. pizzaruntime "github.com/danfragoso/pizzasql-next/pkg/runtime"
  12. )
  13. // KVInfo is an alias for the runtime package type.
  14. type KVInfo = pizzaruntime.KVInfo
  15. // Manager handles the lifecycle of a PizzaKV process
  16. type Manager struct {
  17. cmd *exec.Cmd
  18. info *KVInfo
  19. }
  20. // NewManager creates a new KVManager
  21. func NewManager() *Manager {
  22. return &Manager{}
  23. }
  24. // Start launches pizzakv with the given flags using a Unix socket by default.
  25. func (m *Manager) Start(kvFlags string) (*KVInfo, error) {
  26. sockPath := ".pizzakv.sock"
  27. args := []string{"-unix"}
  28. // Parse and add custom flags if provided
  29. if kvFlags != "" {
  30. customArgs := parseFlags(kvFlags)
  31. args = append(args, customArgs...)
  32. }
  33. // Create the command
  34. cmd := exec.Command("pizzakv", args...)
  35. // Set up process group to allow clean shutdown
  36. cmd.SysProcAttr = &syscall.SysProcAttr{
  37. Setpgid: true,
  38. }
  39. // Redirect output to /dev/null or capture it
  40. cmd.Stdout = os.Stdout
  41. cmd.Stderr = os.Stderr
  42. // Start the process
  43. if err := cmd.Start(); err != nil {
  44. return nil, fmt.Errorf("failed to start pizzakv: %w", err)
  45. }
  46. m.cmd = cmd
  47. m.info = &KVInfo{
  48. PID: cmd.Process.Pid,
  49. Addr: "unix:" + sockPath,
  50. }
  51. time.Sleep(500 * time.Millisecond)
  52. if !m.IsRunning() {
  53. return nil, fmt.Errorf("pizzakv process exited immediately after starting")
  54. }
  55. fmt.Println("Waiting for PizzaKV to be ready...")
  56. if err := m.waitForReady(m.info.Addr, 30*time.Second); err != nil {
  57. m.Stop()
  58. return nil, fmt.Errorf("pizzakv did not become ready: %w", err)
  59. }
  60. if err := pizzaruntime.WriteKV(m.info); err != nil {
  61. m.Stop()
  62. return nil, fmt.Errorf("failed to write runtime file: %w", err)
  63. }
  64. return m.info, nil
  65. }
  66. // Stop stops the pizzakv process
  67. func (m *Manager) Stop() error {
  68. if m.cmd == nil || m.cmd.Process == nil {
  69. return nil
  70. }
  71. if err := m.cmd.Process.Signal(syscall.SIGTERM); err != nil {
  72. if err := m.cmd.Process.Kill(); err != nil {
  73. return fmt.Errorf("failed to kill process: %w", err)
  74. }
  75. }
  76. done := make(chan error, 1)
  77. go func() {
  78. _, err := m.cmd.Process.Wait()
  79. done <- err
  80. }()
  81. select {
  82. case <-done:
  83. case <-time.After(5 * time.Second):
  84. m.cmd.Process.Kill()
  85. }
  86. return nil
  87. }
  88. // IsRunning checks if the pizzakv process is still running
  89. func (m *Manager) IsRunning() bool {
  90. if m.cmd == nil || m.cmd.Process == nil {
  91. return false
  92. }
  93. // Send signal 0 to check if process exists
  94. err := m.cmd.Process.Signal(syscall.Signal(0))
  95. return err == nil
  96. }
  97. // waitForReady waits for PizzaKV to be ready to accept connections.
  98. func (m *Manager) waitForReady(addr string, timeout time.Duration) error {
  99. network, target := parseKVAddr(addr)
  100. deadline := time.Now().Add(timeout)
  101. for time.Now().Before(deadline) {
  102. if !m.IsRunning() {
  103. return fmt.Errorf("process died while waiting for ready")
  104. }
  105. conn, err := net.DialTimeout(network, target, 500*time.Millisecond)
  106. if err == nil {
  107. conn.Close()
  108. return nil
  109. }
  110. time.Sleep(100 * time.Millisecond)
  111. }
  112. return fmt.Errorf("timeout waiting for PizzaKV to become ready at %s", addr)
  113. }
  114. func parseKVAddr(addr string) (network, target string) {
  115. if strings.HasPrefix(addr, "unix:") {
  116. return "unix", strings.TrimPrefix(addr, "unix:")
  117. }
  118. return "tcp", addr
  119. }
  120. // GetInfo returns the KVInfo for the running instance
  121. func (m *Manager) GetInfo() *KVInfo {
  122. return m.info
  123. }
  124. // LoadInfo loads KVInfo from the runtime file
  125. func (m *Manager) LoadInfo() (*KVInfo, error) {
  126. info, err := pizzaruntime.Load()
  127. if err != nil {
  128. return nil, err
  129. }
  130. if info.PizzaKV == nil {
  131. return nil, fmt.Errorf("no pizzakv info in runtime file")
  132. }
  133. return info.PizzaKV, nil
  134. }
  135. // parseFlags parses a flag string like "-iwal -port=9090" into a slice of strings
  136. func parseFlags(flags string) []string {
  137. // Trim whitespace
  138. flags = strings.TrimSpace(flags)
  139. if flags == "" {
  140. return nil
  141. }
  142. var result []string
  143. var current strings.Builder
  144. inQuote := false
  145. for i, r := range flags {
  146. switch r {
  147. case '"', '\'':
  148. inQuote = !inQuote
  149. case ' ':
  150. if !inQuote {
  151. if current.Len() > 0 {
  152. result = append(result, current.String())
  153. current.Reset()
  154. }
  155. } else {
  156. current.WriteRune(r)
  157. }
  158. default:
  159. current.WriteRune(r)
  160. }
  161. // Handle last character
  162. if i == len(flags)-1 && current.Len() > 0 {
  163. result = append(result, current.String())
  164. }
  165. }
  166. return result
  167. }
  168. // ParsePort parses a port from a string (e.g., "localhost:8085" -> 8085)
  169. func ParsePort(addr string) (int, error) {
  170. parts := strings.Split(addr, ":")
  171. if len(parts) != 2 {
  172. return 0, fmt.Errorf("invalid address format: %s", addr)
  173. }
  174. port, err := strconv.Atoi(parts[1])
  175. if err != nil {
  176. return 0, fmt.Errorf("invalid port: %s", parts[1])
  177. }
  178. return port, nil
  179. }