- Notifications
You must be signed in to change notification settings - Fork 9
Implement a get request #14
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
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.
Great start! Left some comments
| | ||
| struct GetCommand { | ||
| let key: String | ||
| var flags: [MemcachedFlag] |
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 we want to use something more specialised than an array here. We probably should create a MemcachedFlags struct instead that holds all of the flags inline. We know what flags exists so we don't need the dynamism that array offers us.
| out.writeBytes(command.key.utf8) | ||
| | ||
| // write flags if there are any | ||
| for flag in command.flags { |
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 we should create MemcachedFlags struct and create a separate extension method on ByteBuffer that offers a mutating func write(flags: MemcachedFlags)
| | ||
| var returnCode: ReturnCode | ||
| var dataLength: UInt64? | ||
| var flags: [MemcachedFlag]? |
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 here let's not use an array and lets add a mutating func readMemcachedFlags() -> MemcachedFlags to ByteBuffer
| self.nextStep = .dataLengthOrFlag(returnCode) | ||
| return .continueDecodeLoop | ||
| | ||
| case .dataLengthOrFlag(let returnCode): |
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 am wondering if we should separate this into two steps one dataLength and one flag and we can decide what the next step is in the case above when we decoded the return code.
| if returnCode == .VA { | ||
| // TODO: Implement decoding of data length | ||
| fatalError("Decoding for VA return code is not yet implemented") | ||
| guard let dataLength = buffer.readInteger(as: UInt64.self) else { |
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.
Is this correct? Is the data length encoded as a UInt64 or rather written out as text?
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.
you are correct, it is written out as text, so I will make that change as well!
| return .returnDecodedResponse(response) | ||
| | ||
| case .decodeValue(let returnCode, let dataLength, let flags): | ||
| guard buffer.readableBytes >= dataLength else { |
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 should probably check here for dataLength + carriageReturn + newLine. To make sure we are not decoding the data until we also have the returns
| var data: ByteBuffer = .init() | ||
| for _ in 0..<dataLength { | ||
| let byte = buffer.readInteger(as: UInt8.self) | ||
| data.writeInteger(byte!) |
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 is a method to read a slice of a buffer which we should use here readSlice(length:)
| nextByte == UInt8.carriageReturn, | ||
| let nextNextByte = buffer.readInteger(as: UInt8.self), | ||
| nextNextByte == UInt8.newline else { | ||
| return .waitForMoreBytes |
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.
When we change the logic here to guard that we have these bytes readable as well we should probably fatalError here because it should never happen
| /// | ||
| /// - parameters: | ||
| /// - integer: The MemcachedFlag to serialize. | ||
| mutating func write(flags: MemcachedFlags) { |
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.
Naming NIT:
| mutating func write(flags: MemcachedFlags) { | |
| mutating func writeMemcachedFlags(_ flags: MemcachedFlags) { |
| /// If a flag is set, its corresponding byte value and a whitespace character is written into the ByteBuffer. | ||
| /// | ||
| /// - parameters: | ||
| /// - integer: The MemcachedFlag to serialize. |
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 doc is outdated
| //===----------------------------------------------------------------------===// | ||
| | ||
| struct MemcachedFlags { | ||
| var v: Bool = false |
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.
Can we give this a good name of what it does and add a doc comment?
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 should also probably be optional since all flags are optional.
| extension MemcachedFlags: Equatable { | ||
| static func == (lhs: MemcachedFlags, rhs: MemcachedFlags) -> Bool { | ||
| return lhs.v == rhs.v | ||
| } | ||
| } | ||
| | ||
| extension MemcachedFlags: Hashable { | ||
| func hash(into hasher: inout Hasher) { | ||
| hasher.combine(v) | ||
| } | ||
| } |
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.
You don't have to implement those. The compiler will synthesise them for you if you write
extension MemcachedFlags: 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.
great catch, didnt think about that!
| var bytes: Set<UInt8> { | ||
| var result = Set<UInt8>() | ||
| for (keyPath, byte) in Self.flagToByte { | ||
| if self[keyPath: keyPath] { | ||
| result.insert(byte) | ||
| } | ||
| } | ||
| return result | ||
| } |
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 shouldn't allocate the intermediate Set here. We can do all of this in the writeMemcachedFlags method. Just go over each field of the struct, check if it is not-nil and write it out.
| extension UInt8 { | ||
| static var size: Int { MemoryLayout<UInt8>.size } |
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 using this anywhere?
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.
correct no longer needed!
| static let flagToByte: [KeyPath<MemcachedFlags, Bool?>: UInt8] = [ | ||
| \MemcachedFlags.shouldReturnValue: 0x76, | ||
| ] |
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 this is going to work for all flags. Some of the flags carry actual value e.g. a numeric TTL. I think we really just have to spell this out in the write method. Like just go field by field of the struct and check if it is optional or not.
| } | ||
| } | ||
| | ||
| extension MemcachedFlags: Equatable {} |
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.
You can remove this since Hashable implies Equatable
| extension MemcachedFlags: Equatable {} |
| /// | ||
| /// - returns: An instance of `MemcachedFlags` containing the flags read from the buffer. | ||
| mutating func readMemcachedFlags() -> MemcachedFlags { | ||
| var flagBytes: Set<UInt8> = [] |
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.
Allocating this set is unnecessary. I would rather init an "empty" MemcachedFlags in the beginning and then read byte by byte. For now we drop everything that is not a v flag.
| /// - returns: An instance of `MemcachedFlags` containing the flags read from the buffer. | ||
| mutating func readMemcachedFlags() -> MemcachedFlags { | ||
| var flagBytes: Set<UInt8> = [] | ||
| while let nextByte = self.readInteger(as: UInt8.self), nextByte != UInt8.whitespace { |
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 this is correct. The next byte can be a whitespace since flags are separated by whitespaces. Furthermore, the next byte could be a newline + carriage return. We probably have to do this differently and handle it inside the decoder itself because we need to understand the state of the whole response, e.g. it depends what kind of response we are decoding if there is anything following the flags or not.
| /// Decode the flag | ||
| case flag(MemcachedResponse.ReturnCode, UInt64?) | ||
| /// Decode the next flag | ||
| case decodeNextFlag(MemcachedResponse.ReturnCode, UInt64?) |
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 do we need two different states here?
| | ||
| let returnCode = MemcachedResponse.ReturnCode(bytes) | ||
| self.nextStep = .dataLengthOrFlag(returnCode) | ||
| self.nextStep = .dataLength(returnCode) |
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 should check the return code here and decide if we transition to dataLength or flags
| case .flag(let returnCode, let dataLength): | ||
| let flags = buffer.readMemcachedFlags() | ||
| | ||
| guard buffer.readableBytes >= 2, | ||
| let nextByte = buffer.readInteger(as: UInt8.self), | ||
| nextByte == UInt8.carriageReturn, | ||
| let nextNextByte = buffer.readInteger(as: UInt8.self), | ||
| nextNextByte == UInt8.newline else { | ||
| preconditionFailure("Expected to find CRLF at end of response") | ||
| } | ||
| | ||
| self.nextStep = .decodeValue(returnCode, dataLength!, flags) | ||
| return .continueDecodeLoop | ||
| | ||
| case .decodeNextFlag(let returnCode, let dataLength): | ||
| while let nextByte = buffer.readInteger(as: UInt8.self), nextByte != UInt8.whitespace { | ||
| // for now consume the byte and do nothing with it. | ||
| // TODO: Implement decoding of flags | ||
| let flags = buffer.readMemcachedFlags() |
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 we can combine these two cases
| mutating func readMemcachedFlags() -> MemcachedFlags { | ||
| var flags = MemcachedFlags() | ||
| while let nextByte = self.readInteger(as: UInt8.self) { | ||
| switch nextByte { | ||
| case UInt8.v: | ||
| flags.shouldReturnValue = true | ||
| case UInt8.whitespace: | ||
| continue | ||
| case UInt8.carriageReturn: | ||
| guard let _ = self.readInteger(as: UInt8.self), self.readableBytes > 0 else { | ||
| break | ||
| } | ||
| default: | ||
| preconditionFailure("Unrecognized flag.") | ||
| } | ||
| } | ||
| return flags | ||
| } |
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 still see a couple of problems here:
- We have to verify after the carriage return that we get a newline
- More importantly it might be that our buffer doesn't contain the full flags yet. We have to support incremental parsing of flags. After thinking about this a bit, we should probably move the flag reading out of this method and into the state machine. We should be trying to read an individual flag + its token and stay in the
decodeFlagstate until we are done. We should also write a test for this where we only have a buffer that contains let's say the flag but not the carriage return and newline and send that as a second buffer into the decode method.
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 added a comment further down that basically says we should try to not do 2 yet since it makes everything a bit more complicated. However can you please add a test where you feed a partial buffer in that doesn't have everything i.e. only a \r and not the \n afterwards and then pass it in after and assert the right things happen.
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 will be handled in a future PR!
| | ||
| init() {} | ||
| | ||
| init(flagBytes: Set<UInt8>) { |
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 still need this?
| /// Decode the data length | ||
| case dataLength(MemcachedResponse.ReturnCode) | ||
| /// Decode the flags | ||
| case flags(MemcachedResponse.ReturnCode, UInt64?) |
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 rename this to decodeFlag and always try to decode a single flag at a time. We just have to update the flags struct and store it as a state here.
| case flags(MemcachedResponse.ReturnCode, UInt64?) | |
| case flags(MemcachedResponse.ReturnCode, UInt64?) |
| /// Decode the flags | ||
| case flags(MemcachedResponse.ReturnCode, UInt64?) | ||
| /// Decode the Value | ||
| case decodeValue(MemcachedResponse.ReturnCode, UInt64, MemcachedFlags?) |
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 need an optional for flags here we can just always have it since all its field are optional
| while let currentByte = buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), currentByte == UInt8.whitespace { | ||
| buffer.moveReaderIndex(forwardBy: 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.
How can there be more than 1 whitespace? Shouldn't we rather just read the next byte and make sure it is a whitespace. Then afterwards we check where the next whitespace is by just getInteger forward. Once we have confirmed that we have a whitespace in the buffer we try to reach the integer from ASCII.
| // Skip over the whitespace, newline or carriage return | ||
| while let currentByte = buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), | ||
| [UInt8.whitespace, UInt8.newline, UInt8.carriageReturn].contains(currentByte) { | ||
| buffer.moveReaderIndex(forwardBy: 1) | ||
| } | ||
| | ||
| let isNextByteWhitespace = buffer.getInteger(at: buffer.readerIndex) == UInt8.whitespace | ||
| self.nextStep = isNextByteWhitespace ? .flags(returnCode, dataLength) : .decodeValue(returnCode, dataLength, nil) |
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 we shouldn't do this here because we actually might have a short buffer. I would recommend that after reading the ASCII integer to transition to the next state which is decodeFlags. In that state we have to handle this whitespace/carriageReturn&newline logic.
| if nextByte == UInt8.whitespace { | ||
| self.nextStep = .decodeNextFlag(returnCode, nil) | ||
| case .flags(let returnCode, let dataLength): | ||
| let flags = buffer.readMemcachedFlags() |
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.
So this is where it gets a bit tricky and I would like us to take a shortcut for now. The problem is that we can have a partial buffer where we don't have the carriageReturn&newline yet and we would need to support reading partial flags. This also includes reading the partial tokens of flags which might be quite big. In a best world, we would go flag by flag and move ourselves forward. However, implementing this requires a bit of logic. What I would do for now is trying to scan the buffer forward to see if we have a \r\n in the buffer. If we do we are going to try and decode if not we are going to return needMoreData and let's create an issue for us to revisit this in the future.
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.
Sounds good, we will create a new issue to handle the partial buffer implementation.
| try self.testDecodeResponse(buffer: &buffer, expectedReturnCode: .NF) | ||
| } | ||
| | ||
| func testDecodeValueResponse() 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.
Yeah we need a couple of more tests here that are trying to pass partial responses and assert that we return the right things.
| // We were expecting a newline after the carriage return, but didn't get it. | ||
| return flags |
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 we can fatalError here for now. This shouldn't happen
| guard buffer.readableBytes >= 2, | ||
| let lastTwoBytes = buffer.getBytes(at: buffer.writerIndex - 2, length: 2), | ||
| lastTwoBytes[0] == UInt8.carriageReturn, | ||
| lastTwoBytes[1] == UInt8.newline else { | ||
| return .waitForMoreBytes | ||
| } |
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 check isn't correct. Instead of checking the last bytes we have to walk the buffer to find the first occurrence of \r\n since there might be more than one response in the buffer.
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.
modified this to find the first occurrence!
| // If it wasn't a newline, it could be the start of the data block. | ||
| return flags |
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 we need to fatalError if it is not a newline anything else is unexpected.
| // Encountered a character we weren't expecting. This could be the start of the data block. | ||
| return flags |
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 are not parsing the v flag anymore here. Additionally let's fatalError here as well for now since we don't support the other flags and the comment is wrong. It cannot be the start of the data block
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.
LGTM! Let's get this merged so that we can focus on the actual Connection actor
Updated our Encoder and Decoder to serialize/deserialize a
getMemcachedResponse. This pr closes #6.Motivation:
This marks the beginning for being able to handle get operations, as well as the foundation set up for building a soon to come Memcached Actor.
Modifications:
vforgetcommand added.getcommands in ourRequestEncoder.ByteBufferto allow us to read/writeMemcachedFlags.ByteBufferto allow us to read an integer from ASCII characters.ResponseDecoderdecoding states.Result:
We can now successfully encode and decode a
getMemcachedResponsethat can be sent to the Memcached server