Skip to content

Commit ec69b5d

Browse files
feat: Add option to disable cluster websocket connection and fix instability (#735)
* feat: Add option to disable cluster websocket connection (#684) * add option to disable cluster websocket connection * add option to disable websocket connection in console manager * remove websocket configuration check and simplify console manager setup * simplify console manager initialization by removing unnecessary parameters * streamline socket connection handling in controller manager * add Close method to Socket interface and implement in socket * add option to disable websocket connection in controller manager * disable websocket in agent configuration * make socket methods thread-safe by adding mutex locks * refactor startController method to improve readability * add reconnect logic to websocket with `closed` state handling and improve thread safety * improve comments in socket.go for clarity and consistency * refactor websocket connection handling to improve clarity and prevent blocking * refactor wssUri function to construct websocket URL more clearly and efficiently * remove redundant comments * enhance websocket connection handling with improved error messages and initialization * refactor websocket event handling to improve clarity, error messaging, and validation * introduce `joining` state to websocket for improved thread safety and prevent deadlock, and enable websocket in agent configuration * refactor websocket reconnect logic for clarity, removing redundant `reconnect` method * fix socket message handling to ensure proper mutex unlocking when socket is closed * refactor * adjust log levels * ensure proper state management on connection failure * ensure proper state management on connection failure * update websocket disconnection logic * enhance websocket disconnection handling with reconnect logic * update websocket disconnection message and handle client closure * add option to create new websocket client with generation tracking * add clientReceiver to manage websocket client generation and prevent stale callbacks * refactor notifyConnectLocked to return a boolean for Join() invocation * Update pkg/websocket/client_receiver.go Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * avoid callback-driven client teardown on transient disconnects and let gophoenix own transport reconnects to prevent protocol error churn * extend tests * fix review finding * initialize sockets in closed state to avoid startup websocket handshakes * fix lint * generate crd docs * fix review findings * fix review findings * switch to graphql subscriptions * add logs * add log * normalize resource value * fix review findings * improve error handling in AgentRuntimeReconciler * add logs * update publisher key * update other keys --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent ffeccd2 commit ec69b5d

23 files changed

Lines changed: 578 additions & 132 deletions

File tree

api/v1alpha1/agentconfig_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ type AgentConfigurationSpec struct {
5454
// Must be greater than 0. Set this field to nil (omit) to disable the limit.
5555
// +kubebuilder:validation:Minimum=1
5656
MaxAgentRunPods *int `json:"maxAgentRunPods,omitempty"`
57+
58+
// DisableWebsocket disables the cluster websocket connection to the Console.
59+
// When enabled, the agent will rely exclusively on polling instead of receiving
60+
// push updates. This is useful in large-scale edge deployments where maintaining
61+
// persistent websocket connections has an infeasible network cost.
62+
DisableWebsocket *bool `json:"disableWebsocket,omitempty"`
5763
}
5864

5965
//+kubebuilder:object:root=true

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

charts/deployment-operator/crds/deployments.plural.sh_agentconfigurations.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ spec:
6060
Must be greater than 0. Set this field to nil (omit) to disable the limit.
6161
minimum: 1
6262
type: integer
63+
disableWebsocket:
64+
description: |-
65+
DisableWebsocket disables the cluster websocket connection to the Console.
66+
When enabled, the agent will rely exclusively on polling instead of receiving
67+
push updates. This is useful in large-scale edge deployments where maintaining
68+
persistent websocket connections has an infeasible network cost.
69+
type: boolean
6370
maxConcurrentReconciles:
6471
description: |-
6572
MaxConcurrentReconciles controls the maximum number of concurrent reconcile loops.

cmd/agent/kubernetes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ func registerKubeReconcilersOrDie(
317317
if err := agentRuntimeReconciler.SetupWithManager(manager); err != nil {
318318
setupLog.Error(err, "unable to create controller", "controller", "AgentRuntime")
319319
}
320-
consoleManager.Socket.AddPublisher("agent.run.event", agentRuntimeReconciler)
320+
consoleManager.Socket.AddPublisher("agent_run", agentRuntimeReconciler)
321321

322322
if err := (&controller.AgentRunReconciler{
323323
Client: manager.GetClient(),

config/crd/bases/deployments.plural.sh_agentconfigurations.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ spec:
6060
Must be greater than 0. Set this field to nil (omit) to disable the limit.
6161
minimum: 1
6262
type: integer
63+
disableWebsocket:
64+
description: |-
65+
DisableWebsocket disables the cluster websocket connection to the Console.
66+
When enabled, the agent will rely exclusively on polling instead of receiving
67+
push updates. This is useful in large-scale edge deployments where maintaining
68+
persistent websocket connections has an infeasible network cost.
69+
type: boolean
6370
maxConcurrentReconciles:
6471
description: |-
6572
MaxConcurrentReconciles controls the maximum number of concurrent reconcile loops.

config/samples/agentConfiguration.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ spec:
1212
vulnerabilityReportUploadInterval: "10m"
1313
maxConcurrentReconciles: 5
1414
maxSentinelRunJobs: 2
15+
disableWebsocket: false
1516

docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ _Appears in:_
8686
| `maxSentinelRunJobs` _integer_ | MaxSentinelRunJobs limits the number of concurrent SentinelRunJobs that can be active at any given time.<br />Must be greater than 0. Set this field to nil (omit) to disable the limit. | | Minimum: 1 <br /> |
8787
| `maxStackRunJobs` _integer_ | MaxStackRunJobs limits the number of concurrent StackRunJobs that can be active at any given time.<br />Must be greater than 0. Set this field to nil (omit) to disable the limit. | | Minimum: 1 <br /> |
8888
| `maxAgentRunPods` _integer_ | MaxAgentRunPods limits the number of concurrent agent run pods that can be active at any given time.<br />Must be greater than 0. Set this field to nil (omit) to disable the limit. | | Minimum: 1 <br /> |
89+
| `disableWebsocket` _boolean_ | DisableWebsocket disables the cluster websocket connection to the Console.<br />When enabled, the agent will rely exclusively on polling instead of receiving<br />push updates. This is useful in large-scale edge deployments where maintaining<br />persistent websocket connections has an infeasible network cost. | | |
8990

9091

9192
#### AgentHelmConfiguration

go.mod

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ require (
3030
github.com/google/go-github/v68 v68.0.0
3131
github.com/grafana/pyroscope-go v1.2.7
3232
github.com/hashicorp/terraform-json v0.26.0
33+
github.com/hasura/go-graphql-client v0.16.0
3334
github.com/mark3labs/mcp-go v0.40.0
3435
github.com/mitchellh/copystructure v1.2.0
3536
github.com/onsi/ginkgo/v2 v2.28.1
@@ -44,7 +45,6 @@ require (
4445
github.com/pluralsh/console/go/client v1.74.1
4546
github.com/pluralsh/console/go/polly v1.0.0
4647
github.com/pluralsh/controller-reconcile-helper v0.1.0
47-
github.com/pluralsh/gophoenix v0.1.3-0.20231201014135-dff1b4309e34
4848
github.com/prometheus/client_golang v1.23.2
4949
github.com/sahilm/fuzzy v0.1.1
5050
github.com/samber/lo v1.52.0
@@ -193,6 +193,7 @@ require (
193193
github.com/ckaznocha/intrange v0.3.1 // indirect
194194
github.com/cloudwego/base64x v0.1.4 // indirect
195195
github.com/cloudwego/iasm v0.2.0 // indirect
196+
github.com/coder/websocket v1.8.14 // indirect
196197
github.com/containerd/containerd v1.7.30 // indirect
197198
github.com/containerd/errdefs v1.0.0 // indirect
198199
github.com/containerd/log v0.1.0 // indirect
@@ -282,7 +283,6 @@ require (
282283
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
283284
github.com/google/uuid v1.6.0 // indirect
284285
github.com/gordonklaus/ineffassign v0.2.0 // indirect
285-
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
286286
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
287287
github.com/gostaticanalysis/comment v1.5.0 // indirect
288288
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
@@ -311,11 +311,9 @@ require (
311311
github.com/jjti/go-spancheck v0.6.5 // indirect
312312
github.com/jmoiron/sqlx v1.4.0 // indirect
313313
github.com/josharian/intern v1.0.0 // indirect
314-
github.com/jpillora/backoff v1.0.0 // indirect
315314
github.com/json-iterator/go v1.1.12 // indirect
316315
github.com/julz/importas v0.2.0 // indirect
317316
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
318-
github.com/kataras/golog v0.1.15 // indirect
319317
github.com/kisielk/errcheck v1.10.0 // indirect
320318
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
321319
github.com/klauspost/compress v1.18.2 // indirect

go.sum

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
339339
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
340340
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
341341
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
342+
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
343+
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
342344
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
343345
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
344346
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -640,8 +642,6 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE
640642
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
641643
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
642644
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
643-
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
644-
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
645645
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
646646
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
647647
github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
@@ -699,6 +699,8 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx
699699
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
700700
github.com/hashicorp/terraform-json v0.26.0 h1:+BnJavhRH+oyNWPnfzrfQwVWCZBFMvjdiH2Vi38Udz4=
701701
github.com/hashicorp/terraform-json v0.26.0/go.mod h1:eyWCeC3nrZamyrKLFnrvwpc3LQPIJsx8hWHQ/nu2/v4=
702+
github.com/hasura/go-graphql-client v0.16.0 h1:DQLfp+djj4j5NPdJkGYym8J55hpm5etML1zqgco78Qc=
703+
github.com/hasura/go-graphql-client v0.16.0/go.mod h1:z/sO2T0zI+HnPNIevQcs+7xA6/gDOc8hgHMrNBzfL2c=
702704
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
703705
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
704706
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
@@ -727,16 +729,12 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
727729
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
728730
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
729731
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
730-
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
731-
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
732732
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
733733
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
734734
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
735735
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
736736
github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
737737
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
738-
github.com/kataras/golog v0.1.15 h1:gDNOENbbn+6me98UW1f9Cs5MRUlAkabnNvmgLFM58Xw=
739-
github.com/kataras/golog v0.1.15/go.mod h1:Ozu1TDa+OKC7fFe7OG64In71yLxjda+6kPl+Rg3v1hA=
740738
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
741739
github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw=
742740
github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
@@ -947,8 +945,6 @@ github.com/pluralsh/console/go/polly v1.0.0 h1:NRg8CMITGllEJ08oYidNuuG/gJGDdDuga
947945
github.com/pluralsh/console/go/polly v1.0.0/go.mod h1:eH42iPlRip2bs6tB+Vg6fxC/sz6oWxyNGVDE9uyA5EY=
948946
github.com/pluralsh/controller-reconcile-helper v0.1.0 h1:BV3dYZFH5rn8ZvZjtpkACSv/GmLEtRftNQj/Y4ddHEo=
949947
github.com/pluralsh/controller-reconcile-helper v0.1.0/go.mod h1:RxAbvSB4/jkvx616krCdNQXPbpGJXW3J1L3rASxeFOA=
950-
github.com/pluralsh/gophoenix v0.1.3-0.20231201014135-dff1b4309e34 h1:ab2PN+6if/Aq3/sJM0AVdy1SYuMAnq4g20VaKhTm/Bw=
951-
github.com/pluralsh/gophoenix v0.1.3-0.20231201014135-dff1b4309e34/go.mod h1:IagWXKFYu6NTHzcJx2dJyrIlZ1Sv2PH3fhOtplA9qOs=
952948
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
953949
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
954950
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

internal/controller/agentruntime_controller.go

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -212,42 +212,46 @@ func (r *AgentRuntimeReconciler) addOrRemoveFinalizer(ctx context.Context, agent
212212
return nil
213213
}
214214

215-
func (r *AgentRuntimeReconciler) Publish(runId string, kick bool) {
216-
err := r.createRunFromId(runId)
215+
func (r *AgentRuntimeReconciler) Publish(runID string, kick bool) {
216+
logger := log.FromContext(r.Ctx)
217+
logger.Info("received websocket agent run event", "id", runID, "kick", kick)
218+
219+
err := r.createRunFromID(runID)
217220
if err != nil {
218-
log := log.FromContext(r.Ctx)
219-
log.Error(err, "failed to create agent run", "id", runId)
221+
logger.Error(err, "failed to process websocket agent run event", "id", runID, "kick", kick)
220222
}
221223
}
222224

223-
func (r *AgentRuntimeReconciler) createRunFromId(runId string) error {
224-
ctx := r.Ctx
225-
226-
run, err := r.ConsoleClient.GetAgentRun(ctx, runId)
225+
func (r *AgentRuntimeReconciler) createRunFromID(runID string) error {
226+
logger := log.FromContext(r.Ctx)
227+
run, err := r.ConsoleClient.GetAgentRun(r.Ctx, runID)
227228
if err != nil {
228229
return fmt.Errorf("failed to get agent run: %w", err)
229230
}
230-
231-
runtimeName := run.Runtime.Name
231+
if run.Runtime == nil || run.Runtime.Name == "" {
232+
return fmt.Errorf("agent run %q runtime details are missing", runID)
233+
}
232234

233235
agentRuntime := &v1alpha1.AgentRuntime{}
234-
nsn := types.NamespacedName{
235-
Name: runtimeName,
236-
Namespace: "",
237-
}
238-
if err := r.Get(ctx, nsn, agentRuntime); err != nil {
236+
if err := r.Get(r.Ctx, types.NamespacedName{Name: run.Runtime.Name, Namespace: ""}, agentRuntime); err != nil {
237+
if errors.IsNotFound(err) {
238+
logger.Info("agent runtime for websocket agent run event not found", "id", runID, "runtime", run.Runtime.Name)
239+
}
239240
return client.IgnoreNotFound(err)
240241
}
241242

242-
return r.createAgentRun(ctx, agentRuntime, run)
243+
return r.createAgentRun(r.Ctx, agentRuntime, run)
243244
}
244245

245246
func (r *AgentRuntimeReconciler) createAgentRun(ctx context.Context, agentRuntime *v1alpha1.AgentRuntime, run *console.AgentRunFragment) error {
246247
logger := log.FromContext(ctx)
248+
key := client.ObjectKey{Name: run.ID, Namespace: agentRuntime.Spec.TargetNamespace}
247249

248-
if r.Get(ctx, client.ObjectKey{Name: run.ID, Namespace: agentRuntime.Spec.TargetNamespace}, &v1alpha1.AgentRun{}) == nil {
250+
if err := r.Get(ctx, key, &v1alpha1.AgentRun{}); err == nil {
249251
logger.Info("agent run already exists", "id", run.ID)
250252
return nil
253+
} else if !errors.IsNotFound(err) {
254+
return fmt.Errorf("failed to lookup agent run: %w", err)
251255
}
252256

253257
agentRun := &v1alpha1.AgentRun{
@@ -277,6 +281,9 @@ func (r *AgentRuntimeReconciler) createAgentRun(ctx context.Context, agentRuntim
277281
}
278282

279283
if err := r.Create(ctx, agentRun); err != nil {
284+
if errors.IsAlreadyExists(err) {
285+
return nil
286+
}
280287
return fmt.Errorf("failed to create agent run: %w", err)
281288
}
282289

0 commit comments

Comments
 (0)