Skip to content

Commit 85d6fc0

Browse files
authored
Merge branch 'main' into heckj/cfrayRequestLogging
2 parents 2327890 + 59df55d commit 85d6fc0

21 files changed

+361
-240
lines changed

Package.resolved

Lines changed: 3 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ let package = Package(
2727
.library(name: "S3Store", targets: ["S3Store"]),
2828
],
2929
dependencies: [
30-
.package(url: "https://github.com/0xLeif/Cache.git", from: "2.1.0"),
3130
.package(url: "https://github.com/JohnSundell/Ink.git", from: "0.5.1"),
3231
.package(url: "https://github.com/swift-server/swift-prometheus.git", from: "1.0.0"),
3332
.package(url: "https://github.com/SwiftPackageIndex/Plot.git", branch: "main"),
@@ -63,7 +62,6 @@ let package = Package(
6362
.product(name: "SPIManifest", package: "SPIManifest"),
6463
.product(name: "SemanticVersion", package: "SemanticVersion"),
6564
.product(name: "SwiftSoup", package: "SwiftSoup"),
66-
.product(name: "Cache", package: "cache"),
6765
.product(name: "CanonicalPackageURL", package: "CanonicalPackageURL"),
6866
.product(name: "CustomDump", package: "swift-custom-dump"),
6967
.product(name: "Dependencies", package: "swift-dependencies"),

Sources/App/Commands/Ingestion.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,11 @@ enum Ingestion {
267267
// Sending 'github' into async let risks causing data races between async let uses and local uses
268268
@Dependency(\.github.fetchMetadata) var fetchMetadata
269269
@Dependency(\.github.fetchLicense) var fetchLicense
270+
@Dependency(\.github.fetchReadme) var fetchReadme
270271

271272
async let metadata = try await fetchMetadata(owner, repository)
272273
async let license = await fetchLicense(owner, repository)
273-
async let readme = await Current.fetchReadme(client, owner, repository)
274+
async let readme = await fetchReadme(owner, repository)
274275

275276
do {
276277
return try await (metadata, license, readme)

Sources/App/Core/AppEnvironment.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import FoundationNetworking
2323

2424

2525
struct AppEnvironment: Sendable {
26-
var fetchReadme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async -> Github.Readme?
2726
var fetchS3Readme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async throws -> String
2827
var fileManager: FileManager
2928
var getStatusCount: @Sendable (_ client: Client, _ status: Gitlab.Builder.Status) async throws -> Int
@@ -64,7 +63,6 @@ extension AppEnvironment {
6463
nonisolated(unsafe) static var logger: Logger!
6564

6665
static let live = AppEnvironment(
67-
fetchReadme: { client, owner, repo in await Github.fetchReadme(client:client, owner: owner, repository: repo) },
6866
fetchS3Readme: { client, owner, repo in try await S3Readme.fetchReadme(client:client, owner: owner, repository: repo) },
6967
fileManager: .live,
7068
getStatusCount: { client, status in

Sources/App/Core/CurrentReferenceCache.swift

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Dependencies
16+
import DependenciesMacros
17+
18+
19+
@DependencyClient
20+
struct CurrentReferenceCacheClient {
21+
var set: @Sendable (_ owner: String, _ repository: String, _ reference: String?) async -> Void
22+
var get: @Sendable (_ owner: String, _ repository: String) async -> String?
23+
}
24+
25+
26+
extension CurrentReferenceCacheClient: DependencyKey {
27+
static let timeToLive: Duration = .seconds(5*60)
28+
29+
static var liveValue: CurrentReferenceCacheClient {
30+
.init(
31+
set: { owner, repository, reference async in
32+
@Dependency(\.redis) var redis
33+
try? await redis.set(key: getKey(owner: owner, repository: repository),
34+
value: reference,
35+
expiresIn: timeToLive)
36+
},
37+
get: { owner, repository in
38+
@Dependency(\.redis) var redis
39+
return try? await redis.get(key: getKey(owner: owner, repository: repository))
40+
}
41+
)
42+
}
43+
44+
static func getKey(owner: String, repository: String) -> String {
45+
"\(owner)/\(repository)".lowercased()
46+
}
47+
}
48+
49+
50+
extension CurrentReferenceCacheClient: TestDependencyKey {
51+
static var testValue: Self { Self() }
52+
}
53+
54+
55+
extension DependencyValues {
56+
var currentReferenceCache: CurrentReferenceCacheClient {
57+
get { self[CurrentReferenceCacheClient.self] }
58+
set { self[CurrentReferenceCacheClient.self] = newValue }
59+
}
60+
}
61+
62+
63+
#if DEBUG
64+
extension CurrentReferenceCacheClient {
65+
static var disabled: Self {
66+
.init(set: { _, _, _ in }, get: { _, _ in nil })
67+
}
68+
69+
static var inMemory: Self {
70+
.init(
71+
set: { owner, repository, reference in
72+
_cache.withValue {
73+
$0[getKey(owner: owner, repository: repository)] = reference
74+
}
75+
},
76+
get: { owner, repository in
77+
_cache.withValue {
78+
$0[getKey(owner: owner, repository: repository)]
79+
}
80+
}
81+
)
82+
}
83+
84+
nonisolated(unsafe) static var _cache = LockIsolated<[String: String]>([:])
85+
}
86+
#endif

Sources/App/Core/Dependencies/EnvironmentClient.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ struct EnvironmentClient {
3939
var collectionSigningCertificateChain: @Sendable () -> [URL] = { XCTFail("collectionSigningCertificateChain"); return [] }
4040
var collectionSigningPrivateKey: @Sendable () -> Data?
4141
var current: @Sendable () -> Environment = { XCTFail("current"); return .development }
42-
var currentReferenceCache: @Sendable () -> CurrentReferenceCache?
4342
var dbId: @Sendable () -> String?
4443
var enableCFRayLogging: @Sendable () -> Bool = { XCTFail("enableCFRayLogging"); return true }
4544
var mastodonCredentials: @Sendable () -> Mastodon.Credentials?
@@ -103,7 +102,6 @@ extension EnvironmentClient: DependencyKey {
103102
Environment.get("COLLECTION_SIGNING_PRIVATE_KEY").map { Data($0.utf8) }
104103
},
105104
current: { (try? Environment.detect()) ?? .development },
106-
currentReferenceCache: { .live },
107105
dbId: { Environment.get("DATABASE_ID") },
108106
enableCFRayLogging: {
109107
Environment.get("ENABLE_CF_RAY_LOGGING")

Sources/App/Core/Dependencies/GithubClient.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ import IssueReporting
2323
struct GithubClient {
2424
var fetchLicense: @Sendable (_ owner: String, _ repository: String) async -> Github.License?
2525
var fetchMetadata: @Sendable (_ owner: String, _ repository: String) async throws(Github.Error) -> Github.Metadata = { _,_ in reportIssue("fetchMetadata"); return .init() }
26+
var fetchReadme: @Sendable (_ owner: String, _ repository: String) async -> Github.Readme?
2627
}
2728

2829

2930
extension GithubClient: DependencyKey {
3031
static var liveValue: Self {
3132
.init(
3233
fetchLicense: { owner, repo in await Github.fetchLicense(owner: owner, repository: repo) },
33-
fetchMetadata: { owner, repo throws(Github.Error) in try await Github.fetchMetadata(owner: owner, repository: repo) }
34+
fetchMetadata: { owner, repo throws(Github.Error) in try await Github.fetchMetadata(owner: owner, repository: repo) },
35+
fetchReadme: { owner, repo in await Github.fetchReadme(owner: owner, repository: repo) }
3436
)
3537
}
3638
}
@@ -40,7 +42,8 @@ extension GithubClient: TestDependencyKey {
4042
static var testValue: Self {
4143
.init(
4244
fetchLicense: { _, _ in unimplemented("fetchLicense"); return nil },
43-
fetchMetadata: { _, _ in unimplemented("fetchMetadata"); return .init() }
45+
fetchMetadata: { _, _ in unimplemented("fetchMetadata"); return .init() },
46+
fetchReadme: { _, _ in unimplemented("fetchReadme"); return nil }
4447
)
4548
}
4649
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import NIOCore
16+
@preconcurrency import RediStack
17+
import Dependencies
18+
import DependenciesMacros
19+
20+
21+
@DependencyClient
22+
struct RedisClient {
23+
var set: @Sendable (_ key: String, _ value: String?, Duration?) async throws -> Void
24+
var get: @Sendable (_ key: String) async throws -> String?
25+
var expire: @Sendable (_ key: String, _ after: Duration) async throws -> Bool
26+
var increment: @Sendable (_ key: String, _ by: Int) async throws -> Int
27+
}
28+
29+
30+
extension RedisClient {
31+
func set(key: String, value: String?, expiresIn: Duration? = nil) async throws {
32+
try await set(key: key, value: value, expiresIn)
33+
}
34+
35+
func increment(key: String) async throws -> Int {
36+
try await increment(key: key, by: 1)
37+
}
38+
}
39+
40+
41+
extension RedisClient: DependencyKey {
42+
static var liveValue: RedisClient {
43+
.init(
44+
set: { key, value, expiresIn in
45+
try await Redis.shared.set(key: key, value: value, expiresIn: expiresIn)
46+
},
47+
get: { key in try await Redis.shared.get(key: key) },
48+
expire: { key, ttl in try await Redis.shared.expire(key: key, after: ttl) },
49+
increment: { key, value in try await Redis.shared.increment(key: key, by: value) }
50+
)
51+
}
52+
}
53+
54+
55+
extension RedisClient: TestDependencyKey {
56+
static var testValue: Self { Self() }
57+
}
58+
59+
60+
extension DependencyValues {
61+
var redis: RedisClient {
62+
get { self[RedisClient.self] }
63+
set { self[RedisClient.self] = newValue }
64+
}
65+
}
66+
67+
68+
#if DEBUG
69+
extension RedisClient {
70+
static var disabled: Self {
71+
.init(set: { _, _, _ in },
72+
get: { _ in nil },
73+
expire: { _, _ in true },
74+
increment: { _, value in value })
75+
}
76+
}
77+
#endif
78+
79+
80+
private actor Redis {
81+
var client: RediStack.RedisClient
82+
static private var task: Task<Redis, Swift.Error>?
83+
84+
static var shared: Redis {
85+
get async throws {
86+
if let task {
87+
return try await task.value
88+
}
89+
let task = Task<Redis, Swift.Error> {
90+
var attemptsLeft = maxConnectionAttempts
91+
while attemptsLeft > 0 {
92+
do {
93+
return try await Redis()
94+
} catch {
95+
attemptsLeft -= 1
96+
Current.logger().warning("Redis connection failed, \(attemptsLeft) attempts left. Error: \(error)")
97+
try? await Task.sleep(for: .milliseconds(500))
98+
}
99+
}
100+
throw Error.unavailable
101+
}
102+
self.task = task
103+
return try await task.value
104+
}
105+
}
106+
107+
enum Error: Swift.Error {
108+
case unavailable
109+
}
110+
111+
private init() async throws {
112+
let connection = RedisConnection.make(
113+
configuration: try .init(hostname: Redis.hostname),
114+
boundEventLoop: NIOSingletons.posixEventLoopGroup.any()
115+
)
116+
self.client = try await connection.get()
117+
}
118+
119+
// This hostname has to match the redir service name in app.yml.
120+
static let hostname = "redis"
121+
static let maxConnectionAttempts = 3
122+
123+
func set(key: String, value: String?, expiresIn: Duration?) async {
124+
if let value {
125+
let buffer = ByteBuffer(string: value)
126+
let value = RESPValue.bulkString(buffer)
127+
if let expiresIn {
128+
let ttl = Int(expiresIn.components.seconds)
129+
try? await client.setex(.init(key), to: value, expirationInSeconds: ttl).get()
130+
} else {
131+
try? await client.set(.init(key), to: value).get()
132+
}
133+
} else {
134+
_ = try? await client.delete([.init(key)]).get()
135+
}
136+
}
137+
138+
func get(key: String) async -> String? {
139+
return try? await client.get(.init(key)).map(\.string).get()
140+
}
141+
142+
func expire(key: String, after ttl: Duration) async throws -> Bool {
143+
try await client.expire(.init(key), after: .init(ttl)).get()
144+
}
145+
146+
func increment(key: String) async throws -> Int {
147+
try await client.increment(.init(key)).get()
148+
}
149+
150+
func increment(key: String, by value: Int) async throws -> Int {
151+
try await client.increment(.init(key), by: value).get()
152+
}
153+
}
154+

0 commit comments

Comments
 (0)