-
Notifications
You must be signed in to change notification settings - Fork 13
add pending request cache to allow for resuming in-flight requests that take longer than a single issuance cycle #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add pending request cache to allow for resuming in-flight requests that take longer than a single issuance cycle #51
Conversation
return nil, nil | ||
} | ||
|
||
// TODO: check if this request is still actually valid for the input metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've not done this just yet, as I don't think it's quite as important as we may think (as volumeAttributes on a pod are not mutable).
The only case where this could be problematic is if a drivers implementation of generateRequest
is non-deterministic/can change between calls. To properly handle the wide-range of weird setups users may have, we may actually need to push this comparison function to the driver implementers interface...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider check privateKey against CSR's public key here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the internal cache is in memory, UIDs are guaranteed to be unique, and the CertificateRequest resource is immutable, I don't think it's actually essential for us to implement the privatekey<>public key check...
I think I am also going to leave this TODO for now, as it isn't really something we handle at all at the moment (and does raise questions around timing, e.g. what if the driver is random, when can we ever really stop?). I'd like us to expand on our expectations around how drivers implement generateRequest before over-complicating this code path :)
// begin a background routine which periodically checks to ensure all members of the pending request map actually | ||
// have corresponding CertificateRequest objects in the apiserver. | ||
// This avoids leaking memory if we don't observe a request being deleted, or we observe it after the lister has purged | ||
// the request data from its cache. | ||
// this routine must be careful to not delete entries from this map that have JUST been added to the map, but haven't | ||
// been observed by the lister yet (else it may purge data we want to keep, causing a whole new request cycle). | ||
// for now, to avoid this case, we only run the routine every 5 minutes. It would be better if we recorded the time we | ||
// added the entry to the map instead, and only purged items from the map that are older that N duration (TBD). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious when will the informer lost the delete event from api server ? I thought the informerFactory will guarantee to resync in a period of time to ensure it captures all the events for eventual consistency ?
As mentioned in your comment, not sure how to prevent the newly added entry not being deleted because of lister is not in sync yet. If the request is happened at the 5 mins edge, will be deleted immediately as the lister does not have it in cache yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep with a resync period, we will definitely see the fact that the CR has been deleted. However, it's not guaranteed that the lister will still have a copy of the object stored - without the object, we can't convert the namespace/name
to a known UID to look up in the requestToPrivateKeyMap.
Hence, if we don't have access to the UID once we have observed the delete, we won't be able to de-register/remove it from our own internal map.
@@ -259,6 +321,10 @@ type Manager struct { | |||
// lister is used as a read-only cache of CertificateRequest resources | |||
lister cmlisters.CertificateRequestLister | |||
|
|||
// A map that associates a CertificateRequest's UID with its private key. | |||
requestToPrivateKeyLock *sync.Mutex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using sync.RWMutex
to improve the performance a little bit on read ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't expect to have many concurrent readers so it seemed like a very negligible performance gain (and keeps things a little simpler to use a regular mutex for future readers)
return nil, nil | ||
} | ||
|
||
// TODO: check if this request is still actually valid for the input metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider check privateKey against CSR's public key here ?
d1d751f
to
96a41ac
Compare
8a6d253
to
356358e
Compare
case cmapi.CertificateRequestReasonFailed: | ||
return false, fmt.Errorf("request %q has failed: %s", updatedReq.Name, readyCondition.Message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we consider call m.deletePendingRequestPrivateKey(req.UID)
when the CertificateRequest
is in Failed
condition ? It likely to fail again with the same private key. Create a new CR might help resolving the problem if it is due to key reuse issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll automatically use a new private key the next time anyway - if a request is failed, it is terminal so will never be returned again by findPendingRequest
(i.e. it won't be re-used). This will in turn trigger a new CR to be created and new private key generated (or at least, a new call to generatePrivateKey).
The item will be deleted from the map once the CR is deleted, although yep perhaps a future optimisation could be to delete terminal failed items from the map a bit early just to save on memory.. but it shouldn't have any functional difference :)
func (m *Manager) handleRequest(ctx context.Context, volumeID string, meta metadata.Metadata, key crypto.PrivateKey, req *cmapi.CertificateRequest) error { | ||
log := m.log.WithValues("volume_id", volumeID) | ||
|
||
// Poll every 200ms for the CertificateRequest to be ready |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to choose 200ms now? This looks like a typical round tripper time for remote data center query.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had reduced it here as this whole block only ever reads from a local in-memory cache anyway, and it reduced test flakes (as there were a few awkward timing issues where we had timeouts of 2s, but 1s sleeps in between each 'loop' here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@munnerz A few comments from me, but otherwise looks good to me 🙂
manager/manager.go
Outdated
requestToPrivateKeyLock.Lock() | ||
defer requestToPrivateKeyLock.Unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For keeping consistency of what the state of the world is between routines, we should lock at the beginning of this function (before listing).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't to be done ^
manager/manager.go
Outdated
go wait.Until(func() { | ||
reqs, err := lister.List(labels.Everything()) | ||
if err != nil { | ||
janitorLogger.Error(err, "failed listing existing requests") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A general comment is that we currently have no coordination between Stop
and our go routines so are returning from Stop
before all resources have been released. We should consider adding this in another PR.
// start at the end of the slice and work back to maxRequestsPerVolume | ||
for i := len(reqs) - 1; i >= m.maxRequestsPerVolume-1; i-- { | ||
for i := len(reqs) - 1; i > m.maxRequestsPerVolume-1; i-- { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why has this been changed? It is because we can now recover the private key between syncs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah exactly - this function was previously doing some ✨ weird ✨ counting logic, which is now fixed (and you can see the behaviour change by taking a look at how the unit tests have changed too)
1ecbbb3
to
0f8a34e
Compare
/lgtm |
/approve |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: 7ing, JoshVanL The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
0f8a34e
to
8293161
Compare
…at take longer than a single issuance cycle Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
Signed-off-by: James Munnelly <[email protected]>
8293161
to
0ce8db0
Compare
@@ -313,16 +312,16 @@ func TestManager_ManageVolume_exponentialBackOffRetryOnIssueErrors(t *testing.T) | |||
Jitter: expBackOffJitter, | |||
Steps: expBackOffSteps, | |||
} | |||
opts.ReadyToRequest = func(meta metadata.Metadata) (bool, string) { | |||
// ReadyToRequest will be called by issue() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is no longer true, hence as part of this PR I've added a temporary function that will only be called during tests, which increments whenever issue() is called.
This is the lesser of the evils IMO, until we have actual metrics support throughout csi-lib, which will allow us to do stuff like counting issue()
calls properly :)
/lgtm |
replaces #48
Some issuers take a very long time to approve and issue a certificate request. Because the CSI drivers NodePublishVolume call has its own implicit timeout (1 minute) which cannot be changed easily, we would like to be able to 'resume' a request even if it has taken longer than 1 minute to complete.
As a concrete example, say an issuer always takes 90s to complete a request (for whatever reason). In these cases, the driver will wait 60s, the context will timeout, 30s later the request will be issued, but upon the NodePublishVolume call being retried we will continuously create a new request.
This is obviously not desirable, so persisting a reference to the
crypto.PrivateKey
in memory allows us to 'resume' the request if it is still usable.This is different to #48 in that I've avoided modifying large parts of existing code-flows - instead, basically using a map as a cache to lookup an existing private key.
I've also added in an event handler that monitors 'delete' operations on CertificateRequest objects so we can handle the case where another entity deletes requests, so we don't keep persisting stale private keys in memory forever (aka a memory link)
This PR is still WIP, as I need to add a number of tests for it. To do that effectively, I need #46 to be merged so we can timeout the first
issue
call.cc @7ing @irbekrm