-
Notifications
You must be signed in to change notification settings - Fork 7
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
Implementation of DatagramSocket #69
Conversation
Awesome! Yes, |
uring/src/main/scala/fs2/io/uring/net/UringDatagramSocket.scala
Outdated
Show resolved
Hide resolved
def apply[F[_]](ring: Uring[F], fd: Int)(implicit | ||
F: Async[F] | ||
): Resource[F, UringDatagramSocket[F]] = | ||
ResizableBuffer(8192).evalMap { buf => |
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 maximum size of a datagram is 65535
. So we can allocate a buffer of that size upfront (it doesn't need to be resizable). At least, that's how FS2 is doing it, maybe we can find an even better way.
https://github.com/typelevel/fs2/blob/5ac2a1148c1ecbdc4a3905a176ce3ac0fb369cc5/io/jvm/src/main/scala/fs2/io/net/AsynchronousDatagramSocketGroup.scala#L155
I am struggling with the read/msg method, could I get some assistance please? |
private[this] def recvmsg(msg: Ptr[msghdr], flags: Int): F[Int] = | ||
ring.call(io_uring_prep_recvmsg(_, fd, msg, 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.
So the msghdr
struct is defined like this in Scala Native:
type msghdr = CStruct7[
Ptr[Byte], // msg_name
socklen_t, // msg_namelen
Ptr[uio.iovec], // msg_iov
CInt, // msg_iovlen
Ptr[Byte], // msg_control
socklen_t, // msg_crontrollen
CInt // msg_flags
]
which is equivalent to this C declaration:
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
https://linux.die.net/man/2/recvmsg
So I think what we want to do is:
-
before calling
io_uring_prep_recvmsg
, we should setupmsghdr
with the pre-allocated buffer. I think we should provide it as themsg_iov
. -
after
io_uring_prep_recvmsg
completes, we can read the datagram out of the buffer. The return value will tell us how many bytes were recieved. Additionally, we can retrieve the address out ofmsg_name
usingmsg_namelen
as the length.
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 have clarified all my doubts, thank you very much :)
buf <- buffer.get(defaultReadSize) | ||
|
||
iov <- F.delay { | ||
val iov = stackalloc[uio.iovec]() |
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 the trick with stack allocations, is that they only exist for the duration of the function in which they are created. So in this case, the iov
allocation will actually go away as soon as the delay
completes, because a delay(...)
is actually a lambda function delay(() => ...)
.
I think we can allocate the iovec
and msghdr
structures at the same time as we allocate the read buffer when we create the DatagramSocket
. Because we can only have one read in progress at a time, we will only need one and the we can re-use it.
uring/src/main/scala/fs2/io/uring/net/UringDatagramSocket.scala
Outdated
Show resolved
Hide resolved
uring/src/main/scala/fs2/io/uring/net/UringDatagramSocket.scala
Outdated
Show resolved
Hide resolved
.evalMap { buf => | ||
(Mutex[F], Mutex[F]).mapN { (readMutex, writeMutex) => | ||
val iov = malloc(sizeof[uio.iovec]).asInstanceOf[Ptr[uio.iovec]] | ||
val remoteAddr = malloc(sizeof[sockaddr_storage]).asInstanceOf[Ptr[sockaddr_storage]] | ||
val msg = malloc(sizeof[msghdr]).asInstanceOf[Ptr[msghdr]] | ||
|
||
new UringDatagramSocket(ring, fd, buf, 65535, readMutex, writeMutex, iov, msg, remoteAddr) | ||
} | ||
} | ||
.flatMap { socket => | ||
val release: F[Unit] = F.delay { | ||
free(socket.iov.asInstanceOf[Ptr[Byte]]) | ||
free(socket.msg.asInstanceOf[Ptr[Byte]]) | ||
free(socket.remoteAddr.asInstanceOf[Ptr[Byte]]) | ||
} | ||
Resource.make(F.pure(socket))(_ => release) | ||
} |
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.
Instead of Resource.eval(...).flatMap(Resource.make(pure(...))(...))
we should just write Resource.make(...)(...)
🙂
ResizableBuffer(65535).evalMap { buf => | ||
(Mutex[F], Mutex[F]).mapN(new UringDatagramSocket(ring, fd, buf, 65535, _, _)) | ||
} | ||
ResizableBuffer(65535) |
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 did we go back to the resizable buffer? I think we can just allocate this with malloc along with the iov
and msghdr
.
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.
Yes, I still have to work on the buffer
val iov = malloc(sizeof[uio.iovec]).asInstanceOf[Ptr[uio.iovec]] | ||
val remoteAddr = malloc(sizeof[sockaddr_storage]).asInstanceOf[Ptr[sockaddr_storage]] | ||
val msg = malloc(sizeof[msghdr]).asInstanceOf[Ptr[msghdr]] |
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.
malloc
may return null
I think (due to out-of-memory) we should check for that.
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.
Here's an example:
val ptr = malloc(size.toUInt) | |
if (ptr == null) | |
throw new RuntimeException(s"malloc: ${errno}") |
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.
Would it be a good idea to make a more generic function in utils that allocates memory size given and check for null ?
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.
Yes, definitely! It can return a Resource[F, Ptr[Byte]]
that frees itself on release.
uring/src/main/scala/fs2/io/uring/net/UringDatagramSocket.scala
Outdated
Show resolved
Hide resolved
Is it worth considering the nonReizableBuffer class? Or is it better to just allocate a new buffer every time we need it with a malloc ? |
Yes I agree, I don't think that it needs a dedicated class. We can just use a pointer and store the size (which in this case is a constant anyway :) |
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 is looking really good! Ready to add some tests? We can re-use the ones from FS2.
def getLocalAddress[F[_]](fd: Int)(implicit F: Sync[F]): F[SocketAddress[IpAddress]] = | ||
F.delay { | ||
SocketAddressHelpers.toSocketAddress { (addr, len) => | ||
if (getsockname(fd, addr, len) == -1) | ||
Left(new IOException(s"getsockname: ${errno}")) | ||
else | ||
Either.unit | ||
} | ||
}.flatMap(_.liftTo) |
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 code is same as here right? Maybe we should move it to SocketAddressHelpers
so we can use it in both places.
fs2-io_uring/uring/src/main/scala/fs2/io/uring/net/UringSocket.scala
Lines 107 to 115 in d26ddb9
def getLocalAddress[F[_]](fd: Int)(implicit F: Sync[F]): F[SocketAddress[IpAddress]] = | |
F.delay { | |
SocketAddressHelpers.toSocketAddress { (addr, len) => | |
if (getsockname(fd, addr, len) == -1) | |
Left(new IOException(s"getsockname: ${errno}")) | |
else | |
Either.unit | |
} | |
}.flatMap(_.liftTo) |
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 :) Yes!! I am going to work on the tests and I think it's an excellent idea to move this piece of code to the SocketAdressHelper 👍
_ <- F.delay { | ||
iov._1 = buffer | ||
iov._2 = defaultReadSize.toULong | ||
} | ||
|
||
_ <- F.delay { | ||
msg.msg_name = remoteAddr.asInstanceOf[Ptr[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.
I think we can combine these delay
blocks, saves some performance.
Also defaultReadSize
will always be 65535
(maximum size of a datagram) so we can hardcode this constant, something like this maybe:
object UringDatagramSocket {
final val MaxDatagramSize = 65535
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 was thinking that it might be interesting to make a mallocResources function that takes a list of sizes and returns Resource[F,List[Ptr[Byte]]] so we can combine all the delays block in the apply function. What do you think ?
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, I was thinking about this as well! I was wondering if we can do:
mallocResource(sizeA + sizeB + sizeC).flatMap { ptr =>
val ptrA = ptr
val ptrB = ptr + sizeA
val ptrC = ptr + sizeA + sizeB
...
}
Then we can avoid the List
as well.
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's brilliant!
remoteAddress <- F.fromEither( | ||
SocketAddressHelpers | ||
.toSocketAddress(remoteAddr) | ||
) |
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.
Sorry, I think I made a mistake before. I think this does need to be suspended in a delay
block, because even though the conversion is not a side-effect, remoteAddr
is mutable memory and reading mutable memory is a side-effect.
sendto(ptr, slice.length, 0, addr, len).void | ||
} | ||
} | ||
.unlessA(datagram.bytes.isEmpty) |
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 did some googling and actually I think it's possible to send an empty datagram. So we should make sure that we can handle that case.
@@ -33,4 +37,14 @@ private[uring] object util { | |||
bytes | |||
} | |||
|
|||
def mallocResource[F[_]: Sync](size: CSize): Resource[F, Ptr[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.
👍
In the FS2 implementation we use a method to open a DatagramSocket. I was thinking about following that model and creating an UringDatagramSocketGroup class that would take care of opening the UringDatagramSocket. Then UringNetwork could take a new argument (UringDatagramSocketGroup) and create an openDatagramSocket method based on the UringDatagramSocketGroup one. What do you think ? |
Yes, definitely, that makes sense to me! |
I am working on implementing a UringDatagramSocket class by extending the DatagramSocket trait, and I have encountered an issue with the read function. Ideally, I was looking for a function in the io_uring library similar to recvfrom, but it doesn't exist. The closest alternative I found is recvmsg. However, I am not certain if using recvmsg is the best solution for implementing the read method in UringDatagramSocket. I would like to seek advice and opinions on whether this is a good approach, or if there is a more suitable alternative.
In addition to the read method implementation, I have also implemented the write method using the sendto function. I would appreciate any feedback or suggestions for potential improvements in the write method as well :)