Skip to content

Commit

Permalink
Add Framework for storing persistent key/value pairs to be used betwe…
Browse files Browse the repository at this point in the history
…en requests (#69)

* Add persist system for storing key/value pairs

Includes implementation of in memory version

* Store as values as Codable

* Persist.get/remove return EventLoopFuture<Void>

* Provide HBRequest to persist calls

* Don't pass whole request, just pass eventLoop

* Add HBPersistDriverFactory.init

* Driver requires HBRequest, move interface to HBRequest.Persist

* Remove duplicate set function add a create function

* Add shutdown function

* Added HBRequest.Persist.create

Also added test to verify it works

* Add HBExtensions.exists which returns if an extension has been set
  • Loading branch information
adam-fowler authored Apr 13, 2021
1 parent 2c33daa commit c90715c
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 2 deletions.
9 changes: 7 additions & 2 deletions Sources/Hummingbird/Extensions/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,22 @@ public struct HBExtensions<ParentObject> {
self.items = [:]
}

/// Get extension from a `KeyPath`
/// Get optional extension from a `KeyPath`
public func get<Type>(_ key: KeyPath<ParentObject, Type>) -> Type? {
self.items[key]?.value as? Type
}

/// Get extension from a `KeyPath`
public func get<Type>(_ key: KeyPath<ParentObject, Type>) -> Type {
guard let value = items[key]?.value as? Type else { preconditionFailure("Cannot get extension without having set it") }
guard let value = items[key]?.value as? Type else { preconditionFailure("Cannot get extension of type \(Type.self) without having set it") }
return value
}

/// Return if extension has been set
public func exists<Type>(_ key: KeyPath<ParentObject, Type>) -> Bool {
self.items[key]?.value != nil
}

/// Get extension from a `KeyPath`. If it doesn't exist then create it. Use this with care it may cause race conditions
/// especially if used on a global object like `HBApplication`.
/// - Parameters:
Expand Down
85 changes: 85 additions & 0 deletions Sources/Hummingbird/Storage/Application+Persist.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

extension HBApplication {
/// Framework for storing persistent key/value pairs between mulitple requests
public struct Persist {
/// Initialise Persist struct
/// - Parameters
/// - factory: Persist driver factory
/// - application: reference to application that can be used during persist driver creation
public init(_ factory: HBPersistDriverFactory, application: HBApplication) {
self.driver = factory.create(application)
}

let driver: HBPersistDriver
}

/// Accessor for persist framework
public var persist: Persist { self.extensions.get(\.persist) }

/// Add persist framework to `HBApplication`.
/// - Parameter using: Factory struct that will create the persist driver when required
public func addPersist(using: HBPersistDriverFactory) {
self.extensions.set(\.persist, value: .init(using, application: self)) { persist in
persist.driver.shutdown()
}
}
}

extension HBRequest {
public struct Persist {
/// Set value for key that will expire after a certain time.
///
/// Doesn't check to see if key already exists. Some drivers may fail it key already exists
/// - Parameters:
/// - key: key string
/// - value: value
/// - expires: time key/value pair will expire
/// - Returns: EventLoopFuture for when value has been set
public func create<Object: Codable>(key: String, value: Object, expires: TimeAmount? = nil) -> EventLoopFuture<Void> {
return self.request.application.persist.driver.create(key: key, value: value, expires: expires, request: self.request)
}

/// Set value for key that will expire after a certain time
/// - Parameters:
/// - key: key string
/// - value: value
/// - expires: time key/value pair will expire
/// - Returns: EventLoopFuture for when value has been set
public func set<Object: Codable>(key: String, value: Object, expires: TimeAmount? = nil) -> EventLoopFuture<Void> {
return self.request.application.persist.driver.set(key: key, value: value, expires: expires, request: self.request)
}

/// Get value for key
/// - Parameters:
/// - key: key string
/// - type: Type of value
/// - Returns: EventLoopFuture that will be filled with value
public func get<Object: Codable>(key: String, as type: Object.Type) -> EventLoopFuture<Object?> {
return self.request.application.persist.driver.get(key: key, as: type, request: self.request)
}

/// Remove value for key
/// - Parameter key: key string
public func remove(key: String) -> EventLoopFuture<Void> {
return self.request.application.persist.driver.remove(key: key, request: self.request)
}

let request: HBRequest
}

/// Accessor for persist framework
public var persist: HBRequest.Persist { .init(request: self) }
}
89 changes: 89 additions & 0 deletions Sources/Hummingbird/Storage/MemoryPersistDriver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIO

/// In memory driver for persist system for storing persistent cross request key/value pairs
class HBMemoryPersistDriver: HBPersistDriver {
init(eventLoopGroup: EventLoopGroup) {
self.values = [:]
self.task = eventLoopGroup.next().scheduleRepeatedTask(initialDelay: .hours(1), delay: .hours(1)) { _ in
self.tidy()
}
}

func shutdown() {
self.task?.cancel()
}

func create<Object: Codable>(key: String, value: Object, expires: TimeAmount? = nil, request: HBRequest) -> EventLoopFuture<Void> {
return request.eventLoop.submit {
self.values[key] = .init(value: value, expires: expires)
}
}

func set<Object: Codable>(key: String, value: Object, expires: TimeAmount? = nil, request: HBRequest) -> EventLoopFuture<Void> {
return request.eventLoop.submit {
self.values[key] = .init(value: value, expires: expires)
}
}

func get<Object: Codable>(key: String, as: Object.Type, request: HBRequest) -> EventLoopFuture<Object?> {
return request.eventLoop.submit {
guard let item = self.values[key] else { return nil }
guard let expires = item.epochExpires else { return item.value as? Object }
guard Item.getEpochTime() <= expires else { return nil }
return item.value as? Object
}
}

func remove(key: String, request: HBRequest) -> EventLoopFuture<Void> {
return request.eventLoop.submit {
self.values[key] = nil
}
}

private func tidy() {
let currentTime = Item.getEpochTime()
self.values = self.values.compactMapValues {
if let expires = $0.epochExpires {
if expires > currentTime {
return nil
}
}
return $0
}
}

struct Item {
/// value stored
let value: Codable
/// epoch time for when item expires
let epochExpires: Int?

init(value: Codable, expires: TimeAmount?) {
self.value = value
self.epochExpires = expires.map { Self.getEpochTime() + Int($0.nanoseconds / 1_000_000_000) }
}

static func getEpochTime() -> Int {
var timeVal = timeval.init()
gettimeofday(&timeVal, nil)
return timeVal.tv_sec
}
}

var values: [String: Item]
var task: RepeatedTask?
}
50 changes: 50 additions & 0 deletions Sources/Hummingbird/Storage/PersistDriver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Hummingbird server framework project
//
// Copyright (c) 2021-2021 the Hummingbird authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIO

/// Protocol for driver supporting persistent Key/Value pairs across requests
public protocol HBPersistDriver {
/// shutdown driver
func shutdown()
/// set value for key
func create<Object: Codable>(key: String, value: Object, expires: TimeAmount?, request: HBRequest) -> EventLoopFuture<Void>
/// set value for key
func set<Object: Codable>(key: String, value: Object, expires: TimeAmount?, request: HBRequest) -> EventLoopFuture<Void>
/// get value for key
func get<Object: Codable>(key: String, as: Object.Type, request: HBRequest) -> EventLoopFuture<Object?>
/// remove value for key
func remove(key: String, request: HBRequest) -> EventLoopFuture<Void>
}

extension HBPersistDriver {
/// default implemenation of shutdown()
public func shutdown() {}
}

/// Factory class for persist drivers
public struct HBPersistDriverFactory {
public let create: (HBApplication) -> HBPersistDriver

/// Initialize HBPersistDriverFactory
/// - Parameter create: HBPersistDriver factory function
public init(create: @escaping (HBApplication) -> HBPersistDriver) {
self.create = create
}

/// In memory driver for persist system
public static var memory: HBPersistDriverFactory {
.init(create: { app in HBMemoryPersistDriver(eventLoopGroup: app.eventLoopGroup) })
}
}
20 changes: 20 additions & 0 deletions Tests/HummingbirdTests/ApplicationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,26 @@ final class ApplicationTests: XCTestCase {
}
}

func testELFOptional() {
let app = HBApplication(testing: .embedded)
app.router
.group("/echo-body")
.post { request -> EventLoopFuture<ByteBuffer?> in
return request.success(request.body.buffer)
}
app.XCTStart()
defer { app.XCTStop() }

let buffer = self.randomBuffer(size: 64)
app.XCTExecute(uri: "/echo-body", method: .POST, body: buffer) { response in
XCTAssertEqual(response.status, .ok)
XCTAssertEqual(response.body, buffer)
}
app.XCTExecute(uri: "/echo-body", method: .POST) { response in
XCTAssertEqual(response.status, .notFound)
}
}

func testOptionalCodable() {
struct Name: HBResponseCodable {
let first: String
Expand Down
Loading

0 comments on commit c90715c

Please sign in to comment.