Skip to content
Draft
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
15 changes: 5 additions & 10 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ import Foundation

let defaultTraits: Set<String> = [
"JSONSupport"

// Disabled due to a bug in SwiftPM with traits that pull in an external dependency.
// Once that's fixed in Swift 6.2.x, we can enable these traits by default.
// Open fix: https://github.com/swiftlang/swift-package-manager/pull/9136
// "LoggingSupport",
// "ReloadingSupport",
]

var traits: Set<Trait> = [
Expand Down Expand Up @@ -146,9 +140,6 @@ let package = Package(
"ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet1.swift.gyb",
"ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet2.swift.gyb",
"ConfigReaderTests/ConfigSnapshotReaderMethodTestsGet3.swift.gyb",
],
resources: [
.copy("Resources")
]
),

Expand Down Expand Up @@ -188,7 +179,11 @@ for target in package.targets {
// https://docs.swift.org/compiler/documentation/diagnostics/nonisolated-nonsending-by-default/
settings.append(.enableUpcomingFeature("NonisolatedNonsendingByDefault"))

settings.append(.enableExperimentalFeature("AvailabilityMacro=Configuration 1.0:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"))
settings.append(
.enableExperimentalFeature(
"AvailabilityMacro=Configuration 1.0:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"
)
)

if enableAllCIFlags {
// Ensure all public types are explicitly annotated as Sendable or not Sendable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ let logger: Logger = ...
let config = ConfigReader(
providers: [
EnvironmentVariablesProvider(),
try await FileProvider<JSONSnapshot>(filePath: "/etc/myapp/config.json"),
try await FileProvider<JSONSnapshot>(
filePath: "/etc/myapp/config.json",
allowMissing: true // Optional: treat missing file as empty config
),
InMemoryProvider(values: [
"http.server.port": 8080,
"http.server.host": "127.0.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,71 @@ such as Kubernetes secrets mounted into a container's filesystem.

> Tip: For comprehensive guidance on handling secrets securely, see <doc:Handling-secrets-correctly>.

### Handling optional configuration files

File-based providers support an `allowMissing` parameter to control whether missing files should throw an error or be treated as empty configuration. This is useful when configuration files are optional.

When `allowMissing` is `false` (the default), missing files throw an error:

```swift
import Configuration

// This will throw an error if config.json doesn't exist
let config = ConfigReader(
provider: try await FileProvider<JSONSnapshot>(
filePath: "/etc/config.json",
allowMissing: false // This is the default
)
)
```

When `allowMissing` is `true`, missing files are treated as empty configuration:

```swift
import Configuration

// This won't throw if config.json is missing - treats it as empty
let config = ConfigReader(
provider: try await FileProvider<JSONSnapshot>(
filePath: "/etc/config.json",
allowMissing: true
)
)

// Returns the default value if the file is missing
let port = config.int(forKey: "server.port", default: 8080)
```

The same applies to other file-based providers:

```swift
// Optional secrets directory
let secretsConfig = ConfigReader(
provider: try await DirectoryFilesProvider(
directoryPath: "/run/secrets",
allowMissing: true
)
)

// Optional environment file
let envConfig = ConfigReader(
provider: try await EnvironmentVariablesProvider(
environmentFilePath: "/etc/app.env",
allowMissing: true
)
)

// Optional reloading configuration
let reloadingConfig = ConfigReader(
provider: try await ReloadingFileProvider<YAMLSnapshot>(
filePath: "/etc/dynamic-config.yaml",
allowMissing: true
)
)
```

> Important: The `allowMissing` parameter only affects missing files. Malformed files, such as invalid JSON and YAML syntax errors will still throw parsing errors regardless of this setting.

### Setting up a fallback hierarchy

Use multiple providers together to provide a configuration hierarchy that can override values at different levels.
Expand Down
39 changes: 39 additions & 0 deletions Sources/Configuration/Documentation.docc/Guides/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,45 @@ When no provider has the requested value:
- **Methods without defaults**: Return nil.
- **Required methods**: Throw an error.

#### File not found errors

File-based providers (``FileProvider``, ``ReloadingFileProvider``, ``DirectoryFilesProvider``, ``EnvironmentVariablesProvider`` with file path) can throw "file not found" errors when expected configuration files don't exist.

Common scenarios and solutions:

**Optional configuration files:**
```swift
// Problem: App crashes when optional config file is missing
let provider = try await FileProvider<JSONSnapshot>(filePath: "/etc/optional-config.json")

// Solution: Use allowMissing parameter
let provider = try await FileProvider<JSONSnapshot>(
filePath: "/etc/optional-config.json",
allowMissing: true
)
```

**Environment-specific files:**
```swift
// Different environments may have different config files
let configPath = "/etc/\(environment)/config.json"
let provider = try await FileProvider<JSONSnapshot>(
filePath: configPath,
allowMissing: true // Gracefully handle missing env-specific configs
)
```

**Container startup issues:**
```swift
// Config files might not be ready when container starts
let provider = try await ReloadingFileProvider<JSONSnapshot>(
filePath: "/mnt/config/app.json",
allowMissing: true // Allow startup with empty config, load when available
)
```

> Important: The `allowMissing` parameter only affects missing files or directories. Files with syntax errors (invalid JSON, YAML, and so on) will still throw parsing errors.

### Reloading provider troubleshooting

#### Configuration not updating
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ServiceLifecycle

let provider = try await ReloadingFileProvider<JSONSnapshot>(
filePath: "/etc/config.json",
allowMissing: true, // Optional: treat missing file as empty config
pollInterval: .seconds(15)
)

Expand Down Expand Up @@ -98,6 +99,58 @@ try await config.watchSnapshot { updates in
| **Service lifecycle** | Not required | Conforms to `Service` and must run in a `ServiceGroup` |
| **Configuration updates** | Require restart | Automatic reload |

### Handling missing files during reloading

Reloading providers support the `allowMissing` parameter to handle cases where configuration files might be temporarily missing or optional. This is particularly useful for:

- Optional configuration files that might not exist in all environments.
- Configuration files that are created or removed dynamically.
- Graceful handling of file system issues during service startup.

#### Missing file behavior

When `allowMissing` is `false` (the default), missing files cause errors:

```swift
let provider = try await ReloadingFileProvider<JSONSnapshot>(
filePath: "/etc/config.json",
allowMissing: false // Default: throw error if file is missing
)
// Will throw an error if config.json doesn't exist
```

When `allowMissing` is `true`, missing files are treated as empty configuration:

```swift
let provider = try await ReloadingFileProvider<JSONSnapshot>(
filePath: "/etc/config.json",
allowMissing: true // Treat missing file as empty config
)
// Won't throw if config.json is missing - uses empty config instead
```

#### Behavior during reloading

If a file becomes missing after the provider starts, the behavior depends on the `allowMissing` setting:

- **`allowMissing: false`**: The provider keeps the last known configuration and logs an error.
- **`allowMissing: true`**: The provider switches to empty configuration.

In both cases, when a valid file comes back, the provider will load it and recover.

```swift
// Example: File gets deleted during runtime
try await config.watchString(forKey: "database.host", default: "localhost") { updates in
for await host in updates {
// With allowMissing: true, this will receive "localhost" when file is removed
// With allowMissing: false, this keeps the last known value
print("Database host: \(host)")
}
}
```

> Important: The `allowMissing` parameter only affects missing files. Malformed files will still cause parsing errors regardless of this setting.

### Advanced features

#### Configuration-driven setup
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

### Creating a directory files provider

- ``init(directoryPath:secretsSpecifier:arraySeparator:keyEncoder:)``
- ``init(directoryPath:allowMissing:secretsSpecifier:arraySeparator:keyEncoder:)``
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

- ``init(secretsSpecifier:bytesDecoder:arraySeparator:)``
- ``init(environmentVariables:secretsSpecifier:bytesDecoder:arraySeparator:)``
- ``init(environmentFilePath:secretsSpecifier:bytesDecoder:arraySeparator:)``
- ``init(environmentFilePath:allowMissing:secretsSpecifier:bytesDecoder:arraySeparator:)``

### Inspecting an environment variable provider

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Creating a file provider

- ``init(snapshotType:parsingOptions:filePath:)``
- ``init(snapshotType:parsingOptions:filePath:allowMissing:)``
- ``init(snapshotType:parsingOptions:config:)``

### Reading configuration files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Creating a reloading file provider

- ``init(snapshotType:parsingOptions:filePath:pollInterval:logger:metrics:)``
- ``init(snapshotType:parsingOptions:filePath:allowMissing:pollInterval:logger:metrics:)``
- ``init(snapshotType:parsingOptions:config:logger:metrics:)``

### Service lifecycle
Expand Down
6 changes: 3 additions & 3 deletions Sources/Configuration/MultiProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ extension MultiProvider {
updateSequences: &updateSequences,
) { providerUpdateSequences in
let updateArrays = combineLatestMany(
elementType: (any ConfigSnapshotProtocol).self,
elementType: (any ConfigSnapshot).self,
failureType: Never.self,
providerUpdateSequences
)
Expand Down Expand Up @@ -364,8 +364,8 @@ nonisolated(nonsending) private func withProvidersWatchingValue<ReturnInner>(
@available(Configuration 1.0, *)
nonisolated(nonsending) private func withProvidersWatchingSnapshot<ReturnInner>(
providers: ArraySlice<any ConfigProvider>,
updateSequences: inout [any (AsyncSequence<any ConfigSnapshotProtocol, Never> & Sendable)],
body: ([any (AsyncSequence<any ConfigSnapshotProtocol, Never> & Sendable)]) async throws -> ReturnInner
updateSequences: inout [any (AsyncSequence<any ConfigSnapshot, Never> & Sendable)],
body: ([any (AsyncSequence<any ConfigSnapshot, Never> & Sendable)]) async throws -> ReturnInner
) async throws -> ReturnInner {
guard let provider = providers.first else {
// Recursion termination, once we've collected all update sequences, execute the body.
Expand Down
Loading
Loading