Skip to content
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

Fastly Cache #21

Merged
merged 15 commits into from
Sep 28, 2023
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-crypto",
"state" : {
"revision" : "33a20e650c33f6d72d822d558333f2085effa3dc",
"version" : "2.5.0"
"revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894",
"version" : "2.6.0"
}
}
],
Expand Down
84 changes: 84 additions & 0 deletions Sources/Compute/Cache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// Cache.swift
//
//
// Created by Andrew Barba on 9/13/23.
//

public struct Cache: Sendable {

public static func getOrSet(_ key: String, _ handler: () async throws -> (FetchResponse, CachePolicy)) async throws -> Entry {
let trx = try await Fastly.Cache.getOrSet(key) {
let (res, cachePolicy) = try await handler()
let length = res.headers[.contentLength].flatMap(Int.init)
return await (.body(res.body.body, length: length), cachePolicy)
}
return try .init(trx)
}

public static func getOrSet(_ key: String, _ handler: () async throws -> ([UInt8], CachePolicy)) async throws -> Entry {
let trx = try await Fastly.Cache.getOrSet(key) {
let (bytes, cachePolicy) = try await handler()
return (.bytes(bytes), cachePolicy)
}
return try .init(trx)
}

public static func getOrSet(_ key: String, _ handler: () async throws -> (String, CachePolicy)) async throws -> Entry {
let trx = try await Fastly.Cache.getOrSet(key) {
let (text, cachePolicy) = try await handler()
return (.bytes(.init(text.utf8)), cachePolicy)
}
return try .init(trx)
}

public static func getOrSet(_ key: String, _ handler: () async throws -> (Data, CachePolicy)) async throws -> Entry {
let trx = try await Fastly.Cache.getOrSet(key) {
let (data, cachePolicy) = try await handler()
return (.bytes(data.bytes), cachePolicy)
}
return try .init(trx)
}

public static func getOrSet(_ key: String, _ handler: () async throws -> ([String: Sendable], CachePolicy)) async throws -> Entry {
let trx = try await Fastly.Cache.getOrSet(key) {
let (json, cachePolicy) = try await handler()
let data = try JSONSerialization.data(withJSONObject: json)
return (.bytes(data.bytes), cachePolicy)
}
return try .init(trx)
}

public static func getOrSet(_ key: String, _ handler: () async throws -> ([Sendable], CachePolicy)) async throws -> Entry {
let trx = try await Fastly.Cache.getOrSet(key) {
let (json, cachePolicy) = try await handler()
let data = try JSONSerialization.data(withJSONObject: json)
return (.bytes(data.bytes), cachePolicy)
}
return try .init(trx)
}
}

extension Cache {

public struct Entry: Sendable {

public let body: ReadableBody

public let age: Int

public let hits: Int

public let contentLength: Int

public let state: CacheState

internal init(_ trx: Fastly.Cache.Transaction) throws {
self.body = try ReadableWasiBody(trx.getBody())
self.age = try trx.getAge()
self.hits = try trx.getHits()
self.contentLength = try trx.getLength()
self.state = try trx.getState()
}
}
}
4 changes: 4 additions & 0 deletions Sources/Compute/Fastly/FastlyBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ extension Fastly {
try wasi(fastly_http_body__close(handle))
}

public mutating func abandon() throws {
try wasi(fastly_http_body__abandon(handle))
}

@discardableResult
public mutating func write(_ bytes: [UInt8], location: BodyWriteEnd = .back) throws -> Int {
defer { used = true }
Expand Down
144 changes: 144 additions & 0 deletions Sources/Compute/Fastly/FastlyCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//
// FastlyCache.swift
//
//
// Created by Andrew Barba on 9/13/23.
//

import ComputeRuntime

extension Fastly {
public struct Cache: Sendable {

public typealias InsertResult = (data: HandlerData, cachePolicy: CachePolicy)

public static func getOrSet(_ key: String, _ handler: () async throws -> InsertResult) async throws -> Transaction {
// Open the transaction
let trx = try Transaction.lookup(key)

// Get current transaction state
let state = try trx.getState()

// If state is usable then return the body from cache
guard state.contains(.mustInsertOrUpdate) else {
return trx
}

do {
// If its not usable then begin executing handler with new value
let (data, cachePolicy) = try await handler()

// Get an instance to the insert handle
var writer = try trx.insertAndStreamBack(cachePolicy: cachePolicy, length: data.length)

// Append bytes from handler to writeable body
switch data {
case .body(let body, _):
try writer.body.append(body)
case .bytes(let bytes):
try writer.body.write(bytes)
}

// Finish the body
try writer.body.close()

return writer.transaction
} catch {
// Cancel the transaction if something went wrong
try trx.cancel()

// Rethrow original error
throw error
}
}
}
}

extension Fastly.Cache {
public enum HandlerData {
case body(_ body: Fastly.Body, length: Int? = nil)
case bytes(_ bytes: [UInt8])

public var length: Int? {
switch self {
case .body(_, let length):
return length
case .bytes(let bytes):
return bytes.count
}
}
}
}

extension Fastly.Cache {
public struct Transaction: Sendable {

internal let handle: WasiHandle

internal init(_ handle: WasiHandle) {
self.handle = handle
}

public static func lookup(_ key: String) throws -> Transaction {
var handle: WasiHandle = 0
let options = CacheLookupOptions.none
var config = CacheLookupConfig()
try wasi(fastly_cache__cache_transaction_lookup(key, key.utf8.count, options.rawValue, &config, &handle))
return Transaction(handle)
}

public func insertAndStreamBack(cachePolicy: CachePolicy, length: Int?) throws -> (body: Fastly.Body, transaction: Transaction) {
var bodyHandle: WasiHandle = 0
var cacheHandle: WasiHandle = 0
var options: CacheWriteOptions = []
var config = CacheWriteConfig()
config.max_age_ns = .init(cachePolicy.maxAge) * 1_000_000_000
if cachePolicy.staleMaxAge > 0 {
options.insert(.staleWhileRevalidateNs)
config.stale_while_revalidate_ns = .init(cachePolicy.staleMaxAge) * 1_000_000_000
}
if let length {
options.insert(.length)
config.length = .init(length)
}
try wasi(fastly_cache__cache_transaction_insert_and_stream_back(handle, options.rawValue, &config, &bodyHandle, &cacheHandle))
return (.init(bodyHandle), .init(cacheHandle))
}

public func getState() throws -> CacheState {
var value: UInt8 = 0
try wasi(fastly_cache__cache_get_state(handle, &value))
return CacheState(rawValue: value)
}

public func getAge() throws -> Int {
var value: UInt64 = 0
try wasi(fastly_cache__cache_get_age_ns(handle, &value))
return .init(value / 1_000_000_000)
}

public func getLength() throws -> Int {
var value: UInt64 = 0
try wasi(fastly_cache__cache_get_length(handle, &value))
return .init(value)
}

public func getHits() throws -> Int {
var value: UInt64 = 0
try wasi(fastly_cache__cache_get_hits(handle, &value))
return .init(value)
}

public func getBody() throws -> Fastly.Body {
var bodyHandle: WasiHandle = 0
let options = CacheGetBodyOptions.none
var config = CacheGetBodyConfig()
try wasi(fastly_cache__cache_get_body(handle, options.rawValue, &config, &bodyHandle))
return .init(bodyHandle)
}

public func cancel() throws {
try wasi(fastly_cache__cache_transaction_cancel(handle))
}
}
}
64 changes: 48 additions & 16 deletions Sources/Compute/Fastly/FastlyStubs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,7 @@
/// avoid link failure.
/// When running with Compute runtime library, they are ignored completely.
#if !arch(wasm32)

/* TYPES */

struct DynamicBackendConfig: Sendable {
var host_override: UnsafePointer<CChar>! = nil
var host_override_len: Int = 0
var connect_timeout_ms: Int = 0
var first_byte_timeout_ms: Int = 0
var between_bytes_timeout_ms: Int = 0
var ssl_min_version: Int = 0
var ssl_max_version: Int = 0
var cert_hostname: UnsafePointer<CChar>! = nil
var cert_hostname_len: Int = 0
var sni_hostname: UnsafePointer<CChar>! = nil
var sni_hostname_len: Int = 0
}
import ComputeRuntime

/* FASTLY_ABI */

Expand Down Expand Up @@ -72,6 +57,8 @@ func fastly_http_body__append(_ dest: WasiHandle, _ src: WasiHandle) -> Int32 {

func fastly_http_body__close(_ handle: WasiHandle) -> Int32 { fatalError() }

func fastly_http_body__abandon(_ handle: WasiHandle) -> Int32 { fatalError() }

func fastly_http_body__write(_ handle: WasiHandle, _ data: UnsafePointer<UInt8>!, _ data_len: Int, _ body_end: Int32, _ nwritten: UnsafeMutablePointer<Int>!) -> Int32 { fatalError() }

func fastly_http_body__read(_ handle: WasiHandle, _ data: UnsafeMutablePointer<UInt8>!, _ data_max_len: Int, _ nwritten: UnsafeMutablePointer<Int>!) -> Int32 { fatalError() }
Expand Down Expand Up @@ -170,4 +157,49 @@ func fastly_http_resp__framing_headers_mode_set(_ resp_handle: WasiHandle, _ mod

func fastly_http_resp__http_keepalive_mode_set(_ resp_handle: WasiHandle, _ mode: UInt32) -> Int32 { fatalError() }

func fastly_cache__cache_transaction_lookup(
_ cache_key: UnsafePointer<CChar>!,
_ cache_key_len: Int,
_ options_mask: UInt32,
_ config: UnsafeMutablePointer<CacheLookupConfig>!,
_ ret: UnsafeMutablePointer<WasiHandle>!
) -> Int32 { fatalError() }

func fastly_cache__cache_transaction_insert_and_stream_back(
_ handle: WasiHandle,
_ options_mask: UInt32,
_ config: UnsafeMutablePointer<CacheWriteConfig>!,
_ ret_body: UnsafeMutablePointer<WasiHandle>!,
_ ret_cache: UnsafeMutablePointer<WasiHandle>!
) -> Int32 { fatalError() }

func fastly_cache__cache_get_state(
_ handle: WasiHandle,
_ ret: UnsafeMutablePointer<UInt8>!
) -> Int32 { fatalError() }

func fastly_cache__cache_get_age_ns(
_ handle: WasiHandle,
_ ret: UnsafeMutablePointer<UInt64>!
) -> Int32 { fatalError() }

func fastly_cache__cache_get_length(
_ handle: WasiHandle,
_ ret: UnsafeMutablePointer<UInt64>!
) -> Int32 { fatalError() }

func fastly_cache__cache_get_hits(
_ handle: WasiHandle,
_ ret: UnsafeMutablePointer<UInt64>!
) -> Int32{ fatalError() }

func fastly_cache__cache_get_body(
_ handle: WasiHandle,
_ options_mask: UInt32,
_ config: UnsafeMutablePointer<CacheGetBodyConfig>!,
_ ret: UnsafeMutablePointer<WasiHandle>!
) -> Int32 { fatalError() }

func fastly_cache__cache_transaction_cancel(_ handle: WasiHandle) -> Int32 { fatalError() }

#endif
Loading