-
Notifications
You must be signed in to change notification settings - Fork 122
refactor pool #192
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
refactor pool #192
Conversation
cc @adtrevor |
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.
Thanks! I left a few comments but I'm not sure if removing the struct State
is the right call here. It makes it quite possible that we're calling out to the user whilst we hold a lock which we must prevent from happening. In the new code, it's unclear to me what bits call out and what bits don't so I think struct State
was actually a good idea.
assert(self.state.activity == .closing) | ||
return self.state.availableConnections | ||
self.lock.withLock { | ||
self.availableConnections.forEach { $0.close() } |
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.
$0.close()
might also be calling out
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.
same as above
@artemredkin Thank you very much for this!
I think currently we can't rely on the event loop correctly notifying remaining futures that it is shutdown, IIRC this was the related issue: apple/swift-nio#1309, the best option would be to fail the requests on shutdown IMO, it also gives a more consistent behaviour between client owned EL and user provided EL I guess?
Tell me if something doesn't make sense, I'll hopefully still remember the reasoning behind them 🙂
Agreed, that's not clean enough |
EDIT: It's actually an assertion in HTTPBin, so not too much problematic for now (and I think it existed prior to this) I hit this assertion in
|
…ient into refactor_pool
…ient into refactor_pool
@swift-server-bot test this please |
@swift-server-bot test this please |
This also closes #188 |
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.
@artemredkin awesome, that looks like a good improvement! Left some more comments.
indirect case parkAnd(Connection, Action) | ||
} | ||
|
||
struct ConnectionsState { |
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.
Should we maybe move the state machine to a separate file?
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.
good idea, done, thanks!
} | ||
|
||
var connectionProviderCount: Int { | ||
return self.connectionProvidersLock.withLock { | ||
self.connectionProviders.count | ||
return self.lock.withLock { |
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 need this connectionProviderCount
property? Seems racy so I'd just remove it.
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 use it one place - client deinit to assert that all providers are deleted (so no decisions based on it are made)
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.
@artemredkin in deinit
we don't need the lock. So we can just do
assert(self.providers.count == 0)
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.
👍
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.
done
var state = HTTP1ConnectionProvider.ConnectionsState(eventLoop: eventLoop) | ||
|
||
var snapshot = state.testsOnly_getInternalState() | ||
XCTAssertEqual(0, snapshot.availableConnections.count) |
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.
they look all great! Quick question: Why are we asserting on the (private) state and not the Action
s that the state machine returns? Examples here: https://github.com/apple/swift-nio-examples/blob/master/backpressure-file-io-channel/Tests/BackpressureChannelToFileIOTests/StateMachineTest.swift#L267-L309
For the example above, I extended the Action
to have lots of functions (one for each of the actions) like
@discardableResult
func assertOpenFile(_ check: (String) throws -> Void,
file: StaticString = #file,
line: UInt = #line) -> Self {
if case .openFile(let path) = self.main {
XCTAssertNoThrow(try check(path))
} else {
XCTFail("action \(self) not \(#function)", file: file, line: line)
}
return self
}
And then you can just write
self.stateMachine.someInput()
.assertActionIsX()
.assertWhateverElse()
and we wouldn't need to expose the state. But again, I think that's something we can do in the future.
The benefit of asserting on the Action
is that we can change the state machine representation to say an enum
without rewriting all the tests.
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 do assert on actions as well (not in this test specifically, but in other tests), I want to test not only action but the way state is changing. I test before the action to "show" what state I was expecting in this test and after to see if we arrived at it, also serves as a documentation of sorts for the test case
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.
Hmm, but the tests shouldn't care how the state machine achieves its goal, assuming it does, right? When we refactor, we'll lose all the 'private state tests'.
The state machine is fully deterministic, so there isn't anything you can't test with just inputs & actions. The state is fully determined by the inputs that have been fed.
As long as most of the tests do only action assertions, I think there's no harm caused also asserting on private state but I think it's a bit of an anti-pattern.
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 agree, but it can be a bit easier to test that when we aqcuire
a connection that it is inside the waiters for example, than to get a series of transformations to aquire
and then release some other connection and then test that our promise was succeeded
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.
Github’s broken, i was meaning to thumbs UP only but fat fingered a DOWN too which I can’t remove now 🤪
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.
Thank you! I think this is a good improvement and the remaining bits can be done iteratively from here on. Let’s get this in soon but maybe @Lukasa could give it a sanity check?
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.
Some minor notes here.
} | ||
return http1Provider | ||
let provider = HTTP1ConnectionProvider(key: key, eventLoop: eventLoop, configuration: self.configuration, pool: self) | ||
self.providers[key] = provider |
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.
Are we not missing a call to enqueue
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.
Ok, after reading through the entire PR I finally found out why we aren't. Can we add a comment here that states that providers begin at +1
?
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.
Done!
self.state = State(eventLoop: eventLoop, parentPool: parentPool, key: key) | ||
/// Sets idle timeout handler and channel inactivity listener. | ||
func setIdleTimeout(timeout: TimeAmount?) { | ||
_ = self.channel.pipeline.addHandler(IdleStateHandler(writeTimeout: timeout)).flatMap { _ in |
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.
Idle handlers should usually go first in the pipeline.
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.
Even if the timeout handling handler is added immediately after it? I don't think there are any handlers that can (or should) handle timeouts in the pipeline at this point...
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.
The reason they should go first in the pipeline is so that they observe the TCP writes/reads, rather than something more structured that may be delayed. Otherwise slow connections risk being killed for being idle, when in practice there's data flow occurring and just not being seen due to intermediate buffering.
This function is potentially callable at any time, so we'll risk fewer bugs by explicitly calling out that the idle state handler should go first. I'm fine for the actual timeout handler to go wherever.
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.
Ah! That makes sense, done
var description: String { | ||
return "HTTP1ConnectionProvider { key: \(self.key), state: \(self.state) }" | ||
} | ||
extension Connection: Hashable { |
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.
Minor nit, but I'd rather not conform something to Hashable
/Equatable
if its definition of hash value and equality is identity.
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? And what would you recommend instead?
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.
Users explicitly testing for identity and explicitly using ObjectIdentifier
as a hash key.
The main reason I object is that Swift pretty pervasively separates the idea of equality and identity. Indeed, the Equatable
docs call this out directly. In particular, being equatable implies that two objects are substitutable. If two objects are only equal if they are literally the same object, we have a way to express that already and do not need to bring equatability into the mix.
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 don't think there is a good way to use ObjectIdentifier
is a Set
, while I can add and test for contains
and remove, since ObjectIdentifier
is not generic, I cannot say leased.map { $0.close() }
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 you need a Set
? Why not just Array
?
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.
@Lukasa and 4) (which I think may be best):
fileprivate struct LeasedConnectionKey: Hashable {
private let connection: Connection
func ==(...) {}
func hash...
}
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.
Sure, works for me.
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 do add and remove connections constantly, its added on every new http call and removed when call is finished. This is a result from a simple benchmark (adds, removes, searches) on a small (8 element) array/set:
name time std iterations
----------------------------------------
array 3208.0 ns ± 29.53 % 425528
set 1677.0 ns ± 52.07 % 827306
In this case set is almost twice as fast...
I think I'll go with 4
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, the problem with Array will be the removals of elements that are not the last...
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.
done
case .failure(let error): | ||
self.connectFailed(error: error, waiter: waiter) | ||
} | ||
} |
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 block is repeated a few times, it seems like we could factor it out a bit.
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.
Good idea, done, thanks!
Ideas for connection pool refactoring. Main idea is that we need to simplify connection lifecycle. Right now its spread over many callbacks, that return some kind of potential action. I think this part can be simpler. There are basically two operations, polling a connection and releasing the connection. All magic should as much as possible be contained in those two methods.
One thing I'm still figuring out is how to simplify idle connection handling.
Closes #175 and #176
There are two things missing at the moment:
Other things to check:
we assert on
openedConnectionCount
on deinit, so we good there, only question is should we cancel those requests on shutdown or just let them fail when we stopeventLoop
IdlePoolConnectionHandler
stuff to connection pool file, otherwise we add idle handler in one place and remove in another