Repository navigation
[#163] Parse CREST query results on the calling thread, reuse connections and stop the per-request 401 - #194
Conversation
…e shared BASIC scheme
…t CREST customizer
…r 10 s idle Connection: close came with OPENIDM-2604 (connections to OpenDJ occasionally timed out). Pooled connections now expire after at most 10 s idle, below common server idle timeouts, instead of being closed after every request.
…pping and soften the pool-wait comment
…t once and say query() skips queryAsync()
vharseko
left a comment
There was a problem hiding this comment.
BASIC_PREEMPTIVE keeps the defect this PR fixes for BASIC (pre-existing, not a regression)
CustomizerScript.groovy:138 puts one new BasicScheme() into a plain BasicAuthCache in the propertyBag, and execute leaves that context alone. RequestAuthCache (4.3.5) hands that one instance to every request, so concurrent authenticate() calls share its Base64 codec. The commons-codec 1.6 embedded in httpclient-osgi 4.3.5 keeps buffer, pos and bitWorkArea as instance fields, which is the cause the description gives for the per-request copy. After a 401, processChallenge() runs on the shared instance and authFailed removes it from the HashMap. The next successful challenge puts a new scheme into the same unsynchronised map from an I/O thread. CREST_SAMPLE uses BASIC_PREEMPTIVE, and it is one of the two options #163 proposes. Filling a SharedBasicAuthCache instead of BasicAuthCache in that branch would close it, because copyOf gives each request a complete copy. A follow-up issue is fine if you'd rather keep this PR's scope.
Minor: per-realm credentials. Once the shared cache is filled, RequestAuthCache looks the credentials up with AuthScope(host, ANY_REALM, "basic"). A customizer that registers credentials per realm on the same host and port gets whichever entry BasicCredentialsProvider's best-match scan meets first. If it is the wrong one, the 401 to a complete preemptive scheme ends as FAILURE without a retry. Before this PR, the challenge carried the realm and the right credentials were picked. The default customizer registers a single AuthScope(host, port) and is not affected. Maybe worth a line in the behaviour-change list, together with the opt-out (builder.disableAuthCaching(), or an own AuthCache in beforeRequest).
… copy of the BASIC scheme The default customizer filled a plain BasicAuthCache that all requests share. On httpclient 4.3.x it returns the one stored BasicScheme, whose Base64 codec is not thread-safe, and its HashMap is changed by authFailed and authSucceeded on the I/O threads. It now fills a SharedBasicAuthCache, which copies the scheme per request and is synchronized.
…t runs The customizer's init closure fills the propertyBag that beforeRequest reads, but execute ran beforeRequest first. The first request of a client therefore went out without the BASIC_PREEMPTIVE AuthCache and the cookie store, and took a 401.
|
Both points hold; I checked them against the httpclient-osgi 4.3.5 and httpasyncclient-osgi 4.0.2 bytecode.
Pinning it end to end exposed a second gap, identical at 78c1e16: Per-realm credentials: confirmed. Description updated (behaviour changes, two test-strength rows, not-pinned list). |
vharseko
left a comment
There was a problem hiding this comment.
Both points are closed.
BASIC_PREEMPTIVE: 4d8eb37 fills aSharedBasicAuthCache, so each request gets its ownBasicScheme, and fdde3ca builds the client beforebeforeRequest, so a client's first request no longer goes out without the preemptive cache. I ran both new tests against mutants on fdde3ca: reverting the order fails onlytestPreemptiveBasicNeverTakesA401(expected [0] but found [1]), a plainBasicAuthCachefails onlytestPreemptiveBasicGivesEachRequestItsOwnScheme(expected not same),if (true)for the guard failstestPreemptiveBasicNeverTakesA401, and both reverts together fail both. The head is green.- Per-realm credentials: the behaviour-change line and both opt-outs match httpasyncclient 4.0.2 (
disableAuthCaching()dropsRequestAuthCache). Agreed that carrying the realm in the copy would not help.
Thanks for catching the init/beforeRequest order as well.
Fixes #163.
ScriptedCRESTConnectorno longer runs query result handlers on the HTTP client's I/O thread. It now stops a query when the handler returnsfalse, takes a 401 only on the first BASIC request to a host, and reuses pooled connections instead of sendingConnection: close.Query handlers run on the caller's thread. The synchronous
AbstractRemoteConnection.query()no longer delegates toqueryAsync(). The I/O thread only buffers the response (BufferedResponseConsumer). The caller then parses the body and calls the handler. Previously, the script'shandleResourceand, withproducerBufferSize0 or a full buffer, the ICFResultsHandlerran on anI/O dispatcherthread. A synchronous CREST call made from such a handler hung as soon as its connection landed on that dispatcher. The exception mapping ofquery()is unchanged:ResourceExceptionreaches the caller as is;Throwableraised before the response arrives becomes anInternalServerErrorException;adapt;ServiceUnavailableException.queryAsyncand the publicQueryResultResponseHandlerare unchanged. AqueryAsynchandler still runs on the I/O thread. Its Javadoc now says so, and says thatquery()does not call it, so a subclass that overridesqueryAsyncto change the request must overridequeryas well. The body is still buffered in full.A
falsefrom the handler stops the query.StreamingJsonSlurperstops parsing at once and skips its callback.parseQueryResponsethen returns aQueryResponsewithout a paged-results cookie and leaves the rest of the response unread. This also applies toqueryAsync, which uses the sameparseQueryResponse.Only the first BASIC request takes a 401. After the customizer's
beforeRequest,ScriptedCRESTConfiguration.executeinstalls a sharedSharedBasicAuthCacheinto a request context that has noAuthCache. The first successful challenge fills it, and later requests send their credentials up front.BasicSchemes. A Digest scheme carries a nonce count that concurrent requests must not share.BasicAuthCacheis a plainHashMapbefore HttpClient 4.4.getreturns a newBasicSchemefor every request, with the cached scheme's credentials charset and proxy/target state. On httpclient 4.3.x, which the bundle imports,BasicSchemeencodes through a shared commons-codecBase64instance and is not thread-safe. One instance shared by concurrent requests produced wrongAuthorizationheaders. The copy does not carry the realm: the Basic header does not use it, and preemptive authentication looks up the credentials for any realm.AuthCacheis left alone.BASIC_PREEMPTIVEputs one there: the default customizer now fills aSharedBasicAuthCachewith aBasicSchemeinstead of a plainBasicAuthCache, so these requests get their own copy too. Before, all of them shared oneBasicScheme, andauthFailedandauthSucceededchanged the cache'sHashMapon the I/O threads.executenow builds the client, which runs the customizer'sinit, before it runsbeforeRequest.initfills thepropertyBagthatbeforeRequestreads, so the first request of a client went out without theBASIC_PREEMPTIVEAuthCacheand without the customizer's cookie store, and withBASIC_PREEMPTIVEit took a 401.Connections are reused. The
Connection: close, TEheader is gone.getHttpAsyncClient()now setsIdleKeepAliveStrategyon the builder before the customizer'sinitclosure runs, so a custom customizer can still replace it.Keep-Alive: timeoutallows, and for at most 10 s. That is below the idle timeouts of common servers (Jetty 30 s, Tomcat 20 s). Atimeout=0expires it at once (1 ms), because the pool keeps a duration of 0 forever. The pool closes an expired connection the next time it would lease it. The I/O reactor closes a connection as soon as the server closes it.The default customizer limits connect and pool waits. A connect attempt fails after 10 s. A request that is still waiting for a pooled connection after 60 s fails. The pool checks that deadline only when it releases a connection or a connect attempt ends, so a wait can run over. The socket timeout stays unlimited, because a long query may send nothing until its result is ready. The proxy branch now copies these settings and adds the proxy. Before, it replaced the whole
RequestConfig.Behaviour changes:
Errorthrown by a handler now reaches the caller (it used to stop the dispatcher and hang the caller);falsefrom a handler ends the results, without a cookie;BASIC_PREEMPTIVE, the first request of a client no longer takes one either;BASIC, the requests after the first look up their credentials for any realm of the host and port, asBASIC_PREEMPTIVEalready did. A customizer that registers credentials per realm on one host and port may then send the wrong ones, and that request fails on the 401 without a retry.builder.disableAuthCaching()ininit, or an ownAuthCacheset inbeforeRequest, brings back a challenge on every request;Remaining risk: a server can close an idle connection just as the client reuses it. That request then fails, because httpasyncclient 4.0.2 does not retry. The 10 s cap keeps reuse below the idle timeouts of common servers.
Not changed:
read,create,update,patch,deleteandactionparse their responses (none of them calls user code).Test strength
query()parses on the callerScriptedCRESTConnectorTest#testSearchHandlerRunsOnTheCallingThreadI/O dispatcher 4)#testNestedGetObjectFromTheSearchHandlerCompletesquery()revert, because keep-alive keeps the nested calls on one pooled connection off the handler's dispatcher —#testSearchHandlerRunsOnTheCallingThreadis the pinInternalServerErrorException#testHandlerExceptionReachesTheCallerAsInternalServerErrorNotFoundException#testQueryOfAMissingResourceFailsWithNotFoundErrorfromexecute→InternalServerErrorExceptionAbstractRemoteConnectionTest#testQueryWrapsAnErrorFromExecuteInInternalServerErrorServiceUnavailableException#testInterruptedQueryFailsWithServiceUnavailableinterrupt()removed it fails on its 10 stimeOutinstead of hangingResourceExceptionpasses through#testQueryPassesATransportResourceExceptionThroughthrow adapt(t)→throw new InternalServerErrorException(t)in the wait phase killedfalsestops the queryScriptedCRESTConnectorTest#testSearchStopsWhenTheHandlerReturnsFalseexpected [1] but found [2])StreamingJsonSlurperTest(5)parseObjectfails 4 of themScriptedCRESTConnectorTest#testOperationsAfterTheFirstDoNotTakeA401expected [0] but found [20])SharedBasicAuthCacheTestkeep/drop/removeSharedBasicAuthCacheTest#testGivesEachRequestItsOwnScheme#testCopyKeepsTheProxyState,#testCopyKeepsTheCredentialsCharsetget→new BasicScheme()killedBASIC_PREEMPTIVEscheme copy per requestScriptedCRESTConfigurationTest#testPreemptiveBasicGivesEachRequestItsOwnSchemeexpected not same [BASIC] but found [BASIC]); dropping the pre-filled scheme fails it tooBASIC_PREEMPTIVEfirst request,initbeforebeforeRequest,AuthCacheguardScriptedCRESTConnectorTest#testPreemptiveBasicNeverTakesA401expected [0] but found [1]); mutantsif (true)for the guard and dropping the pre-filled scheme killedScriptedCRESTConfigurationTest#testDefaultCustomizerBoundsConnectAndPoolWaits,#testDefaultCustomizerKeepsTheTimeoutsWithAProxyexpected [10000] but found [-1])timeout=0IdleKeepAliveStrategyTest(4)#testExpiresAZeroAnnouncedTimeoutAtOncered at ff4bc0f (expected [1] but found [10000])ScriptedCRESTConfigurationTest#testClientDropsConnectionsAfterTenIdleSecondsDefaultConnectionKeepAliveStrategy)ScriptedCRESTConnectorTest#testOperationsReuseOneConnectionexpected [1] but found [20])Not pinned by a test:
synchronizedonSharedBasicAuthCache;mvn -pl OpenICF-groovy-connector test:Tests run: 175, Failures: 0, Errors: 0, Skipped: 42. The 42 skipped tests are the five*SampleTestclasses, which need a private configuration.