Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions Sources/DevConfiguration/Core/ConfigVariableReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Configuration
import DevFoundation
import OSLog
import Synchronization

/// Provides access to configuration values queried by a `ConfigVariable`.
///
Expand Down Expand Up @@ -39,7 +40,14 @@ import OSLog
///
/// The reader never throws. If resolution fails, it returns the variable’s default value and posts a
/// ``ConfigVariableAccessFailedEvent`` to the event bus.
public struct ConfigVariableReader {
public final class ConfigVariableReader: Sendable {
/// The mutable state of a ``ConfigVariableReader``, protected by a `Mutex`.
private struct MutableState: Sendable {
/// The variables that have been registered with the reader, keyed by their configuration key.
var registeredVariables: [ConfigKey: RegisteredConfigVariable] = [:]
}


/// The access reporter that is used to report configuration access events.
public let accessReporter: any AccessReporter

Expand All @@ -54,8 +62,8 @@ public struct ConfigVariableReader {
/// The event bus used to post diagnostic events like ``ConfigVariableDecodingFailedEvent``.
public let eventBus: EventBus

/// The variables that have been registered with this reader, keyed by their configuration key.
private(set) var registeredVariables: [ConfigKey: RegisteredConfigVariable] = [:]
/// The mutable state protected by a mutex.
private let mutableState = Mutex(MutableState())

/// The logger used for registration diagnostics.
private static let logger = Logger(subsystem: "DevConfiguration", category: "ConfigVariableReader")
Expand All @@ -68,7 +76,7 @@ public struct ConfigVariableReader {
/// - Parameters:
/// - providers: The configuration providers, queried in order until a value is found.
/// - eventBus: The event bus that telemetry events are posted on.
public init(providers: [any ConfigProvider], eventBus: EventBus) {
public convenience init(providers: [any ConfigProvider], eventBus: EventBus) {
self.init(
providers: providers,
accessReporter: EventBusAccessReporter(eventBus: eventBus),
Expand All @@ -91,6 +99,12 @@ public struct ConfigVariableReader {
self.providers = providers
self.eventBus = eventBus
}


/// The variables that have been registered with this reader, keyed by their configuration key.
var registeredVariables: [ConfigKey: RegisteredConfigVariable] {
mutableState.withLock { $0.registeredVariables }
}
}


Expand All @@ -107,7 +121,7 @@ extension ConfigVariableReader {
/// warning is logged, and an assertion failure is triggered.
///
/// - Parameter variable: The configuration variable to register.
public mutating func register<Value>(_ variable: ConfigVariable<Value>) {
public func register<Value>(_ variable: ConfigVariable<Value>) {
let defaultContent: ConfigContent
do {
defaultContent = try variable.content.encode(variable.defaultValue)
Expand All @@ -117,17 +131,19 @@ extension ConfigVariableReader {
return
}

if registeredVariables[variable.key] != nil {
assertionFailure("Config variable '\(variable.key)' is already registered")
Self.logger.error("Config variable '\(variable.key)' is already registered; overwriting")
}
mutableState.withLock { state in
if state.registeredVariables[variable.key] != nil {
assertionFailure("Config variable '\(variable.key)' is already registered")
Self.logger.error("Config variable '\(variable.key)' is already registered; overwriting")
}

registeredVariables[variable.key] = RegisteredConfigVariable(
key: variable.key,
defaultContent: defaultContent,
secrecy: variable.secrecy,
metadata: variable.metadata
)
state.registeredVariables[variable.key] = RegisteredConfigVariable(
key: variable.key,
defaultContent: defaultContent,
secrecy: variable.secrecy,
metadata: variable.metadata
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct ConfigVariableReaderRegistrationTests: RandomValueGenerating {
@Test
mutating func registerStoresVariableWithCorrectProperties() {
// set up
var reader = ConfigVariableReader(providers: [InMemoryProvider(values: [:])], eventBus: EventBus())
let reader = ConfigVariableReader(providers: [InMemoryProvider(values: [:])], eventBus: EventBus())

var metadata = ConfigVariableMetadata()
metadata[TestTeamMetadataKey.self] = randomAlphanumericString()
Expand All @@ -47,7 +47,7 @@ struct ConfigVariableReaderRegistrationTests: RandomValueGenerating {
@Test
mutating func registerMultipleVariablesStoresAll() {
// set up
var reader = ConfigVariableReader(providers: [InMemoryProvider(values: [:])], eventBus: EventBus())
let reader = ConfigVariableReader(providers: [InMemoryProvider(values: [:])], eventBus: EventBus())
let key1 = randomConfigKey()
let key2 = randomConfigKey()
let variable1 = ConfigVariable(key: key1, defaultValue: randomBool())
Expand All @@ -68,7 +68,7 @@ struct ConfigVariableReaderRegistrationTests: RandomValueGenerating {
@Test
func registerDuplicateKeyHalts() async {
await #expect(processExitsWith: .failure) {
var reader = ConfigVariableReader(
let reader = ConfigVariableReader(
providers: [InMemoryProvider(values: [:])],
eventBus: EventBus()
)
Expand All @@ -84,7 +84,7 @@ struct ConfigVariableReaderRegistrationTests: RandomValueGenerating {
@Test
func registerWithEncodeFailureHalts() async {
await #expect(processExitsWith: .failure) {
var reader = ConfigVariableReader(
let reader = ConfigVariableReader(
providers: [InMemoryProvider(values: [:])],
eventBus: EventBus()
)
Expand Down