Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/NIO/Bootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
/// .serverChannelOption(ChannelOptions.backlog, value: 256)
/// .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
///
/// // Set the handlers that are appled to the accepted child `Channel`s.
/// // Set the handlers that are applied to the accepted child `Channel`s.
/// .childChannelInitializer { channel in
/// // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline.
/// channel.pipeline.add(handler: BackPressureHandler()).then { () in
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/ByteBuffer-core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ private func toIndex(_ value: Int) -> Index {
/// The 'discardable bytes' are usually bytes that have already been read, they can however still be accessed using
/// the random access methods. 'Readable bytes' are the bytes currently available to be read using the sequential
/// access interface (`read<Type>`/`write<Type>`). Getting `writableBytes` (bytes beyond the writer index) is undefined
/// behaviour and might yield aribitrary bytes (_not_ `0` initialised).
/// behaviour and might yield arbitrary bytes (_not_ `0` initialised).
///
/// ### Slicing
/// `ByteBuffer` supports slicing a `ByteBuffer` without copying the underlying storage.
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/Codec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ extension ByteToMessageDecoder {
/// Calls `decode` until there is nothing left to decode.
public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
var buffer = self.unwrapInboundIn(data)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind removing this whitespace?

if self.cumulationBuffer != nil {
self.cumulationBuffer!.write(buffer: &buffer)
buffer = self.cumulationBuffer!
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/CompositeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

/// An `Error` that represents multiple errors that occurred during an operation.
///
/// Because SwiftNIO frequently does multiple jobs at once, it is possible some operatons out
/// Because SwiftNIO frequently does multiple jobs at once, it is possible some operations out
/// of a set of operations may fail. This is most noticeable with `Channel.flush` on the
/// `DatagramChannel`, where some, but not all, of the datagram writes may fail. In this case
/// the `flush` should accurately represent that set of possibilities. The `NIOCompositeError`
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/EventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public protocol EventLoop: EventLoopGroup {
/// Submit a given task to be executed by the `EventLoop`. Once the execution is complete the returned `EventLoopFuture` is notified.
///
/// - parameters:
/// - task: The closure that will be submited to the `EventLoop` for execution.
/// - task: The closure that will be submitted to the `EventLoop` for execution.
/// - returns: `EventLoopFuture` that is notified once the task was executed.
func submit<T>(_ task: @escaping () throws -> T) -> EventLoopFuture<T>

Expand Down
4 changes: 2 additions & 2 deletions Sources/NIO/EventLoopFuture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ public struct EventLoopPromise<T> {
///
/// This means that for any `EventLoopFuture` that your code did not create itself (via
/// `EventLoopPromise.futureResult`), use of `hopTo` is **strongly encouraged** to help guarantee thread-safety. It
/// should only be elided when thread-safety is provably not needed.
/// should only be elided when thread-safety is proved to be not needed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need a change: Merriam Webster defines "provably" as the adjectival form of "prove".

///
/// The "thread affinity" of `EventLoopFuture`s is critical to writing safe, performant concurrent code without
/// boilerplate. It allows you to avoid needing to write or use locks in your own code, instead using the natural
Expand Down Expand Up @@ -734,7 +734,7 @@ extension EventLoopFuture {
/// Fulfill the given `EventLoopPromise` with the results from this `EventLoopFuture`.
///
/// This is useful when allowing users to provide promises for you to fulfill, but
/// when you are calling functions that return their own proimses. They allow you to
/// when you are calling functions that return their own promises. They allow you to
/// tidy up your computational pipelines. For example:
///
/// ```
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/FileRegion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/// Usually a `FileRegion` will allow the underlying transport to use `sendfile` to transfer its content and so allows transferring
/// the file content without copying it into user-space at all. If the actual transport implementation really can make use of sendfile
/// or if it will need to copy the content to user-space first and use `write` / `writev` is an implementation detail. That said
/// using `FileRegion` is the recommend way to transfer file content if possible.
/// using `FileRegion` is the recommended way to transfer file content if possible.
///
/// One important note, depending your `ChannelPipeline` setup it may not be possible to use a `FileRegion` as a `ChannelHandler` may
/// need access to the bytes (in a `ByteBuffer`) to transform these.
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/LinuxCPUSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import CNIOLinux
/// The ids of all the cpus.
let cpuIds: Set<Int>

/// Create a new instace
/// Create a new instance
///
/// - arguments:
/// - cpuIds: The `Set` of CPU ids. It must be non-empty and can not contain invalid ids.
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/MarkedCircularBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public struct MarkedCircularBuffer<E>: CustomStringConvertible, AppendableCollec

// MARK: Marking

/// Marks the buffer at the current index, making the last idex in the buffer marked.
/// Marks the buffer at the current index, making the last index in the buffer marked.
public mutating func mark() {
let count = self.buffer.count
if count > 0 {
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/PendingDatagramWritesManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ extension PendingDatagramWritesState {
/// value. The most important purpose of this object is to call `sendto` or `sendmmsg` depending on the writes held and
/// the availability of the functions.
final class PendingDatagramWritesManager: PendingWritesManager {
/// Storage for mmsghdr strutures. Only present on Linux because Darwin does not support
/// Storage for mmsghdr structures. Only present on Linux because Darwin does not support
/// gathering datagram writes.
private var msgs: UnsafeMutableBufferPointer<MMsgHdr>

Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/Selector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ final class Selector<R: Registration> {

/* this is technically a bad idea as we're abusing ARC to deallocate scarce resources (a file descriptor)
for us. However, this is used for the event loop so there shouldn't be much churn.
The reson we do this is because `self.wakeup()` may (and will!) be called on arbitrary threads. To not
The reason we do this is because `self.wakeup()` may (and will!) be called on arbitrary threads. To not
suffer from race conditions we would need to protect waking the selector up and closing the selector. That
is likely to cause performance problems. By abusing ARC, we get the guarantee that there won't be any future
wakeup calls as there are no references to this selector left. 💁
Expand Down
2 changes: 1 addition & 1 deletion Sources/NIO/Thread.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ final class Thread {
/// - name: The name of the `Thread` or `nil` if no specific name should be set.
/// - body: The function to execute within the spawned `Thread`.
static func spawnAndRun(name: String? = nil, body: @escaping (Thread) -> Void) {
// Unfortunally the pthread_create method take a different first argument depending on if its on linux or macOS, so ensure we use the correct one.
// Unfortunately the pthread_create method take a different first argument depending on if its on linux or macOS, so ensure we use the correct one.
#if os(Linux)
var pt: pthread_t = pthread_t()
#else
Expand Down
10 changes: 7 additions & 3 deletions Sources/NIOChatServer/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ final class LineDelimiterCodec: ByteToMessageDecoder {
///
/// As we are using an `MultiThreadedEventLoopGroup` that uses more then 1 thread we need to ensure proper
/// synchronization on the shared state in the `ChatHandler` (as the same instance is shared across
/// child `Channel`s). For this a serial `DispatchQueue` is used when we modify the shared state (the `Dictonary`).
/// child `Channel`s). For this a serial `DispatchQueue` is used when we modify the shared state (the `Dictionary`).
/// As `ChannelHandlerContext` is not thread-safe we need to ensure we only operate on the `Channel` itself while
/// `Dispatch` executed the submitted block.
final class ChatHandler: ChannelInboundHandler {
Expand Down Expand Up @@ -119,7 +119,7 @@ let bootstrap = ServerBootstrap(group: group)
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)

// Set the handlers that are appled to the accepted Channels
// Set the handlers that are applied to the accepted Channels
.childChannelInitializer { channel in
// Add handler that will buffer data until a \n is received
channel.pipeline.add(handler: LineDelimiterCodec()).then { v in
Expand Down Expand Up @@ -177,7 +177,11 @@ let channel = try { () -> Channel in
}
}()

print("ChatServer started and listening on \(channel.localAddress!)")
if let localAddress = channel.localAddress {
print("ChatServer started and listening on \(localAddress)")
} else {
print("ChatServer started but the address could not be determined.")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to change these: we will always know our local address at this stage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa I was unsure about that one. Sure, this can't actually happen but then always having a always having a force unwrap here is also not great in the examples... So I thought that is okay and I thought @normanmaurer approved too but he actually didn't, he just ran the tests. Apologies!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my motivation in making this change. I wanted to remove the force unwraps in the examples because I know that avoiding force unwraps is a common Swift development technique.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, blindly replacing force unwraps with conditional binding is no better than blindly force unwrapping values. A force unwrap should only be used when the value being optional would represent programmer error, or when it is literally not possible to recover from the optional case.

Put another way, this is a perfectly good force-unwrap that (I hope) no-one would suggest we should replace with guard let:

let x = URL(string: "https://github.com/")!

A more complex example that I consider to be the same is this code block:

private func parseRecordHeader(buffer: inout ByteBuffer) throws -> Int {
// First, the obvious check: are there enough bytes for us to parse a complete
// header? Because of this check we can use unsafe integer reads in the rest of
// this function, as we'll only ever read tlsRecordHeaderLength number of bytes
// here.
guard buffer.readableBytes >= tlsRecordHeaderLength else {
throw InternalSniErrors.recordIncomplete
}
// Check the content type.
let contentType: UInt8 = buffer.readInteger()!
guard contentType == tlsContentTypeHandshake else {
// Whatever this is, it's not a handshake message, so something has gone
// wrong. We're going to fall back to the default handler here and let
// that handler try to clean up this mess.
throw InternalSniErrors.invalidRecord
}
// Now, check the major version.
let majorVersion: UInt8 = buffer.readInteger()!
guard majorVersion == 3 else {
// A major version of 3 is the major version used for SSLv3 and all subsequent versions
// of the protocol. If that's not what this is, we don't know what's happening here.
// Again, let the default handler make sense of this.
throw InternalSniErrors.invalidRecord
}
// Skip the minor version byte, then grab the content length.
buffer.moveReaderIndex(forwardBy: 1)
let contentLength: UInt16 = buffer.readInteger()!
return Int(contentLength)
}

This block contains three force-unwraps, but all of them are "safe" because of the guard at the top of the function. If any of those force-unwraps ever fails, that failure will be because I have made an error in assuming that tlsRecordHeaderLength is at least 5, or because of an error in the ByteBuffer implementation. Throwing an error in that case would be entirely misleading. This is exactly like force-unwrapping the URL above: the force unwrap is justified by my other code, and I simply cannot express to the compiler that I know this will always succeed in any way other than a force unwrap.

Returning to localAddress: my argument is that NIO makes a promise that localAddress will always be well-defined and non-optional once the bind promise returns, until the file descriptor is closed. If that promise is not kept, NIO is quite severely wrong about the state of the world, and it should probably crash rather than keep running in this confused state. For this reason, I consider the force-unwrap here to be totally safe. It's a short way to say "this value being nil here indicates a malfunctioning program".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa agreed if you can know that it can never be nil and that is the case for the code you quote.
But we're talking about the example code here and users don't know this can not be nil as they're not NIO devs.
I don't really have a good suggestion because we can't change the API (as not all channels have localAddresses) but we also shouldn't make everybody force unwrap here I don't think. Maybe the solution is to have a String returning function that is non-optional and just fills in "<not available>" or something if unavailable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, isn't the solution to this to write a comment that explains the force unwrap?

Let me reframe that. Right now the patch gives an equally misleading indication about the way this API behaves, because it implies that sometimes NIO may just be unable to find the local address for no good reason ("address could not be determined"). That's not right: there are well defined cases when the address can be found, and well defined cases where it cannot. I can enumerate them right now:

  1. getsockname has no return value, which means that there is no bound address on the machine at this time, meaning either the socket is closed or was never bound.
  2. SocketAddress cannot parse the return value of getsockname, which means this is not an address family that NIO natively understands.
  3. A coding error where NIO has dropped the ball somehow, and a valid SocketAddress-representable socket address was not correctly parsed or returned by the OS.

If the goal of this is to educate users about our APIs, we should do that here by placing a comment that says, basically: "We know the local address can be represented in SocketAddress because we just did so earlier, and we know that NIO promises that if it can represent the local address in a SocketAddress then it will definitely return it from localAddress after bind but before we call close. As a result we can safely force unwrap this result here, as a nil value represents a bug either in this code or NIO."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, if we really hate the force-unwrap, we can do guard let ... else { fatalError() }, but that's just a long way of spelling !.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa everything you write is totally accurate, yet often the user may just want to print a string and I think many people just don't like to force unwrap for that...
And sure, a better comment would probably help here to give the users confidence on when then can force unwrap this 'safely'.

I don't really know what to do here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa and @weissi Thank you for much for the insightful feedback. After reading through the explanation on specific cases where the local address could not be found, I agree that the, "address could not be determined," message is misleading. I also agree that a force unwrap failure here signifies a much larger issue in NIO. I do however feel that more accurate information on why the force unwrap failed would be beneficial as well. As a middle ground between using a force unwrap and the current patch in place I think the best option would be to use a guard statement to unwrap localAddress. If a failure should ever occur I an error message provides more specific information on what could be happening in these specific failures.

The new PR addressing this feedback is up here: #273
Thank you very much for the time and the explanation provided.


// This will never unblock as we don't close the ServerChannel.
try channel.closeFuture.wait()
Expand Down
6 changes: 5 additions & 1 deletion Sources/NIOHTTP1Server/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,11 @@ let channel = try { () -> Channel in
}
}()

print("Server started and listening on \(channel.localAddress!), htdocs path \(htdocs)")
if let localAddress = channel.localAddress {
print("Server started and listening on \(localAddress), htdocs path \(htdocs)")
} else {
print("Server started but the address could not be determined, htdocs path \(htdocs)")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to change these: we will always know our local address at this stage.


// This will never unblock as we don't close the ServerChannel
try channel.closeFuture.wait()
Expand Down
46 changes: 44 additions & 2 deletions Sources/NIOWebSocketServer/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,51 @@ defer {
try! group.syncShutdownGracefully()
}

// First argument is the program path
let arguments = CommandLine.arguments
let arg1 = arguments.dropFirst().first
let arg2 = arguments.dropFirst(2).first

let channel = try! bootstrap.bind(host: "localhost", port: 8888).wait()
print("Server started and listening on \(channel.localAddress!)")
let defaultHost = "localhost"
let defaultPort = 8888

enum BindTo {
case ip(host: String, port: Int)
case unixDomainSocket(path: String)
}

let bindTarget: BindTo
switch (arg1, arg1.flatMap(Int.init), arg2.flatMap(Int.init)) {
case (.some(let h), _ , .some(let p)):
/* we got two arguments, let's interpret that as host and port */
bindTarget = .ip(host: h, port: p)

case (let portString?, .none, _):
// Couldn't parse as number, expecting unix domain socket path.
bindTarget = .unixDomainSocket(path: portString)

case (_, let p?, _):
// Only one argument --> port.
bindTarget = .ip(host: defaultHost, port: p)

default:
bindTarget = .ip(host: defaultHost, port: defaultPort)
}

let channel = try { () -> Channel in
switch bindTarget {
case .ip(let host, let port):
return try bootstrap.bind(host: host, port: port).wait()
case .unixDomainSocket(let path):
return try bootstrap.bind(unixDomainSocketPath: path).wait()
}
}()

if let localAddress = channel.localAddress {
print("Server started and listening on \(localAddress)")
} else {
print("Server started but the address could not be determined.")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to change these: we will always know our local address at this stage.


// This will never unblock as we don't close the ServerChannel
try channel.closeFuture.wait()
Expand Down