-
-
Notifications
You must be signed in to change notification settings - Fork 79
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
Adding Per Message Compression Extensions from RFC-7692. #142
Conversation
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 quick initial notes on this - the PR needs tidying up to start with, there's extra whitespace added, commented out code, removed tests etc
The main issue is adding a dependency on something that is not yet 1.0.0 - this is a blocker as we can't guarantee API breaking changes. This will need to be tagged a 1.0.0 before we can consider the PR (or inline the code we need)
@0xTim You are not serious, are you? As much as I like Vapor and use it every day, websocket-kit has had multiple breaking changes in the last years. Most recently, one of the latest releases (with the Sendable stuff) broke literally every single app I had using websocket-kit directly, within a minor version. And I can prove it if you need to. There is literally no QA going on in any of the Vapor repositories. I trust @adam-fowler with compress-nio in a pre-1.0 version 10000% more than any of the Vapor repositories, especially websocket-kit, to not introduce any breaking changes. Copying over code will very surely introduce another weak point in a barely maintained repository. I and surely many others would much rather depend on a maintained version of code doing a crucial part of what's being implemented in this PR. If you really insist, why not simply version-lock it? Or reduce to bug-fix releases only? And then manually update the dependency if it makes (probably non-breaking) minor bumps. This PR is very good and feature-complete. It even takes care of edge cases covered in the RFC. It introduces a long-awaited feature that is a crucial part of any real websocket library in any language. Vapor being the de-facto library used for Swift backends, you are the only one of any languages I know without websocket compression support. I would put priority on getting this merged ASAP. A thorough code review will follow. |
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.
Code quality should really be improved, especially logging. And tests need to be added. I think this is a great start for a great addition. I'll have to read up on PMCE, but I think that the implementation is definitely a great addition and pretty well implemented.
Sources/WebSocketKit/WebSocket.swift
Outdated
} | ||
|
||
@preconcurrency public func onText(_ callback: @Sendable @escaping (WebSocket, String) -> ()) { | ||
print("\(self.onTextCallback._eventLoop)") |
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.
Print statements must be removed
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.
PMCE RFC is somewhat obtuse. I'd be very surprised if it were perfectly implemented currently but it has worked against 4 different servers in my lab tests. These are all good suggestions and ive addressed most of them I think.
Sources/WebSocketKit/WebSocket.swift
Outdated
self.channel.writeAndFlush(frame, promise: promise) | ||
} | ||
//using compression | ||
if pmce != nil && pmce!.enabled { |
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.
Please if let
unwrap this, instead of force unwrapping
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.
Sources/WebSocketKit/WebSocket.swift
Outdated
} | ||
} | ||
catch { | ||
print("websocket-kit: \(error)") |
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.
Get rid of this print statement, and add some error handling. Of course, you can keep logging in here through Logger
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.
Im not sure what error handling there would be other than logging the underlying error in the catch.
Sources/WebSocketKit/WebSocket.swift
Outdated
} | ||
else if frame.rsv1 && pmce == nil { | ||
if pmce?.logging ?? false { | ||
print("websocket-kit: PMCE: received compressed frame without PMCE configured! Closing per RFC-7692. You could have a configuration 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.
Use swift-log
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.
That would mean adding a dep to websocket kit for it I assume. I had looked into it possibly but thought it kinda out of scope and thought if ppl wanted logger there they would have one by now and maybe there was a reason websocket had no logger.
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.
How is logging handled right now in websocket-kit? Simply no logging or with prints? If it's no logging I would keep it that way and make a separate PR eventually adding proper logging and the dependency.
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.
It was no logging. I asked Tim and he agreed it shold be added. I can't make another fork though so I just did it on this PR. It was kind of a pain point to debug it without logging so I added what I could after asking around for a while. But logging has been added via swift log to websocket and pmce classes with a boxed bool to enable with a defeat of false. that way you can turn it on for real time issues without needed a fork to add 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.
Logging is added now on main.
self.maxFrameSize = maxFrameSize | ||
self.minNonFinalFragmentSize = 0 | ||
self.maxAccumulatedFrameCount = Int.max | ||
self.maxAccumulatedFrameSize = Int.max |
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 the max frame sizes should ever be set to .max
implicitly.
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.
that might be a good catch. Not sure where I saw that from, thought I pulled it from elsewhere in the codebase.
Do you know what default should be?
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 have a specific requirement. But I tend to require the API consumer to think about this. In our case - Vapor itself
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 feel like I copied it from elsewhere, but could have pulled it from somehwere... I just looked. I got it from the pre existing TLS init I was basing adding pmce arg on public init( tlsConfiguration: TLSConfiguration? = nil, maxFrameSize: Int = 1 << 14 )
so I was using vapor as the guide as far as I could tell.
I do see ur point of the caller should set that but I see no way for them to pass it on init.
Seeing as it's all sendable now, shouldn't those be loop bound to be mutable? That would prevent them from chasing it after the fact.
Im surprised it doesnt warn me. as it did on WebSocket having mutable state while being sendable. am confused.
override func setUp() async throws { | ||
fflush(stdout) | ||
} | ||
|
||
override func tearDown() { | ||
try! self.elg.syncShutdownGracefully() |
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.
Not a fan of force unwrapped optionals. And try!
. You can make tearDown()
throwing.
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 think those were there originally I dont recall writing them.
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.
Ive removed the !.s I can find. Not sure what to do about the ones some other maintainer left in there.
I can try if its important but feel it could unravel things maybe.
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.
If another user did that, I don't think that's your responsibility to fix it 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.
I dont mind fixing them, but without understanding the test I cant say my change would still be a valid test. I did that in the cases I did change, like with the URL, but didnt want to go down the rabbit hole like a cowboy. Someone should do it I think. Since I already fixed deprecations without asking it's kinda six of one half dozen of another.
@@ -279,42 +287,6 @@ final class WebSocketKitTests: XCTestCase { | |||
try server.close(mode: .all).wait() | |||
} | |||
|
|||
func testLocally() throws { |
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 was this removed?
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 thought it was something I had added to test locally. It hangs forever the only test that didnt ever pass .
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 adding what I mentioned in discord that I removed it because it was skipped anyway and mistakenly thought had made it due to its vague name as I do that when drafting a test. Especially since not skipped it would hang forever.
Adding it back just adds a skipped taste is the takeaway I htink.
@@ -479,7 +451,9 @@ final class WebSocketKitTests: XCTestCase { | |||
} | |||
|
|||
func testBadURLInWebsocketConnect() async throws { | |||
XCTAssertThrowsError(try WebSocket.connect(to: "%w", on: self.elg, onUpgrade: { _ in }).wait()) { | |||
// %w seems to now get to NIO and it attempts to connect to localhost:80 ... empty string makes the test pass | |||
XCTAssertThrowsError(try WebSocket.connect(to: "", on: self.elg, onUpgrade: { _ in }).wait()) { |
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.
Again, let's make sure we don't change tests just to get things passing.
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 test is no longer valid and thus did not pass when it in fact should have passed because it wasnt testing what it said it was testing. it expects %w to return nil not a URL if I recall. As a QA engineer I take tests pretty seriously.
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'd add I tested this with a temporary test of URL init with both params and discerning what the test was testing, from that result and the test expectations. So I changed the setup to satisfy the expectation that existed prior.
I was surprised %w returned a URL and not nil.
var elg: EventLoopGroup! | ||
|
||
|
||
// compression usage 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.
Let's remove this old (commented) testing code
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 just noticed the tests I added to websocket kit dont seem to be 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.
Should be fixed now.
import NIOConcurrencyHelpers | ||
|
||
public final class WebSocket: Sendable { | ||
enum PeerType: Sendable { | ||
|
||
public enum PeerType: Sendable { |
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 need to be public
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 think it's actually needed to be public but I'll check once more. Seems it wa a couple days ago that I changed it so might not have the same api.
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 just checked and it needs to be public because types outside Websocket need it.
internal doesnt suffice because an inter needs it. I really think this should be like surface protein for a websocket. It's a perfectly fine enum and is useful for the classes around websocket to support its use.
…nd error where appropriate.
As per this comment you could consider integration testing with other implementations |
…integration tests as well.
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #142 +/- ##
===========================================
- Coverage 82.85% 63.11% -19.74%
===========================================
Files 6 7 +1
Lines 659 1231 +572
===========================================
+ Hits 546 777 +231
- Misses 113 454 +341
|
The coverage seems odd to drop so much. I only removed a skipped test and added some that werent there. I don't understand that result. |
@0xTim is right here to question relying on a package that hasn't hit v1.0. CompressNIO has had a number of breaking changes in the last few releases. Although many of those have been to add support needed by both this PR and the equivalent in hummingbird-websocket. To make things easier I will move compress-nio to v1.0 in the next week or so. |
@adam-fowler Thanks a ton. Your quick responsiveness and guidance has been instrumental in getting this done on time for me. |
@jhoughjr I just released compress-nio v1.0 so you can update to that and it won't be an issue anymore |
Thank you @adam-fowler |
Thought I pushed yesterday, seemed today it didnt go so pushed it now. |
Hey @jhoughjr any particular reason this was closed? With the recent changes and compression 1.0 this was getting into a good shape |
My first PR is PMCE support for WebSocket server and client.
There is an accompanying PR going to Vapor to support the changes to the WebSocketUpgrader to enable conditional use of PMCE.
I've tried to comment everything as descriptive as possible but without knowledge of the RFC, the code may not make obvious sense.
The changes are mostly encapsulated in the new PMCE class and its members.
Websocket's main changes are an optional PMCE which is Sendable and some optional delegation to it if configured and enabled for use in the send and handle methods.
WebSocketClient.Configuration also has an optional PMCE so the client can configure its side.
The public API is mainly through converting DeflateConfigs <> HTTPHeaders.
ie if you want to serve pace web sockets, create the desire DeflateConfig and grab its headers.
Pass them out of shouldUpgrade. As a client, do the same and pass them into the connect override with the ClientConfiguration.
I have some test apps and examples in another repo and am contiuning to write documentation.
I have tested client against a node.js , a go and a vapor/websocketkit (this) implementation of PMCE servers.
I have tested the vapor/websocket-kit server against the vapor/websocket-kit client, and a web browser client, all of which works, but I'm sure its not perfect yet.
I believe MahdiBM has some experience with this issue as well as adamfowler and may be useful ion review.
I can answer any questions in discord.