manager.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. // Package runtime owns the long-lived VoWiFi orchestrators used by the
  2. // service. It keeps HTTP requests short while preserving every evidence-backed
  3. // state transition through the supplied state callback.
  4. package runtime
  5. import (
  6. "context"
  7. "errors"
  8. "fmt"
  9. "log/slog"
  10. "sync"
  11. "time"
  12. "vocat/internal/vowifi"
  13. )
  14. var (
  15. ErrNotRegistered = errors.New("vowifi runtime: device is not registered")
  16. ErrOperationInProgress = errors.New("vowifi runtime: an operation is already in progress")
  17. ErrClosed = errors.New("vowifi runtime: manager is closed")
  18. )
  19. const defaultOperationTimeout = 2 * time.Minute
  20. type StateHandler func(context.Context, vowifi.State) error
  21. type OrchestratorFactory func(context.Context, string) (*vowifi.Orchestrator, error)
  22. type Options struct {
  23. Logger *slog.Logger
  24. OperationTimeout time.Duration
  25. OnState StateHandler
  26. Factory OrchestratorFactory
  27. }
  28. type Manager struct {
  29. ctx context.Context
  30. cancel context.CancelFunc
  31. logger *slog.Logger
  32. operationTimeout time.Duration
  33. onState StateHandler
  34. factory OrchestratorFactory
  35. mu sync.Mutex
  36. closed bool
  37. entries map[string]*entry
  38. wg sync.WaitGroup
  39. }
  40. type entry struct {
  41. orchestrator *vowifi.Orchestrator
  42. busy bool
  43. reconnectPending bool
  44. stopWatch func()
  45. }
  46. func New(options Options) *Manager {
  47. if options.Logger == nil {
  48. options.Logger = slog.Default()
  49. }
  50. if options.OperationTimeout <= 0 {
  51. options.OperationTimeout = defaultOperationTimeout
  52. }
  53. ctx, cancel := context.WithCancel(context.Background())
  54. return &Manager{
  55. ctx: ctx,
  56. cancel: cancel,
  57. logger: options.Logger,
  58. operationTimeout: options.OperationTimeout,
  59. onState: options.OnState,
  60. factory: options.Factory,
  61. entries: make(map[string]*entry),
  62. }
  63. }
  64. // Ensure registers a runtime for deviceID on demand. This keeps device
  65. // configuration and runtime lifecycle in sync when a modem is added after the
  66. // service has already started.
  67. func (manager *Manager) Ensure(ctx context.Context, deviceID string) error {
  68. if ctx == nil {
  69. ctx = context.Background()
  70. }
  71. manager.mu.Lock()
  72. if manager.closed {
  73. manager.mu.Unlock()
  74. return ErrClosed
  75. }
  76. if _, exists := manager.entries[deviceID]; exists {
  77. manager.mu.Unlock()
  78. return nil
  79. }
  80. if manager.factory == nil {
  81. manager.mu.Unlock()
  82. return ErrNotRegistered
  83. }
  84. // The factory is called while holding the manager lock so concurrent status
  85. // and enable requests cannot create duplicate runtimes for the same device.
  86. orchestrator, err := manager.factory(ctx, deviceID)
  87. if err != nil {
  88. manager.mu.Unlock()
  89. return err
  90. }
  91. if orchestrator == nil {
  92. manager.mu.Unlock()
  93. return errors.New("vowifi runtime: factory returned a nil orchestrator")
  94. }
  95. state := orchestrator.State()
  96. if state.DeviceID != deviceID {
  97. manager.mu.Unlock()
  98. _ = orchestrator.Close(context.Background())
  99. return fmt.Errorf(
  100. "vowifi runtime: factory returned device %q for %q",
  101. state.DeviceID,
  102. deviceID,
  103. )
  104. }
  105. states, stopWatch := orchestrator.Subscribe(8)
  106. manager.entries[deviceID] = &entry{
  107. orchestrator: orchestrator,
  108. stopWatch: stopWatch,
  109. }
  110. manager.wg.Add(1)
  111. manager.mu.Unlock()
  112. go manager.watch(deviceID, states)
  113. return nil
  114. }
  115. func (manager *Manager) Register(orchestrator *vowifi.Orchestrator) error {
  116. if orchestrator == nil {
  117. return errors.New("vowifi runtime: orchestrator is nil")
  118. }
  119. state := orchestrator.State()
  120. if state.DeviceID == "" {
  121. return errors.New("vowifi runtime: orchestrator device ID is empty")
  122. }
  123. manager.mu.Lock()
  124. if manager.closed {
  125. manager.mu.Unlock()
  126. return ErrClosed
  127. }
  128. if _, exists := manager.entries[state.DeviceID]; exists {
  129. manager.mu.Unlock()
  130. return fmt.Errorf("vowifi runtime: device %q is already registered", state.DeviceID)
  131. }
  132. states, stopWatch := orchestrator.Subscribe(8)
  133. item := &entry{
  134. orchestrator: orchestrator,
  135. stopWatch: stopWatch,
  136. }
  137. manager.entries[state.DeviceID] = item
  138. manager.wg.Add(1)
  139. manager.mu.Unlock()
  140. go manager.watch(state.DeviceID, states)
  141. return nil
  142. }
  143. func (manager *Manager) State(deviceID string) (vowifi.State, error) {
  144. manager.mu.Lock()
  145. item := manager.entries[deviceID]
  146. closed := manager.closed
  147. manager.mu.Unlock()
  148. if item == nil {
  149. if closed {
  150. return vowifi.State{}, ErrClosed
  151. }
  152. if err := manager.Ensure(manager.ctx, deviceID); err != nil {
  153. return vowifi.State{}, err
  154. }
  155. manager.mu.Lock()
  156. item = manager.entries[deviceID]
  157. manager.mu.Unlock()
  158. }
  159. return item.orchestrator.State(), nil
  160. }
  161. // RequestEnabled queues an enable or disable transaction and returns
  162. // immediately. Callers observe progress through State; provider errors are
  163. // persisted in the orchestrator state instead of being lost with an HTTP
  164. // request context.
  165. func (manager *Manager) RequestEnabled(deviceID string, enabled bool) (vowifi.State, error) {
  166. if err := manager.Ensure(manager.ctx, deviceID); err != nil {
  167. return vowifi.State{}, err
  168. }
  169. return manager.startOperation(deviceID, false, func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
  170. if enabled {
  171. _, err := orchestrator.Enable(ctx)
  172. return err
  173. }
  174. _, err := orchestrator.Disable(ctx)
  175. return err
  176. })
  177. }
  178. func (manager *Manager) RequestReconnect(deviceID string) (vowifi.State, error) {
  179. if err := manager.Ensure(manager.ctx, deviceID); err != nil {
  180. return vowifi.State{}, err
  181. }
  182. return manager.startOperation(deviceID, true, func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
  183. _, err := orchestrator.Reconnect(ctx)
  184. return err
  185. })
  186. }
  187. func (manager *Manager) SendSMS(
  188. ctx context.Context,
  189. deviceID string,
  190. request vowifi.SMSSubmitRequest,
  191. ) (vowifi.SMSSubmitResult, error) {
  192. if err := manager.Ensure(ctx, deviceID); err != nil {
  193. return vowifi.SMSSubmitResult{}, err
  194. }
  195. manager.mu.Lock()
  196. if manager.closed {
  197. manager.mu.Unlock()
  198. return vowifi.SMSSubmitResult{}, ErrClosed
  199. }
  200. item := manager.entries[deviceID]
  201. manager.mu.Unlock()
  202. if item == nil {
  203. return vowifi.SMSSubmitResult{}, ErrNotRegistered
  204. }
  205. return item.orchestrator.SendSMS(ctx, request)
  206. }
  207. func (manager *Manager) startOperation(
  208. deviceID string,
  209. coalesceReconnect bool,
  210. operation func(context.Context, *vowifi.Orchestrator) error,
  211. ) (vowifi.State, error) {
  212. manager.mu.Lock()
  213. if manager.closed {
  214. manager.mu.Unlock()
  215. return vowifi.State{}, ErrClosed
  216. }
  217. item := manager.entries[deviceID]
  218. if item == nil {
  219. manager.mu.Unlock()
  220. return vowifi.State{}, ErrNotRegistered
  221. }
  222. if item.busy {
  223. state := item.orchestrator.State()
  224. if coalesceReconnect {
  225. // Route changes and repeated reconnect clicks only need the latest
  226. // result. Keep one pending reconnect behind the active lifecycle
  227. // operation instead of rejecting the request or running two modem/
  228. // tunnel transactions concurrently.
  229. item.reconnectPending = true
  230. manager.mu.Unlock()
  231. return state, nil
  232. }
  233. manager.mu.Unlock()
  234. return state, ErrOperationInProgress
  235. }
  236. item.busy = true
  237. manager.wg.Add(1)
  238. manager.mu.Unlock()
  239. go manager.runOperations(deviceID, item, operation)
  240. return item.orchestrator.State(), nil
  241. }
  242. func (manager *Manager) runOperations(
  243. deviceID string,
  244. item *entry,
  245. operation func(context.Context, *vowifi.Orchestrator) error,
  246. ) {
  247. defer manager.wg.Done()
  248. for {
  249. ctx, cancel := context.WithTimeout(manager.ctx, manager.operationTimeout)
  250. err := operation(ctx, item.orchestrator)
  251. cancel()
  252. if err != nil &&
  253. !errors.Is(err, context.Canceled) &&
  254. !errors.Is(err, vowifi.ErrAlreadyEnabled) {
  255. manager.logger.Warn(
  256. "VoWiFi operation failed",
  257. "device_id", deviceID,
  258. "error", err,
  259. )
  260. }
  261. manager.mu.Lock()
  262. if manager.closed || !item.reconnectPending {
  263. item.busy = false
  264. manager.mu.Unlock()
  265. return
  266. }
  267. item.reconnectPending = false
  268. manager.mu.Unlock()
  269. // Read the route only when this runs. If the user bound, unbound, then
  270. // rebound while busy, the single reconnect uses the final persisted
  271. // binding instead of replaying stale intermediate routes.
  272. operation = func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
  273. _, err := orchestrator.Reconnect(ctx)
  274. return err
  275. }
  276. }
  277. }
  278. func (manager *Manager) watch(deviceID string, states <-chan vowifi.State) {
  279. defer manager.wg.Done()
  280. for {
  281. select {
  282. case <-manager.ctx.Done():
  283. return
  284. case state, ok := <-states:
  285. if !ok {
  286. return
  287. }
  288. if manager.onState == nil {
  289. continue
  290. }
  291. ctx, cancel := context.WithTimeout(manager.ctx, 5*time.Second)
  292. err := manager.onState(ctx, state)
  293. cancel()
  294. if err != nil && !errors.Is(err, context.Canceled) {
  295. manager.logger.Error(
  296. "persist VoWiFi state",
  297. "device_id", deviceID,
  298. "phase", state.Phase,
  299. "error", err,
  300. )
  301. }
  302. }
  303. }
  304. }
  305. func (manager *Manager) Close(ctx context.Context) error {
  306. if ctx == nil {
  307. ctx = context.Background()
  308. }
  309. manager.mu.Lock()
  310. if manager.closed {
  311. manager.mu.Unlock()
  312. return nil
  313. }
  314. manager.closed = true
  315. manager.cancel()
  316. items := make([]*entry, 0, len(manager.entries))
  317. for _, item := range manager.entries {
  318. items = append(items, item)
  319. }
  320. manager.mu.Unlock()
  321. var closeErrors []error
  322. for _, item := range items {
  323. if item.stopWatch != nil {
  324. item.stopWatch()
  325. }
  326. if err := item.orchestrator.Close(ctx); err != nil {
  327. closeErrors = append(closeErrors, err)
  328. }
  329. }
  330. done := make(chan struct{})
  331. go func() {
  332. manager.wg.Wait()
  333. close(done)
  334. }()
  335. select {
  336. case <-ctx.Done():
  337. closeErrors = append(closeErrors, ctx.Err())
  338. case <-done:
  339. }
  340. return errors.Join(closeErrors...)
  341. }