Skip to content

Commit

Permalink
Merge pull request #42 from dfed/dfed--objective-c-wrapper
Browse files Browse the repository at this point in the history
Create CADCacheAdvance: a Objective-C compatibility wrapper around a CacheAdvance<Data>
  • Loading branch information
dfed authored May 9, 2020
2 parents eb8744a + ea758dd commit 151ab3f
Show file tree
Hide file tree
Showing 17 changed files with 487 additions and 120 deletions.
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@ matrix:
after_success:
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/iOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/iOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/iOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/iOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e

- osx_image: xcode11.2
env: ACTION="swift-package";PLATFORMS="tvOS_12,tvOS_13";
after_success:
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/tvOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/tvOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/tvOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/tvOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e

- osx_image: xcode11.2
env: ACTION="swift-package";PLATFORMS="macOS_10_15";
after_success:
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/macOS_10_15 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/macOS_10_15 -t 8344b011-6b2a-4b3d-a573-eaf49684318e

- osx_image: xcode11.2
env: ACTION="swift-package";PLATFORMS="watchOS_5,watchOS_6";
Expand Down
4 changes: 2 additions & 2 deletions CacheAdvance.podspec
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
Pod::Spec.new do |s|
s.name = 'CacheAdvance'
s.version = '1.0.1'
s.version = '1.1.0'
s.license = 'Apache License, Version 2.0'
s.summary = 'A performant cache for logging systems. CacheAdvance persists log events 30x faster than SQLite.'
s.homepage = 'https://github.com/dfed/CacheAdvance'
s.authors = 'Dan Federman'
s.source = { :git => 'https://github.com/dfed/CacheAdvance.git', :tag => s.version }
s.swift_version = '5.1'
s.source_files = 'Sources/CacheAdvance/**/*.{swift}', 'Sources/SwiftTryCatch/**/*.{h,m}'
s.source_files = 'Sources/**/*.{swift}', 'Sources/**/*.{h,m}'
s.ios.deployment_target = '12.0'
s.tvos.deployment_target = '12.0'
s.watchos.deployment_target = '5.0'
Expand Down
26 changes: 23 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ let package = Package(
products: [
.library(
name: "CacheAdvance",
targets: ["CacheAdvance"]),
targets: ["CacheAdvance"]
),
.library(
name: "CADCacheAdvance",
targets: ["CADCacheAdvance"]
)
],
targets: [
.target(
Expand All @@ -24,7 +29,21 @@ let package = Package(
),
.testTarget(
name: "CacheAdvanceTests",
dependencies: ["CacheAdvance"]),
dependencies: ["CacheAdvance", "LorumIpsum"]
),
.target(
name: "CADCacheAdvance",
dependencies: ["CacheAdvance"],
swiftSettings: [.define("SWIFT_PACKAGE_MANAGER")]
),
.target(
name: "LorumIpsum",
dependencies: []
),
.testTarget(
name: "CADCacheAdvanceTests",
dependencies: ["CADCacheAdvance", "LorumIpsum"]
),
.target(
name: "SwiftTryCatch",
dependencies: [],
Expand All @@ -35,7 +54,8 @@ let package = Package(
),
.testTarget(
name: "SwiftTryCatchTests",
dependencies: ["SwiftTryCatch"])
dependencies: ["SwiftTryCatch"]
)
],
swiftLanguageVersions: [.v5]
)
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ let myCache = try CacheAdvance<MyMessageType>(
maximumBytes: 5000,
shouldOverwriteOldMessages: false)
```

```objc
CADCacheAdvance *const cache = [[CADCacheAdvance alloc]
initWithFileURL:[NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:@"MyCache"]
maximumBytes:5000
shouldOverwriteOldMessages:YES
error:nil];
```
To begin caching messages, you need to create a CacheAdvance instance with:
* A file URL – this URL must represent a file that has already been created. You can create a file by using `FileManager`'s [createFile(atPath:contents:attributes:)](https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile) API.
Expand All @@ -28,7 +36,11 @@ To begin caching messages, you need to create a CacheAdvance instance with:
### Appending messages to disk
```swift
try myCache.append(message: aMessageInstance)
try myCache.append(message: someMessage)
```

```objc
[cache appendMessage:someData error:nil];
```
By the time the above method exits, the message will have been persisted to disk. A CacheAdvance keeps no in-memory buffer. Appending a new message is cheap, as a CacheAdvance needs to encode and persist only the new message and associated metadata.
Expand All @@ -43,6 +55,10 @@ To ensure that caches can be read from 32bit devices, messages should not be lar
let cachedMessages = try myCache.messages()
```

```objc
NSArray<NSData> *const cachedMessages = [cache messagesAndReturnError:nil];
```
This method reads all cached messages from disk into memory.
### Thread safety
Expand All @@ -57,7 +73,7 @@ If a `CacheAdvanceError.fileCorrupted` error is thrown, the cache file is corrup
## How it works
CacheAdvance immediately persists each appended messages to disk using `FileHandle`s. Messages are encoded using a `JSONEncoder`. Messages are written to disk as an encoded data blob that is prefixed with the length of the message. The length of a message is stored using a `UInt32` to ensure that the size of the data on disk that stores a message's length is consistent between devices.
CacheAdvance immediately persists each appended messages to disk using `FileHandle`s. Messages are encoded into `Data` using a `JSONEncoder` by default, though the encoding/decoding mechanism can be customized. Messages are written to disk as an encoded data blob that is prefixed with the length of the message. The length of a message is stored using a `UInt32` to ensure that the size of the data on disk that stores a message's length is consistent between devices.
The first 64bytes of a CacheAdvance is reserved for storing metadata about the file. Any configuration data that must be static between cache opens should be stored in this header. It is also reasonable to store mutable information in the header, if doing so speeds up reads or writes to the file. The header format is managed by [FileHeader.swift](Sources/CacheAdvance/FileHeader.swift).
Expand Down
1 change: 1 addition & 0 deletions Scripts/build.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ for rawPlatform in rawPlatforms {
"-configuration", "Release",
"-derivedDataPath", platform.derivedDataPath,
"-PBXBuildsContinueAfterErrors=0",
"OTHER_CFLAGS='-DGENERATED_XCODE_PROJECT'",
]
if !platform.destination.isEmpty {
xcodeBuildArguments.append("-destination")
Expand Down
127 changes: 127 additions & 0 deletions Sources/CADCacheAdvance/CADCacheAdvance.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// Created by Dan Federman on 4/24/20.
// Copyright © 2020 Dan Federman.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#if SWIFT_PACKAGE_MANAGER
// Swift Package Manager defines multiple modules, while other distribution mechanisms do not.
// We only need to import CacheAdvance if this project is being built with Swift Package Manager.
import CacheAdvance
#endif
import Foundation

/// A cache that enables the performant persistence of individual messages to disk.
/// This cache is intended to be written to and read from using the same serial queue.
/// - Attention: This type is meant to be used by Objective-C code, and is not exposed to Swift. Swift code should use CacheAdvance<T>.
@objc(CADCacheAdvance)
@available(swift, obsoleted: 1.0)
public final class __ObjectiveCCompatibleCacheAdvanceWithGenericData: NSObject {

// MARK: Initialization

/// Creates a new instance of the receiver.
///
/// - Parameters:
/// - fileURL: The file URL indicating the desired location of the on-disk store. This file should already exist.
/// - maximumBytes: The maximum size of the cache, in bytes. Logs larger than this size will fail to append to the store.
/// - shouldOverwriteOldMessages: When `true`, once the on-disk store exceeds maximumBytes, new entries will replace the oldest entry.
///
/// - Warning: `maximumBytes` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
/// - Warning: `shouldOverwriteOldMessages` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
@objc
public init(
fileURL: URL,
maximumBytes: Bytes,
shouldOverwriteOldMessages: Bool)
throws
{
cache = try CacheAdvance<Data>(
fileURL: fileURL,
maximumBytes: maximumBytes,
shouldOverwriteOldMessages: shouldOverwriteOldMessages,
decoder: PassthroughDataDecoder(),
encoder: PassthroughDataEncoder())
}

// MARK: Public

@objc
public var fileURL: URL {
cache.fileURL
}

/// Checks if the all the header metadata provided at initialization matches the persisted header. If not, the cache is not writable.
/// - Returns: `true` if the cache is writable.
@objc
public var isWritable: Bool {
(try? cache.isWritable()) ?? false
}

/// Appends a message to the cache.
/// - Parameter message: A message to write to disk. Must be smaller than both `maximumBytes - FileHeader.expectedEndOfHeaderInFile` and `MessageSpan.max`.
@objc
public func appendMessage(_ message: Data) throws {
try cache.append(message: message)
}

/// - Returns: `true` when there are no messages written to the file, or when the file can not be read.
@objc
public var isEmpty: Bool {
(try? cache.isEmpty()) ?? true
}

/// Fetches all messages from the cache.
@objc
public func messages() throws -> [Data] {
try cache.messages()
}

// MARK: Private

private let cache: CacheAdvance<Data>
}

// MARK: - PassthroughDataDecoder

/// A decoder that treats all messages as if they are `Data`.
final class PassthroughDataDecoder: MessageDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
if let data = data as? T {
return data
} else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: [],
debugDescription: "Type was not Data"))
}
}
}

// MARK: - PassthroughDataDecoder

/// A encoder that treats all messages as if they are `Data`.
final class PassthroughDataEncoder: MessageEncoder {
func encode<T>(_ value: T) throws -> Data where T : Encodable {
if let value = value as? Data {
return value
} else {
throw EncodingError.invalidValue(
value,
EncodingError.Context(
codingPath: [],
debugDescription: "Value was not Data"))
}
}
}
15 changes: 12 additions & 3 deletions Sources/CacheAdvance/CacheAdvance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,29 @@ public final class CacheAdvance<T: Codable> {
/// - fileURL: The file URL indicating the desired location of the on-disk store. This file should already exist.
/// - maximumBytes: The maximum size of the cache, in bytes. Logs larger than this size will fail to append to the store.
/// - shouldOverwriteOldMessages: When `true`, once the on-disk store exceeds maximumBytes, new entries will replace the oldest entry.
/// - decoder: The decoder that will be used to decode already-persisted messages.
/// - encoder: The encoder that will be used to encode messages prior to persistence.
///
/// - Warning: `maximumBytes` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
/// - Warning: `shouldOverwriteOldMessages` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
/// - Warning: `decoder` must have a consistent implementation for the life of a cache. Changing this value after logs have been persisted to a cache may prevent reading messages from this cache.
/// - Warning: `encoder` must have a consistent implementation for the life of a cache. Changing this value after logs have been persisted to a cache may prevent reading messages from this cache.
public init(
fileURL: URL,
maximumBytes: Bytes,
shouldOverwriteOldMessages: Bool)
shouldOverwriteOldMessages: Bool,
decoder: MessageDecoder = JSONDecoder(),
encoder: MessageEncoder = JSONEncoder())
throws
{
self.fileURL = fileURL

writer = try FileHandle(forWritingTo: fileURL)
reader = try CacheReader(forReadingFrom: fileURL)
header = try CacheHeaderHandle(forReadingFrom: fileURL, maximumBytes: maximumBytes, overwritesOldMessages: shouldOverwriteOldMessages)

self.decoder = decoder
self.encoder = encoder
}

deinit {
Expand Down Expand Up @@ -219,6 +228,6 @@ public final class CacheAdvance<T: Codable> {

private var hasSetUpFileHandles = false

private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private let decoder: MessageDecoder
private let encoder: MessageEncoder
}
4 changes: 2 additions & 2 deletions Sources/CacheAdvance/EncodableMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct EncodableMessage<T: Codable> {
/// - Parameters:
/// - message: The messages to encode.
/// - encoder: The encoder to use.
init(message: T, encoder: JSONEncoder) {
init(message: T, encoder: MessageEncoder) {
self.message = message
self.encoder = encoder
}
Expand All @@ -51,6 +51,6 @@ struct EncodableMessage<T: Codable> {
// MARK: Private

private let message: T
private let encoder: JSONEncoder
private let encoder: MessageEncoder

}
25 changes: 25 additions & 0 deletions Sources/CacheAdvance/MessageDecoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Created by Dan Federman on 4/26/20.
// Copyright © 2020 Dan Federman.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

/// An object capable of decoding a message of type `T` from `Data`.
public protocol MessageDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}

extension JSONDecoder: MessageDecoder {}
25 changes: 25 additions & 0 deletions Sources/CacheAdvance/MessageEncoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Created by Dan Federman on 4/26/20.
// Copyright © 2020 Dan Federman.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

/// An object capable of encoding a message of type `T` to `Data`.
public protocol MessageEncoder {
func encode<T>(_ value: T) throws -> Data where T : Encodable
}

extension JSONEncoder: MessageEncoder {}
2 changes: 2 additions & 0 deletions Sources/CacheAdvance/ObjectiveC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import Foundation
#if SWIFT_PACKAGE_MANAGER
// Swift Package Manager defines multiple modules, while other distribution mechanisms do not.
// We only need to import SwiftTryCatch if this project is being built with Swift Package Manager.
import SwiftTryCatch
#endif

Expand Down
Loading

0 comments on commit 151ab3f

Please sign in to comment.