detached 9fac95c

fix: Docker container event stream and monitoring

- Add timeout context for Docker event streams to prevent stale connections
- Implement max consecutive failures tracking with warning logs
- Add periodic event stream health checks and automatic reconnection
- Enhance error handling and backoff strategy for event stream failures
- Improve logging for connection status and reconnection attempts
commit
9fac95c17204682c401d35b29eddd7801c02c77f
author
bamen · · 2025-03-10 16:10 UTC
parent
086e0df
signature
unsigned
1 file · +43 -3
M pkg/docker/containers.go +43 -3
@@ -758,6 +758,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
758758 maxDelay := 1 * time.Minute
759759 currentDelay := initialDelay
760760 consecutiveFailures := 0
761+ maxConsecutiveFailures := 10
761762
762763 for {
763764 // Check if context was canceled (application is shutting down)
@@ -765,11 +766,15 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
765766 return
766767 }
767768
769+ // Add a timeout context for the Events call
770+ // This helps prevent long-running connections that might become stale
771+ eventCtx, eventCancel := context.WithTimeout(ctx, 5*time.Minute)
772+
768773 // Set up filters for the events we're interested in
769774 filters := makeEventFilters(networkFilter)
770775
771- // Start listening for events
772- messages, errs := dockerCli.Events(ctx, events.ListOptions{
776+ // Start listening for events with timeout context
777+ messages, errs := dockerCli.Events(eventCtx, events.ListOptions{
773778 Filters: filters,
774779 })
775780
@@ -777,6 +782,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
777782
778783 // Process events in an inner loop
779784 listenSuccess := false
785+ lastEventTime := time.Now()
780786 eventStreamLoop:
781787 for {
782788 select {
@@ -784,15 +790,26 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
784790 if err != nil && ctx.Err() == nil {
785791 // Only log if this isn't due to context cancellation
786792 log.Error("Error in container event stream", "error", err)
793+
794+ // Cancel the event context to clean up resources
795+ eventCancel()
796+
787797 // Increment failure counter for backoff calculation
788798 consecutiveFailures++
789799
800+ // If we have too many consecutive failures, log a warning
801+ if consecutiveFailures >= maxConsecutiveFailures {
802+ log.Warn("Multiple consecutive failures in container event stream",
803+ "count", consecutiveFailures,
804+ "will_continue_trying", true)
805+ }
806+
790807 // Calculate backoff delay with exponential increase
791808 if listenSuccess {
792809 // If we had successful events before failure, reset the delay
793810 currentDelay = initialDelay
794811 } else {
795- // Exponential backoff: double the delay each time, up to max
812+ // Exponential backoff: increase the delay each time, up to max
796813 currentDelay = time.Duration(float64(currentDelay) * 1.5)
797814 if currentDelay > maxDelay {
798815 currentDelay = maxDelay
@@ -808,6 +825,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
808825 }
809826 // Context was canceled, exit completely
810827 if ctx.Err() != nil {
828+ eventCancel()
811829 return
812830 }
813831 case event := <-messages:
@@ -819,6 +837,9 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
819837 log.Info("Successfully receiving container events")
820838 }
821839
840+ // Update last event time
841+ lastEventTime = time.Now()
842+
822843 // Process container events
823844 if event.Type == "container" {
824845 containerID := event.Actor.ID
@@ -870,12 +891,31 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
870891 // Add other event types as needed
871892 }
872893 }
894+ case <-time.After(30 * time.Second):
895+ // If we haven't received events for a while, check if the connection is still alive
896+ timeSinceLastEvent := time.Since(lastEventTime)
897+
898+ // If it's been more than 2 minutes since the last event, reconnect
899+ if timeSinceLastEvent > 2*time.Minute {
900+ log.Debug("No events received for too long, reconnecting",
901+ "time_since_last_event", timeSinceLastEvent)
902+
903+ // Cancel the event context to clean up resources
904+ eventCancel()
905+
906+ // Break out of the inner event loop to reconnect
907+ break eventStreamLoop
908+ }
873909 case <-ctx.Done():
910+ eventCancel()
874911 return
875912 }
876913 }
877914
878915 // If we get here, the event stream failed and we need to reconnect
916+ // Clean up the event context
917+ eventCancel()
918+
879919 // Wait for the calculated delay before reconnecting
880920 select {
881921 case <-time.After(currentDelay):
@@ -758,6 +758,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
758 maxDelay := 1 * time.Minute758 maxDelay := 1 * time.Minute
759 currentDelay := initialDelay759 currentDelay := initialDelay
760 consecutiveFailures := 0760 consecutiveFailures := 0
761+ maxConsecutiveFailures := 10
761 762
762 for {763 for {
763 // Check if context was canceled (application is shutting down)764 // Check if context was canceled (application is shutting down)
@@ -765,11 +766,15 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
765 return766 return
766 }767 }
767 768
769+ // Add a timeout context for the Events call
770+ // This helps prevent long-running connections that might become stale
771+ eventCtx, eventCancel := context.WithTimeout(ctx, 5*time.Minute)
772+
768 // Set up filters for the events we're interested in773 // Set up filters for the events we're interested in
769 filters := makeEventFilters(networkFilter)774 filters := makeEventFilters(networkFilter)
770 775
771- // Start listening for events776+ // Start listening for events with timeout context
772- messages, errs := dockerCli.Events(ctx, events.ListOptions{777+ messages, errs := dockerCli.Events(eventCtx, events.ListOptions{
773 Filters: filters,778 Filters: filters,
774 })779 })
775 780
@@ -777,6 +782,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
777 782
778 // Process events in an inner loop783 // Process events in an inner loop
779 listenSuccess := false784 listenSuccess := false
785+ lastEventTime := time.Now()
780 eventStreamLoop:786 eventStreamLoop:
781 for {787 for {
782 select {788 select {
@@ -784,15 +790,26 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
784 if err != nil && ctx.Err() == nil {790 if err != nil && ctx.Err() == nil {
785 // Only log if this isn't due to context cancellation791 // Only log if this isn't due to context cancellation
786 log.Error("Error in container event stream", "error", err)792 log.Error("Error in container event stream", "error", err)
793+
794+ // Cancel the event context to clean up resources
795+ eventCancel()
796+
787 // Increment failure counter for backoff calculation797 // Increment failure counter for backoff calculation
788 consecutiveFailures++798 consecutiveFailures++
789 799
800+ // If we have too many consecutive failures, log a warning
801+ if consecutiveFailures >= maxConsecutiveFailures {
802+ log.Warn("Multiple consecutive failures in container event stream",
803+ "count", consecutiveFailures,
804+ "will_continue_trying", true)
805+ }
806+
790 // Calculate backoff delay with exponential increase807 // Calculate backoff delay with exponential increase
791 if listenSuccess {808 if listenSuccess {
792 // If we had successful events before failure, reset the delay809 // If we had successful events before failure, reset the delay
793 currentDelay = initialDelay810 currentDelay = initialDelay
794 } else {811 } else {
795- // Exponential backoff: double the delay each time, up to max812+ // Exponential backoff: increase the delay each time, up to max
796 currentDelay = time.Duration(float64(currentDelay) * 1.5)813 currentDelay = time.Duration(float64(currentDelay) * 1.5)
797 if currentDelay > maxDelay {814 if currentDelay > maxDelay {
798 currentDelay = maxDelay815 currentDelay = maxDelay
@@ -808,6 +825,7 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
808 }825 }
809 // Context was canceled, exit completely826 // Context was canceled, exit completely
810 if ctx.Err() != nil {827 if ctx.Err() != nil {
828+ eventCancel()
811 return829 return
812 }830 }
813 case event := <-messages:831 case event := <-messages:
@@ -819,6 +837,9 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
819 log.Info("Successfully receiving container events")837 log.Info("Successfully receiving container events")
820 }838 }
821 839
840+ // Update last event time
841+ lastEventTime = time.Now()
842+
822 // Process container events843 // Process container events
823 if event.Type == "container" {844 if event.Type == "container" {
824 containerID := event.Actor.ID845 containerID := event.Actor.ID
@@ -870,12 +891,31 @@ func ListenForContainerEvents(networkFilter string, callback func(string, string
870 // Add other event types as needed891 // Add other event types as needed
871 }892 }
872 }893 }
894+ case <-time.After(30 * time.Second):
895+ // If we haven't received events for a while, check if the connection is still alive
896+ timeSinceLastEvent := time.Since(lastEventTime)
897+
898+ // If it's been more than 2 minutes since the last event, reconnect
899+ if timeSinceLastEvent > 2*time.Minute {
900+ log.Debug("No events received for too long, reconnecting",
901+ "time_since_last_event", timeSinceLastEvent)
902+
903+ // Cancel the event context to clean up resources
904+ eventCancel()
905+
906+ // Break out of the inner event loop to reconnect
907+ break eventStreamLoop
908+ }
873 case <-ctx.Done():909 case <-ctx.Done():
910+ eventCancel()
874 return911 return
875 }912 }
876 }913 }
877 914
878 // If we get here, the event stream failed and we need to reconnect915 // If we get here, the event stream failed and we need to reconnect
916+ // Clean up the event context
917+ eventCancel()
918+
879 // Wait for the calculated delay before reconnecting919 // Wait for the calculated delay before reconnecting
880 select {920 select {
881 case <-time.After(currentDelay):921 case <-time.After(currentDelay):

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close