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

feat(core): clean up providers during shutdown and enable transaction context propagation #22

Open
wants to merge 17 commits into
base: qa
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
.dart_tool/
.dart_tool/
2 changes: 1 addition & 1 deletion open-feature-dart-server-sdk/lib/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class FeatureClient {
HookStage.ERROR,
flagKey,
currentContext,
error: error,
error: error,
);
return defaultValue;
} finally {
Expand Down
95 changes: 75 additions & 20 deletions open-feature-dart-server-sdk/lib/open_feature_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import 'domain_manager.dart'; // Required for @visibleForTesting
import 'feature_provider.dart';
import 'package:meta/meta.dart';

import 'transaction_context.dart';


// Define OpenFeatureEventType to represent different event types.
enum OpenFeatureEventType {
providerChanged,
Expand All @@ -17,19 +20,23 @@ class OpenFeatureEvent {
final OpenFeatureEventType type;
final String message;
final dynamic data;

OpenFeatureEvent(this.type, this.message, {this.data});
}




/// Abstract OpenFeatureProvider interface for extensibility
abstract class OpenFeatureProvider {
static final Logger _logger = Logger('OpenFeatureProvider');

String get name;



// Shutdown method for cleaning up resources.
Future<void> shutdown() async {
_logger.info('Shutting down provider: $name');
// Default implementation does nothing.
}

// Generic method to get a feature flag's value
Future<dynamic> getFlag(String flagKey, {Map<String, dynamic>? context});
}
Expand All @@ -43,21 +50,35 @@ class OpenFeatureNoOpProvider implements OpenFeatureProvider {
Future<dynamic> getFlag(String flagKey,
{Map<String, dynamic>? context}) async {

OpenFeatureProvider._logger
.info('Returning default value for flag: $flagKey');

return null; // Return null or default values for flags.
}

// Implement the shutdown method (even if it does nothing).
@override
Future<void> shutdown() async {
// No-op shutdown implementation (does nothing).
OpenFeatureProvider._logger.info('Shutting down provider: $name');

}
}

/// Global evaluation context shared across feature evaluations
class OpenFeatureEvaluationContext {
final Map<String, dynamic> attributes;
TransactionContext?
transactionContext; // Optional, for transaction-specific data

OpenFeatureEvaluationContext(this.attributes);
OpenFeatureEvaluationContext(this.attributes, {this.transactionContext});

/// Merge this context with another context
OpenFeatureEvaluationContext merge(OpenFeatureEvaluationContext other) {
return OpenFeatureEvaluationContext({...attributes, ...other.attributes});
return OpenFeatureEvaluationContext(
{...attributes, ...other.attributes},
transactionContext: other.transactionContext ?? this.transactionContext,
);
}
}

Expand Down Expand Up @@ -125,6 +146,9 @@ class OpenFeatureAPI {
// Domain manager to manage client-provider bindings
final DomainManager _domainManager = DomainManager();

// Stack to manage transaction contexts
final List<TransactionContext> _transactionContextStack = [];


// Extension management
final Map<String, ExtensionConfig> _extensions = {};
Expand Down Expand Up @@ -165,18 +189,16 @@ class OpenFeatureAPI {
void dispose() {
_logger.info('Disposing OpenFeatureAPI resources.');
_providerStreamController.close();
_eventStreamController.close();
_eventStreamController.close();

_extensionEventController.close();

}

// Original API methods
void setProvider(OpenFeatureProvider provider) {
_logger.info('Provider is being set to: ${provider.name}');
_provider = provider;


// Emit provider update
_providerStreamController.add(provider);

Expand Down Expand Up @@ -207,6 +229,14 @@ class OpenFeatureAPI {
_hooks.addAll(hooks);
}

/// Reset the singleton instance for testing purposes.
///
/// This ensures a clean state for each test case.
@visibleForTesting
static void resetInstance() {
_instance?.dispose();
_instance = null;
}

/// Emit an event to the event stream.
void _emitEvent(OpenFeatureEvent event) {
Expand All @@ -232,14 +262,10 @@ class OpenFeatureAPI {
Stream<OpenFeatureProvider> get providerUpdates =>
_providerStreamController.stream;



// New extension-related methods
Future<void> registerExtension(
String extensionId, ExtensionConfig config) async {



String extensionId, ExtensionConfig config) async {
_logger.info('Registering extension: $extensionId');

// Validate dependencies
Expand All @@ -266,21 +292,18 @@ class OpenFeatureAPI {
extensionId: extensionId,
type: 'REGISTRATION',
status: 'UNREGISTERED',
));
}
)); }

T? getExtension<T>(String extensionId) {
return _extensionInstances[extensionId] as T?;
}

List<String> getRegisteredExtensions() {
return List.unmodifiable(_extensions.keys);
}

Stream<ExtensionEvent> get extensionEvents =>
_extensionEventController.stream;


Future<bool> evaluateBooleanFlag(String flagKey, String clientId,
{Map<String, dynamic>? context}) async {
// Get provider for the client
Expand All @@ -289,7 +312,6 @@ class OpenFeatureAPI {
_logger.info('Using provider $providerName for client $clientId');
_runBeforeEvaluationHooks(flagKey, context);


try {
final result = await _provider.getFlag(flagKey, context: context);

Expand Down Expand Up @@ -349,7 +371,40 @@ class OpenFeatureAPI {

/// Streams for listening to events and provider updates.
Stream<OpenFeatureEvent> get events => _eventStreamController.stream;


// **Shutdown**: Gracefully clean up the provider during shutdown
Future<void> shutdown() async {
_logger.info('Shutting down OpenFeatureAPI...');
await _provider.shutdown(); // Shutdown the provider
// Optionally, cleanup transaction contexts

_transactionContextStack.clear();
dispose();
}

// **Transaction Context Propagation**: Set a specific evaluation context for a transaction
void pushTransactionContext(TransactionContext context) {
_transactionContextStack.add(context);
_logger.info('Pushed new transaction context: ${context.id}');
}

TransactionContext? popTransactionContext() {
if (_transactionContextStack.isNotEmpty) {
final context = _transactionContextStack.removeLast();
_logger.info('Popped transaction context: ${context.id}');
return context;
} else {
_logger.warning('No transaction context to pop');
return null;
}
}

TransactionContext? get currentTransactionContext {
return _transactionContextStack.isNotEmpty
? _transactionContextStack.last
: null;
}

}

// Dependency Injection for managing the singleton lifecycle
Expand Down
26 changes: 26 additions & 0 deletions open-feature-dart-server-sdk/lib/transaction_context.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class TransactionContext {
final String id; // Unique identifier for the transaction context
final Map<String, dynamic> attributes;

TransactionContext(this.id, this.attributes);

/// Merge this context with another context
TransactionContext merge(TransactionContext other) {
return TransactionContext(id, {...attributes, ...other.attributes});
}

/// Get a specific attribute from the context
dynamic get(String key) {
return attributes[key];
}

/// Set an attribute in the context
void set(String key, dynamic value) {
attributes[key] = value;
}

/// Clear all attributes (for transaction completion)
void clear() {
attributes.clear();
}
}
31 changes: 31 additions & 0 deletions open-feature-dart-server-sdk/test/domain_manager_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:open_feature_dart_server_sdk/domain_manager.dart';
import 'package:test/test.dart';
// Update with the correct path

void main() {
group('DomainManager', () {
DomainManager? domainManager;

setUp(() {
domainManager = DomainManager();
});

test('should bind a client to a provider', () {
domainManager!.bindClientToProvider('client1', 'providerA');
final providerName = domainManager!.getProviderForClient('client1');
expect(providerName, equals('providerA'));
});

test('should return null for unbound client', () {
final providerName = domainManager!.getProviderForClient('client2');
expect(providerName, isNull);
});

test('should update provider binding for a client', () {
domainManager!.bindClientToProvider('client1', 'providerA');
domainManager!.bindClientToProvider('client1', 'providerB');
final providerName = domainManager!.getProviderForClient('client1');
expect(providerName, equals('providerB'));
});
});
}
Loading
Loading