Add offset writes and file size support to NonBlockingFileIO (#1408)

* Add offset writes and file size support to NonBlockingFileIO

Motivation:

NonBlockingFileIO currently only supports writes from 0 or appending,
if opened with O_APPEND. To facilitate random read/write file access
the API needs to evolve.

Modifications:

- Add toOffset function parameter to write for offset file write access
- Add changeFileSize function for growing or shrinking the file
- Add readFileSize function for asynchronously reading the file size

Result:

- Improve API for random access scenarios
- Provide method for reading and writing file size

* Conform to code review

* Formatting

* Update Sources/NIO/NonBlockingFileIO.swift

Co-Authored-By: Johannes Weiss <johannesweiss@apple.com>

* Update Sources/NIO/NonBlockingFileIO.swift

Co-Authored-By: Johannes Weiss <johannesweiss@apple.com>

* Modify offset handling to support partial writes.
Clean up stray comment.

* Fix precondition check within offset pwrite handling

* Improve guard for readability

* White space cleanup

* Single spacing issue

Co-authored-by: Johannes Weiss <johannesweiss@apple.com>
This commit is contained in:
Marcus Liotta 2020-03-02 12:02:05 +01:00 committed by GitHub
parent 9030326196
commit 5755c21a61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 186 additions and 10 deletions

View File

@ -349,6 +349,44 @@ public struct NonBlockingFileIO {
}
}
/// Changes the file size of `fileHandle` to `size`.
///
/// If `size` is smaller than the current file size, the remaining bytes will be truncated and are lost. If `size`
/// is larger than the current file size, the gap will be filled with zero bytes.
///
/// - parameters:
/// - fileHandle: The `NIOFileHandle` to write to.
/// - size: The new file size in bytes to write.
/// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
/// - returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error.
public func changeFileSize(fileHandle: NIOFileHandle,
size: Int64,
eventLoop: EventLoop) -> EventLoopFuture<()> {
return self.threadPool.runIfActive(eventLoop: eventLoop) {
try fileHandle.withUnsafeFileDescriptor { descriptor -> Void in
try Posix.ftruncate(descriptor: descriptor, size: off_t(size))
}
}
}
/// Returns the length of the file associated with `fileHandle`.
///
/// - parameters:
/// - fileHandle: The `NIOFileHandle` to read from.
/// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
/// - returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error.
public func readFileSize(fileHandle: NIOFileHandle,
eventLoop: EventLoop) -> EventLoopFuture<Int64> {
return self.threadPool.runIfActive(eventLoop: eventLoop) {
return try fileHandle.withUnsafeFileDescriptor { descriptor in
let curr = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_CUR)
let eof = try Posix.lseek(descriptor: descriptor, offset: 0, whence: SEEK_END)
try Posix.lseek(descriptor: descriptor, offset: curr, whence: SEEK_SET)
return Int64(eof)
}
}
}
/// Write `buffer` to `fileHandle` in `NonBlockingFileIO`'s private thread pool which is separate from any `EventLoop` thread.
///
/// - parameters:
@ -359,7 +397,29 @@ public struct NonBlockingFileIO {
public func write(fileHandle: NIOFileHandle,
buffer: ByteBuffer,
eventLoop: EventLoop) -> EventLoopFuture<()> {
var byteCount = buffer.readableBytes
return self.write0(fileHandle: fileHandle, toOffset: nil, buffer: buffer, eventLoop: eventLoop)
}
/// Write `buffer` starting from `toOffset` to `fileHandle` in `NonBlockingFileIO`'s private thread pool which is separate from any `EventLoop` thread.
///
/// - parameters:
/// - fileHandle: The `NIOFileHandle` to write to.
/// - toOffset: The file offset to write to.
/// - buffer: The `ByteBuffer` to write.
/// - eventLoop: The `EventLoop` to create the returned `EventLoopFuture` from.
/// - returns: An `EventLoopFuture` which is fulfilled if the write was successful or fails on error.
public func write(fileHandle: NIOFileHandle,
toOffset: Int64,
buffer: ByteBuffer,
eventLoop: EventLoop) -> EventLoopFuture<()> {
return self.write0(fileHandle: fileHandle, toOffset: toOffset, buffer: buffer, eventLoop: eventLoop)
}
private func write0(fileHandle: NIOFileHandle,
toOffset: Int64?,
buffer: ByteBuffer,
eventLoop: EventLoop) -> EventLoopFuture<()> {
let byteCount = buffer.readableBytes
guard byteCount > 0 else {
return eventLoop.makeSucceededFuture(())
@ -367,13 +427,22 @@ public struct NonBlockingFileIO {
return self.threadPool.runIfActive(eventLoop: eventLoop) {
var buf = buffer
while byteCount > 0 {
var offsetAccumulator: Int = 0
repeat {
let n = try buf.readWithUnsafeReadableBytes { ptr in
precondition(ptr.count == byteCount)
let res = try fileHandle.withUnsafeFileDescriptor { descriptor in
try Posix.write(descriptor: descriptor,
pointer: ptr.baseAddress!,
size: byteCount)
precondition(ptr.count == byteCount - offsetAccumulator)
let res: IOResult<ssize_t> = try fileHandle.withUnsafeFileDescriptor { descriptor in
if let toOffset = toOffset {
return try Posix.pwrite(descriptor: descriptor,
pointer: ptr.baseAddress!,
size: byteCount - offsetAccumulator,
offset: off_t(toOffset + Int64(offsetAccumulator)))
} else {
return try Posix.write(descriptor: descriptor,
pointer: ptr.baseAddress!,
size: byteCount - offsetAccumulator)
}
}
switch res {
case .processed(let n):
@ -383,9 +452,8 @@ public struct NonBlockingFileIO {
throw Error.descriptorSetToNonBlocking
}
}
byteCount -= n
}
offsetAccumulator += n
} while offsetAccumulator < byteCount
}
}

View File

@ -57,7 +57,9 @@ private let sysAccept = accept
private let sysConnect = connect
private let sysOpen: (UnsafePointer<CChar>, CInt) -> CInt = open
private let sysOpenWithMode: (UnsafePointer<CChar>, CInt, mode_t) -> CInt = open
private let sysFtruncate = ftruncate
private let sysWrite = write
private let sysPwrite = pwrite
private let sysRead = read
private let sysPread = pread
private let sysLseek = lseek
@ -353,6 +355,14 @@ internal enum Posix {
}
}
@inline(never)
@discardableResult
public static func ftruncate(descriptor: CInt, size: off_t) throws -> CInt {
return try wrapSyscall {
sysFtruncate(descriptor, size)
}
}
@inline(never)
public static func write(descriptor: CInt, pointer: UnsafeRawPointer, size: Int) throws -> IOResult<Int> {
return try wrapSyscallMayBlock {
@ -360,6 +370,13 @@ internal enum Posix {
}
}
@inline(never)
public static func pwrite(descriptor: CInt, pointer: UnsafeRawPointer, size: Int, offset: off_t) throws -> IOResult<Int> {
return try wrapSyscallMayBlock {
sysPwrite(descriptor, pointer, size, offset)
}
}
@inline(never)
public static func writev(descriptor: CInt, iovecs: UnsafeBufferPointer<IOVector>) throws -> IOResult<Int> {
return try wrapSyscallMayBlock {

View File

@ -46,8 +46,13 @@ extension NonBlockingFileIOTest {
("testFileRegionReadFromPipeFails", testFileRegionReadFromPipeFails),
("testReadFromNonBlockingPipeFails", testReadFromNonBlockingPipeFails),
("testSeekPointerIsSetToFront", testSeekPointerIsSetToFront),
("testReadingFileSize", testReadingFileSize),
("testChangeFileSizeShrink", testChangeFileSizeShrink),
("testChangeFileSizeGrow", testChangeFileSizeGrow),
("testWriting", testWriting),
("testWriteMultipleTimes", testWriteMultipleTimes),
("testWritingWithOffset", testWritingWithOffset),
("testWritingBeyondEOF", testWritingBeyondEOF),
("testFileOpenWorks", testFileOpenWorks),
("testFileOpenWorksWithEmptyFile", testFileOpenWorksWithEmptyFile),
("testFileOpenFails", testFileOpenFails),

View File

@ -453,6 +453,45 @@ class NonBlockingFileIOTest: XCTestCase {
XCTAssertEqual(2, numCalls)
}
func testReadingFileSize() throws {
try withTemporaryFile(content: "0123456789") { (fileHandle, _) -> Void in
let size = try self.fileIO.readFileSize(fileHandle: fileHandle,
eventLoop: eventLoop).wait()
XCTAssertEqual(size, 10)
}
}
func testChangeFileSizeShrink() throws {
try withTemporaryFile(content: "0123456789") { (fileHandle, _) -> Void in
try self.fileIO.changeFileSize(fileHandle: fileHandle,
size: 1,
eventLoop: eventLoop).wait()
let fileRegion = try FileRegion(fileHandle: fileHandle)
var buf = try self.fileIO.read(fileRegion: fileRegion,
allocator: allocator,
eventLoop: eventLoop).wait()
XCTAssertEqual("0", buf.readString(length: buf.readableBytes))
}
}
func testChangeFileSizeGrow() throws {
try withTemporaryFile(content: "0123456789") { (fileHandle, _) -> Void in
try self.fileIO.changeFileSize(fileHandle: fileHandle,
size: 100,
eventLoop: eventLoop).wait()
let fileRegion = try FileRegion(fileHandle: fileHandle)
var buf = try self.fileIO.read(fileRegion: fileRegion,
allocator: allocator,
eventLoop: eventLoop).wait()
let zeros = (1...90).map { _ in UInt8(0) }
guard let bytes = buf.readBytes(length: buf.readableBytes)?.suffix(from: 10) else {
XCTFail("readBytes(length:) should not be nil")
return
}
XCTAssertEqual(zeros, Array(bytes))
}
}
func testWriting() throws {
var buffer = allocator.buffer(capacity: 3)
buffer.writeStaticString("123")
@ -499,6 +538,53 @@ class NonBlockingFileIOTest: XCTestCase {
}
}
func testWritingWithOffset() throws {
var buffer = allocator.buffer(capacity: 3)
buffer.writeStaticString("123")
try withTemporaryFile(content: "hello") { (fileHandle, _) -> Void in
try self.fileIO.write(fileHandle: fileHandle,
toOffset: 1,
buffer: buffer,
eventLoop: eventLoop).wait()
let offset = try fileHandle.withUnsafeFileDescriptor {
try Posix.lseek(descriptor: $0, offset: 0, whence: SEEK_SET)
}
XCTAssertEqual(offset, 0)
var readBuffer = try self.fileIO.read(fileHandle: fileHandle,
byteCount: 5,
allocator: self.allocator,
eventLoop: self.eventLoop).wait()
XCTAssertEqual(5, readBuffer.readableBytes)
XCTAssertEqual("h123o", readBuffer.readString(length: readBuffer.readableBytes))
}
}
// This is undefined behavior and may cause different
// results on other platforms. Please add #if:s according
// to platform requirements.
func testWritingBeyondEOF() throws {
var buffer = allocator.buffer(capacity: 3)
buffer.writeStaticString("123")
try withTemporaryFile(content: "hello") { (fileHandle, _) -> Void in
try self.fileIO.write(fileHandle: fileHandle,
toOffset: 6,
buffer: buffer,
eventLoop: eventLoop).wait()
let fileRegion = try FileRegion(fileHandle: fileHandle)
var buf = try self.fileIO.read(fileRegion: fileRegion,
allocator: self.allocator,
eventLoop: self.eventLoop).wait()
XCTAssertEqual(9, buf.readableBytes)
XCTAssertEqual("hello", buf.readString(length: 5))
XCTAssertEqual([ UInt8(0) ], buf.readBytes(length: 1))
XCTAssertEqual("123", buf.readString(length: buf.readableBytes))
}
}
func testFileOpenWorks() throws {
let content = "123"
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in