Repository: stackrox/stackrox
Pull request #14642: ROX-28390: Fix race condition in TLS issuer
Author: login: vladbologa
Labels: name: area/sensor
Created: 2025-03-17T22:09:56Z | Merged: 2025-03-27T09:18:26Z

### Description

<!--
A detailed explanation of the changes in your PR. Feel free to remove this
section if the title of your PR is sufficiently descriptive. To learn more
about contributing to this project, check "*.md" files under:
    https://github.com/stackrox/stackrox/tree/master/.github
-->

The Secured Cluster TLS issuer allows only one request to Central at a time. In order to do this, it was using an atomic bool `requestOngoing`  to stop other simultaneous requests, and `ongoingRequestID` (guarded by `ongoingRequestIDMutex`) to make sure that received responses belong to the current request.

However, this approach allowed a race condition to take place when there are requests happening in quick succession, and the online / offline mode of Sensor changes. The situation was the following, as reproduced occasionally by the `TestSensorOnlineOfflineModes` test:
* request (1) is triggered
* sensor goes into offline mode, and cancels request (1)
* before request (1) manages to reset `requestOngoing`, a response arrives and it is stored in `responseFromCentral`
* request (2) is triggered, receives the response of request (1) and finishes; the response to request (2) is discarded because there is no `requestOngoing`
* as the response to request (1) was a preprogrammed error, request (2) doesn't create TLS secrets and the test fails

To eliminate this scenario, I made the following changes:
* replace the `requestOngoing` atomic bool with a `requestMutex` that surrounds the entire request. This simplifies things a bit, and if a second request is coming while the previous didn't finish, it won't return an error but instead will wait until the first request completes
* use a queue  to pass received messages to the `requestCertificates` goroutine
* moved the request ID check to the `receive` function, which runs in the same goroutine as `requestCertificates`, therefore eliminating the need for a mutex for `ongoingRequestID`

In the current approach it's still possible that a response from a previous request arrives when a new request is already taking place (this is something that can't be prevented), but it will simply be discarded in `receive`, which then reads the channel until the correct response arrives, or the context expires.

### User-facing documentation

- [x] CHANGELOG is updated **OR** update is not needed
- [x] [documentation PR](https://spaces.redhat.com/display/StackRox/Submitting+a+User+Documentation+Pull+Request) is created and is linked above **OR** is not needed

### Testing and quality

<!--
General Availability requirements: https://github.com/stackrox/stackrox/blob/master/PR_GA.md
Feature Flags usage: https://github.com/stackrox/stackrox/blob/master/pkg/features/README.md
-->

- [x] the change is production ready: the change is GA or otherwise the functionality is gated by a feature flag
- [ ] CI results are inspected

#### Automated testing

<!--
If no tests have been contributed, please explain why unless it's obvious,
e.g., the PR is a one-line comment change.
-->

- [ ] added unit tests
- [ ] added e2e tests
- [ ] added regression tests
- [ ] added compatibility tests
- [x] modified existing tests

#### How I validated my change

<!--
Use this space to explain **how you validated** that **your change functions
exactly how you expect it**. Feel free to attach JSON snippets, curl commands,
screenshots, etc. Apply a simple benchmark: would the information you provided
convince any reviewer or any external reader that you did enough to validate
your change.

It is acceptable to assume trust and keep this section light, e.g. as a
bullet-point list.

It is acceptable to skip testing in cases when CI is sufficient, or it's a
markdown or code comment change only. It is also acceptable to skip testing for
changes that are too taxing to test before merging. In such case you are
responsible for the change after it gets merged which includes reverting,
fixing, etc. Make sure you validate the change ASAP after it gets merged or
explain in PR when the validation will be performed. Explain here why you
skipped testing in case you did so.

Have you created automated tests for your change? Explain here which validation
activities you did manually and why so.
-->

Ran the `TestSensorOnlineOfflineModes` test in a loop overnight.



Diff:
diff --git a/sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go b/sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
index 14b40ab9fed1e..f2b463104135d 100644
--- a/sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
+++ b/sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
@@ -11,6 +11,7 @@ import (
 	"github.com/stackrox/rox/pkg/centralsensor"
 	"github.com/stackrox/rox/pkg/concurrency"
 	"github.com/stackrox/rox/pkg/logging"
+	"github.com/stackrox/rox/pkg/queue"
 	"github.com/stackrox/rox/pkg/sync"
 	"github.com/stackrox/rox/pkg/uuid"
 	"github.com/stackrox/rox/sensor/common"
@@ -63,7 +64,7 @@ func NewSecuredClusterTLSIssuer(
 		getServiceCertificatesRepoFn: securedcluster.NewServiceCertificatesRepo,
 		msgToCentralC:                make(chan *message.ExpiringMessage),
 		newMsgFromSensorFn:           newSecuredClusterMsgFromSensor,
-		responseReceived:             concurrency.NewSignal(),
+		responseQueue:                queue.NewQueue[*Response](),
 		requiredCentralCapability: func() *centralsensor.CentralCapability {
 			centralCap := centralsensor.CentralCapability(centralsensor.SecuredClusterCertificatesReissue)
 			return &centralCap
@@ -101,11 +102,8 @@ type tlsIssuerImpl struct {
 	getServiceCertificatesRepoFn serviceCertificatesRepoGetter
 	certRefresher                concurrency.RetryTicker
 	msgToCentralC                chan *message.ExpiringMessage
-	responseFromCentral          atomic.Pointer[Response]
-	responseReceived             concurrency.Signal
-	ongoingRequestID             string
-	ongoingRequestIDMutex        sync.Mutex
-	requestOngoing               atomic.Bool
+	responseQueue                *queue.Queue[*Response]
+	requestMutex                 sync.Mutex
 	newMsgFromSensorFn           newMsgFromSensor
 	requiredCentralCapability    *centralsensor.CentralCapability
 	started                      atomic.Bool
@@ -229,30 +227,13 @@ func (i *tlsIssuerImpl) ProcessMessage(msg *central.MsgToSensor) error {
 		return nil
 	}
 
-	go i.dispatch(response)
-	return nil
-}
-
-func (i *tlsIssuerImpl) dispatch(response *Response) {
-	if !i.requestOngoing.Load() {
-		log.Warnf("Received a response for request ID %q, but there is no ongoing RequestCertificates call.", response.RequestId)
-		return
-	}
-	ongoingRequestID := concurrency.WithLock1(&i.ongoingRequestIDMutex, func() string {
-		return i.ongoingRequestID
-	})
-	if ongoingRequestID != response.RequestId {
-		log.Warnf("Request ID %q does not match ongoing request ID %q.",
-			response.RequestId, ongoingRequestID)
-		return
-	}
+	i.responseQueue.Push(response)
 
-	i.responseFromCentral.Store(response)
-	i.responseReceived.Signal()
+	return nil
 }
 
 // requestCertificates makes a new request for a new set of secured cluster certificates from Central.
-// Concurrent requests are *not* supported.
+// Concurrent requests are *not* supported, this function will block until previous calls finish.
 func (i *tlsIssuerImpl) requestCertificates(ctx context.Context) (*Response, error) {
 	if i.requiredCentralCapability != nil {
 		// Central capabilities are only available after this component is created,
@@ -262,24 +243,15 @@ func (i *tlsIssuerImpl) requestCertificates(ctx context.Context) (*Response, err
 		}
 	}
 
-	if !i.requestOngoing.CompareAndSwap(false, true) {
-		return nil, errors.New("concurrent requests are not supported.")
-	}
-
-	defer func() {
-		i.requestOngoing.Store(false)
-		i.responseReceived.Reset()
-	}()
-
-	requestID := uuid.NewV4().String()
-	concurrency.WithLock(&i.ongoingRequestIDMutex, func() {
-		i.ongoingRequestID = requestID
+	// Ensure only one request can be active at a time. By protecting both send and receive
+	// with the same mutex, we prevent a future send from starting while an older receive is still running.
+	return concurrency.WithLock2(&i.requestMutex, func() (*Response, error) {
+		requestID := uuid.NewV4().String()
+		if err := i.send(ctx, requestID); err != nil {
+			return nil, err
+		}
+		return i.receive(ctx, requestID)
 	})
-
-	if err := i.send(ctx, requestID); err != nil {
-		return nil, err
-	}
-	return i.receive(ctx)
 }
 
 // send a cert refresh request to Central
@@ -293,11 +265,21 @@ func (i *tlsIssuerImpl) send(ctx context.Context, requestID string) error {
 }
 
 // receive handles the response to a specific certificate request
-func (i *tlsIssuerImpl) receive(ctx context.Context) (*Response, error) {
-	select {
-	case <-ctx.Done():
-		return nil, ctx.Err()
-	case <-i.responseReceived.Done():
-		return i.responseFromCentral.Load(), nil
+func (i *tlsIssuerImpl) receive(ctx context.Context, requestID string) (*Response, error) {
+	for {
+		response := i.responseQueue.PullBlocking(ctx)
+		if ctx.Err() != nil {
+			return nil, ctx.Err()
+		}
+		if response == nil {
+			return nil, errors.New("received nil response")
+		}
+
+		if response.RequestId == requestID {
+			return response, nil
+		}
+
+		log.Warnf("Ignoring response, ID %q does not match ongoing request ID %q.",
+			response.RequestId, requestID)
 	}
 }
diff --git a/sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go b/sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go
index 73f26a6a4229a..8778f34f93ed3 100644
--- a/sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go
+++ b/sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go
@@ -13,6 +13,7 @@ import (
 	"github.com/stackrox/rox/pkg/concurrency"
 	"github.com/stackrox/rox/pkg/mtls"
 	testutilsMTLS "github.com/stackrox/rox/pkg/mtls/testutils"
+	"github.com/stackrox/rox/pkg/queue"
 	"github.com/stackrox/rox/pkg/uuid"
 	"github.com/stackrox/rox/sensor/common"
 	"github.com/stackrox/rox/sensor/common/centralcaps"
@@ -87,7 +88,7 @@ func newSecuredClusterTLSIssuerFixture(k8sClientConfig fakeK8sClientConfig) *sec
 		getServiceCertificatesRepoFn: fixture.componentGetter.getServiceCertificatesRepo,
 		msgToCentralC:                make(chan *message.ExpiringMessage),
 		newMsgFromSensorFn:           newSecuredClusterMsgFromSensor,
-		responseReceived:             concurrency.NewSignal(),
+		responseQueue:                queue.NewQueue[*Response](),
 		requiredCentralCapability: func() *centralsensor.CentralCapability {
 			centralCap := centralsensor.CentralCapability(centralsensor.SecuredClusterCertificatesReissue)
 			return &centralCap
@@ -134,7 +135,7 @@ func (f *securedClusterTLSIssuerFixture) respondRequest(
 			response = &central.IssueSecuredClusterCertsResponse{RequestId: interceptedRequestID}
 		}
 		f.interceptedRequestID.Store(response.GetRequestId())
-		f.tlsIssuer.dispatch(NewResponseFromSecuredClusterCerts(response))
+		f.tlsIssuer.responseQueue.Push(NewResponseFromSecuredClusterCerts(response))
 	}
 }
 
@@ -267,12 +268,14 @@ func (s *securedClusterTLSIssuerTests) TestSecuredClusterTLSIssuerProcessMessage
 		},
 	}
 
-	fixture.tlsIssuer.ongoingRequestID = expectedResponse.RequestId
-	fixture.tlsIssuer.requestOngoing.Store(true)
+	fixture.mockForStart(mockForStartConfig{})
+	fixture.tlsIssuer.Notify(common.SensorComponentEventCentralReachable)
+	s.Require().NoError(fixture.tlsIssuer.Start())
 
 	assert.NoError(s.T(), fixture.tlsIssuer.ProcessMessage(msg))
 	assert.Eventually(s.T(), func() bool {
-		return fixture.tlsIssuer.responseReceived.IsDone()
+		response := fixture.tlsIssuer.responseQueue.Pull()
+		return response != nil
 	}, 2*time.Second, 100*time.Millisecond)
 }
 
@@ -282,9 +285,14 @@ func (s *securedClusterTLSIssuerTests) TestSecuredClusterTLSIssuerProcessMessage
 		Msg: &central.MsgToSensor_ReprocessDeployments{},
 	}
 
+	fixture.mockForStart(mockForStartConfig{})
+	fixture.tlsIssuer.Notify(common.SensorComponentEventCentralReachable)
+	s.Require().NoError(fixture.tlsIssuer.Start())
+
 	assert.NoError(s.T(), fixture.tlsIssuer.ProcessMessage(msg))
 	assert.Never(s.T(), func() bool {
-		return fixture.tlsIssuer.responseReceived.IsDone()
+		response := fixture.tlsIssuer.responseQueue.Pull()
+		return response != nil
 	}, 200*time.Millisecond, 50*time.Millisecond)
 }
 
@@ -304,6 +312,10 @@ func (s *securedClusterTLSIssuerTests) TestSecuredClusterTLSIssuerRequestSuccess
 	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 	defer cancel()
 
+	f.mockForStart(mockForStartConfig{})
+	f.tlsIssuer.Notify(common.SensorComponentEventCentralReachable)
+	s.Require().NoError(f.tlsIssuer.Start())
+
 	go f.respondRequest(ctx, s.T(), nil)
 
 	response, err := f.tlsIssuer.requestCertificates(ctx)


Reviews:
user: login: lvalerom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T12:03:35Z
user: login: vladbologa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:21:35Z
user: login: vladbologa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:22:47Z
user: login: vladbologa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T14:31:59Z
user: login: porridge
body: The unit tests seem to be failing with `WARNING: DATA RACE` 😟 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T07:20:58Z
user: login: vladbologa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T10:07:34Z
user: login: vladbologa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T12:06:45Z
user: login: lvalerom
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T11:37:24Z
user: login: vladbologa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T12:54:46Z
user: login: vladbologa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:26:59Z
user: login: lvalerom
body: Looks great! Thank you!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T09:07:21Z

Review comments:
diff_hunk: @@ -262,24 +245,14 @@ func (i *tlsIssuerImpl) requestCertificates(ctx context.Context) (*Response, err
 		}
 	}
 
-	if !i.requestOngoing.CompareAndSwap(false, true) {
-		return nil, errors.New("concurrent requests are not supported.")
-	}
-
-	defer func() {
-		i.requestOngoing.Store(false)
-		i.responseReceived.Reset()
-	}()
-
-	requestID := uuid.NewV4().String()
-	concurrency.WithLock(&i.ongoingRequestIDMutex, func() {
-		i.ongoingRequestID = requestID
+	// make sure that only one request can be active
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
user: login: lvalerom
body: [nit] Maybe expand this comment a little bit explaining that the `send` needs to be called in the same lock block as `receive`, otherwise I think we can get in a situation like:
- Old request is locking in `receive` (not stopped yet)
- New request calls `send`
- New request waits to lock `requestMutex`
- Old request processes the response from the new request and discards it.
created_at: 2025-03-18T11:54:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +87,7 @@ func newSecuredClusterTLSIssuerFixture(k8sClientConfig fakeK8sClientConfig) *sec
 		getServiceCertificatesRepoFn: fixture.componentGetter.getServiceCertificatesRepo,
 		msgToCentralC:                make(chan *message.ExpiringMessage),
 		newMsgFromSensorFn:           newSecuredClusterMsgFromSensor,
-		responseReceived:             concurrency.NewSignal(),
+		responseFromCentral:          make(chan *Response),
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go
user: login: lvalerom
body: This channel is never closed. I know we do that in multiple components but we are planning a refactor to close them and I think it's better if we don't introduce new channels without closing them.
created_at: 2025-03-18T11:57:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -234,25 +231,11 @@ func (i *tlsIssuerImpl) ProcessMessage(msg *central.MsgToSensor) error {
 }
 
 func (i *tlsIssuerImpl) dispatch(response *Response) {
-	if !i.requestOngoing.Load() {
-		log.Warnf("Received a response for request ID %q, but there is no ongoing RequestCertificates call.", response.RequestId)
-		return
-	}
-	ongoingRequestID := concurrency.WithLock1(&i.ongoingRequestIDMutex, func() string {
-		return i.ongoingRequestID
-	})
-	if ongoingRequestID != response.RequestId {
-		log.Warnf("Request ID %q does not match ongoing request ID %q.",
-			response.RequestId, ongoingRequestID)
-		return
-	}
-
-	i.responseFromCentral.Store(response)
-	i.responseReceived.Signal()
+	i.responseFromCentral <- response
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
user: login: lvalerom
body: The `dispatch` goroutine might leak on stop. Not a big problem in production since sensor only stops if the pod is being stopped but for integration test it might be a problem.
created_at: 2025-03-18T12:02:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -234,25 +231,11 @@ func (i *tlsIssuerImpl) ProcessMessage(msg *central.MsgToSensor) error {
 }
 
 func (i *tlsIssuerImpl) dispatch(response *Response) {
-	if !i.requestOngoing.Load() {
-		log.Warnf("Received a response for request ID %q, but there is no ongoing RequestCertificates call.", response.RequestId)
-		return
-	}
-	ongoingRequestID := concurrency.WithLock1(&i.ongoingRequestIDMutex, func() string {
-		return i.ongoingRequestID
-	})
-	if ongoingRequestID != response.RequestId {
-		log.Warnf("Request ID %q does not match ongoing request ID %q.",
-			response.RequestId, ongoingRequestID)
-		return
-	}
-
-	i.responseFromCentral.Store(response)
-	i.responseReceived.Signal()
+	i.responseFromCentral <- response
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
user: login: vladbologa
body: Should be fixed now.
created_at: 2025-03-18T14:21:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,24 +245,14 @@ func (i *tlsIssuerImpl) requestCertificates(ctx context.Context) (*Response, err
 		}
 	}
 
-	if !i.requestOngoing.CompareAndSwap(false, true) {
-		return nil, errors.New("concurrent requests are not supported.")
-	}
-
-	defer func() {
-		i.requestOngoing.Store(false)
-		i.responseReceived.Reset()
-	}()
-
-	requestID := uuid.NewV4().String()
-	concurrency.WithLock(&i.ongoingRequestIDMutex, func() {
-		i.ongoingRequestID = requestID
+	// make sure that only one request can be active
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
user: login: vladbologa
body: Done
created_at: 2025-03-18T14:22:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +87,7 @@ func newSecuredClusterTLSIssuerFixture(k8sClientConfig fakeK8sClientConfig) *sec
 		getServiceCertificatesRepoFn: fixture.componentGetter.getServiceCertificatesRepo,
 		msgToCentralC:                make(chan *message.ExpiringMessage),
 		newMsgFromSensorFn:           newSecuredClusterMsgFromSensor,
-		responseReceived:             concurrency.NewSignal(),
+		responseFromCentral:          make(chan *Response),
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go
user: login: vladbologa
body: I know, one possibility would be to close the channel when `Stop` is called, but it's not without issues.

In `Stop` (or `deactivate` more precisely) I could wait for all the `dispatch` goroutines to end (e.g. with `WaitGroup`) and then close the channel. But that still would allow the following scenario:

```
tlsIssuer.ProcessMessage(msg) // calls "go dispatch(msg)"
tlsIssuer.Stop()
// the `dispatch()` goroutine started by `ProcessMessage` starts after `Stop` ended, and tries to write to a closed channel -> panic
```

So I'm a bit reluctant because we might accidentally introduce new races, for little benefit. But I'm open to suggestions.
created_at: 2025-03-18T14:31:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +87,7 @@ func newSecuredClusterTLSIssuerFixture(k8sClientConfig fakeK8sClientConfig) *sec
 		getServiceCertificatesRepoFn: fixture.componentGetter.getServiceCertificatesRepo,
 		msgToCentralC:                make(chan *message.ExpiringMessage),
 		newMsgFromSensorFn:           newSecuredClusterMsgFromSensor,
-		responseReceived:             concurrency.NewSignal(),
+		responseFromCentral:          make(chan *Response),
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go
user: login: vladbologa
body: I'm now closing the `responseFromCentral` on `Stop`, and also waiting for ongoing requests (`requestCertificates`) and responses (`dispatch`) to finish. Hopefully I've covered possible races.
created_at: 2025-03-19T10:07:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,24 +268,22 @@ func (i *tlsIssuerImpl) requestCertificates(ctx context.Context) (*Response, err
 		}
 	}
 
-	if !i.requestOngoing.CompareAndSwap(false, true) {
-		return nil, errors.New("concurrent requests are not supported.")
-	}
+	concurrency.WithLock(&i.activateLock, func() { i.wgRequest.Add(1) })
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
user: login: vladbologa
body: I might be misusing `WaitGroup`s here: `Add` must be called before calling `Wait`. But I can't guarantee that due to the non-deterministic timings of these calls:
- `requestCertificates` is called by a timer on its own goroutine
- `Wait` is called in `deactivate` on a different gouroutine (either called by `Stop`, or the connection gouroutine in case of `Nofity` when going offline) 

To work around this I added locks, using the mutex of the `activate`/`deactivate` functions. If the `deactivate` method is already running at this point, then we'll wait until it finishes to call `Add`, which will happen after `Wait` finishes, and then the context will already have expired, causing `requestCertificates` to exit.

I used a similar pattern with the `wgDispatch WaitGroup`.

 I'm still wondering if simply not closing `responseFromCentral` would be better, to reduce unnecessary complexity.
created_at: 2025-03-21T12:00:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,24 +268,22 @@ func (i *tlsIssuerImpl) requestCertificates(ctx context.Context) (*Response, err
 		}
 	}
 
-	if !i.requestOngoing.CompareAndSwap(false, true) {
-		return nil, errors.New("concurrent requests are not supported.")
-	}
+	concurrency.WithLock(&i.activateLock, func() { i.wgRequest.Add(1) })
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
user: login: lvalerom
body: Would it be possible to use a [queue](https://github.com/stackrox/stackrox/blob/1caa989e673ddebd60b7b93296bf1f00cab4bbb9/pkg/queue/queue.go#L21) instead of a channel to handle all this logic? So, instead of using the `responseFromCentral` channel, use a queue (`responseQueue`). With that, I think all the `waitGroups` are no longer needed and it would simplify the logic.
created_at: 2025-03-25T11:37:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -262,24 +268,22 @@ func (i *tlsIssuerImpl) requestCertificates(ctx context.Context) (*Response, err
 		}
 	}
 
-	if !i.requestOngoing.CompareAndSwap(false, true) {
-		return nil, errors.New("concurrent requests are not supported.")
-	}
+	concurrency.WithLock(&i.activateLock, func() { i.wgRequest.Add(1) })
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer.go
user: login: vladbologa
body: Didn't know we had this, I'll take a look.
created_at: 2025-03-25T12:54:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +87,7 @@ func newSecuredClusterTLSIssuerFixture(k8sClientConfig fakeK8sClientConfig) *sec
 		getServiceCertificatesRepoFn: fixture.componentGetter.getServiceCertificatesRepo,
 		msgToCentralC:                make(chan *message.ExpiringMessage),
 		newMsgFromSensorFn:           newSecuredClusterMsgFromSensor,
-		responseReceived:             concurrency.NewSignal(),
+		responseFromCentral:          make(chan *Response),
path: sensor/kubernetes/certrefresh/securedcluster_tls_issuer_test.go
user: login: vladbologa
body: Closing this conversation as I removed the channel.
created_at: 2025-03-26T15:26:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: openshift-ci[bot]
created_at: 2025-03-17T22:10:00Z
body: Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with `/test all`
author_association: NONE
user: login: rhacs-bot
created_at: 2025-03-17T22:35:43Z
body: [//]: # (meta:type=build)

Images are ready for the commit at 0f580db.

To use with deploy scripts, first `export MAIN_IMAGE_TAG=4.8.x-242-g0f580db6bb`.
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-17T23:17:31Z
body: ## [Codecov](https://app.codecov.io/gh/stackrox/stackrox/pull/14642?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) Report
Attention: Patch coverage is `86.95652%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 48.88%. Comparing base [(`d228674`)](https://app.codecov.io/gh/stackrox/stackrox/commit/d22867428080ff0928788095ec7e634b43b863f0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) to head [(`0f580db`)](https://app.codecov.io/gh/stackrox/stackrox/commit/0f580db6bb60c3e9b65d0dcbbd184b62b950bf0f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).
> Report is 93 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/stackrox/stackrox/pull/14642?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Patch % | Lines |
|---|---|---|
| [...ubernetes/certrefresh/securedcluster\_tls\_issuer.go](https://app.codecov.io/gh/stackrox/stackrox/pull/14642?src=pr&el=tree&filepath=sensor%2Fkubernetes%2Fcertrefresh%2Fsecuredcluster_tls_issuer.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#diff-c2Vuc29yL2t1YmVybmV0ZXMvY2VydHJlZnJlc2gvc2VjdXJlZGNsdXN0ZXJfdGxzX2lzc3Vlci5nbw==) | 86.95% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/stackrox/stackrox/pull/14642?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master   #14642      +/-   ##
==========================================
- Coverage   49.29%   48.88%   -0.41%     
==========================================
  Files        2530     2547      +17     
  Lines      185039   186949    +1910     
==========================================
+ Hits        91208    91394     +186     
- Misses      86603    88314    +1711     
- Partials     7228     7241      +13     
```

| [Flag](https://app.codecov.io/gh/stackrox/stackrox/pull/14642/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | Coverage Δ | |
|---|---|---|
| [go-unit-tests](https://app.codecov.io/gh/stackrox/stackrox/pull/14642/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox) | `48.88% <86.95%> (-0.41%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/stackrox/stackrox/pull/14642?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stackrox).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: vladbologa
created_at: 2025-03-19T10:04:24Z
body: > The unit tests seem to be failing with `WARNING: DATA RACE` 😟

I was now creating the `responseFromCentral` channel in `activate` and closing it in `deactivate`, and focused on data races between `deactivate` and `dispatch`, and omitted races that could occur in `requestCertificates`. I hope it's fixed now.
author_association: CONTRIBUTOR
user: login: vladbologa
created_at: 2025-03-27T09:18:18Z
body: > Looks great! Thank you!

Thank you for the queue tip! :)
author_association: CONTRIBUTOR