44 "errors"
55 "fmt"
66 "log"
7+ "runtime"
78 "sort"
89 "strconv"
910 "strings"
@@ -17,6 +18,13 @@ const (
1718 StatusStopped = "stopped"
1819)
1920
21+ var settingsTunnelRetryBackoff = []time.Duration {
22+ 2 * time .Second ,
23+ 5 * time .Second ,
24+ 10 * time .Second ,
25+ 30 * time .Second ,
26+ }
27+
2028type ConfigRecord struct {
2129 ID string `json:"id"`
2230 Name string `json:"name"`
@@ -63,6 +71,7 @@ type Manager struct {
6371 appTunnelEnabled bool
6472 defaultEngine string
6573 tunnels map [string ]* managedTunnel
74+ retryBackoff []time.Duration
6675}
6776
6877func NewManager (store Store , appPort string , appTunnelEnabled bool , defaultEngine string ) * Manager {
@@ -83,6 +92,7 @@ func NewManagerWithStarter(store Store, appPort string, appTunnelEnabled bool, d
8392 appTunnelEnabled : appTunnelEnabled ,
8493 defaultEngine : engine ,
8594 tunnels : make (map [string ]* managedTunnel ),
95+ retryBackoff : settingsTunnelRetryBackoff ,
8696 }
8797}
8898
@@ -393,8 +403,16 @@ func (m *Manager) startAsync(id string) {
393403 _ = runtimeToStop .Stop ()
394404 }
395405
396- go func () {
406+ go m .startLoop (id , seq , cfg )
407+ }
408+
409+ func (m * Manager ) startLoop (id string , seq uint64 , cfg ConfigRecord ) {
410+ attempt := 0
411+ for {
397412 runtime , err := m .startTunnel (cfg .Engine , cfg .LocalPort )
413+ if err == nil && runtime == nil {
414+ err = errors .New ("tunnel starter returned nil runtime" )
415+ }
398416
399417 m .mu .Lock ()
400418 current , ok := m .tunnels [id ]
@@ -405,19 +423,59 @@ func (m *Manager) startAsync(id string) {
405423 }
406424 return
407425 }
408- if err != nil {
409- current .status = StatusStopped
410- current .lastError = err .Error ()
426+ if err == nil {
427+ current .runtime = runtime
428+ current .status = StatusStarted
429+ current .lastError = ""
411430 m .mu .Unlock ()
412- log .Printf ("tunnel: failed to start %s on port %s: %v" , current .config .Name , current .config .LocalPort , err )
413431 return
414432 }
415433
416- current .runtime = runtime
417- current .status = StatusStarted
418- current .lastError = ""
434+ current .runtime = nil
435+ current .status = StatusStarting
436+ current .lastError = err .Error ()
437+ name := current .config .Name
438+ port := current .config .LocalPort
439+ delay := retryDelay (m .retryBackoff , attempt )
440+ attempt ++
419441 m .mu .Unlock ()
420- }()
442+
443+ if runtime != nil {
444+ _ = runtime .Stop ()
445+ }
446+ log .Printf ("tunnel: failed to start %s on port %s: %v; retrying in %s" , name , port , err , delay )
447+ if ! m .waitForRetry (id , seq , delay ) {
448+ return
449+ }
450+ }
451+ }
452+
453+ func (m * Manager ) waitForRetry (id string , seq uint64 , delay time.Duration ) bool {
454+ if delay <= 0 {
455+ runtime .Gosched ()
456+ return m .shouldContinueStart (id , seq )
457+ }
458+ timer := time .NewTimer (delay )
459+ defer timer .Stop ()
460+ <- timer .C
461+ return m .shouldContinueStart (id , seq )
462+ }
463+
464+ func (m * Manager ) shouldContinueStart (id string , seq uint64 ) bool {
465+ m .mu .RLock ()
466+ defer m .mu .RUnlock ()
467+ current , ok := m .tunnels [id ]
468+ return ok && current .launchSeq == seq && current .config .Enabled
469+ }
470+
471+ func retryDelay (backoff []time.Duration , attempt int ) time.Duration {
472+ if len (backoff ) == 0 {
473+ return time .Second
474+ }
475+ if attempt < len (backoff ) {
476+ return backoff [attempt ]
477+ }
478+ return backoff [len (backoff )- 1 ]
421479}
422480
423481func (m * Manager ) snapshotPortsLocked (excludeID string ) map [string ]string {
0 commit comments