Skip to content
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
7 changes: 7 additions & 0 deletions packages/firebase_admin_sdk/lib/src/app/environment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,11 @@ abstract class Environment {
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
return env[cloudTasksEmulatorHost];
}

/// Gets the Kit Instance ID if running within a kit.
static String? getKitInstanceId() {
final env =
Zone.current[envSymbol] as Map<String, String>? ?? Platform.environment;
return env['FIREBASE_KIT_INSTANCE_ID'];
}
Comment on lines +178 to +183

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can a firebase doc link when this is released, so we know how/from where this 'FIREBASE_KIT_INSTANCE_ID' is injected?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely worth considering. It is a reserved env var; I could see us documenting it, though with this API it shouldn't actually be necessary to set or read directly I believe.

}
8 changes: 7 additions & 1 deletion packages/firebase_admin_sdk/lib/src/functions/functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,17 @@ class Functions implements FirebaseService {
/// final queue = functions.taskQueue('myFunction');
/// await queue.enqueue({'data': 'value'});
/// ```
TaskQueue taskQueue(String functionName, {String? extensionId}) {
TaskQueue taskQueue(
String functionName, {
@Deprecated('Use scope instead. Will be removed in a future major version.')
String? extensionId,
FunctionScope? scope,
}) {
return TaskQueue._(
functionName: functionName,
requestHandler: _requestHandler,
extensionId: extensionId,
scope: scope,
);
}

Expand Down
56 changes: 56 additions & 0 deletions packages/firebase_admin_sdk/lib/src/functions/functions_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,59 @@ class TaskOptions {
/// Contains experimental features that may change in future releases.
final TaskOptionsExperimental? experimental;
}

/// Represents the scope of a function in a task queue.
sealed class FunctionScope {
const FunctionScope._();

/// Targets a function co-deployed in the same codebase/deployment context.
const factory FunctionScope.current() = _CurrentFunctionScope;

/// Targets a global function (e.g. without kit or extension namespacing).
const factory FunctionScope.global() = _GlobalFunctionScope;

/// Targets a function deployed within a specific Firebase Extension.
const factory FunctionScope.extension(String extensionId) =
_ExtensionFunctionScope;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just clarifying, we didn't include kit here because we don't plan to expose it in the public api, yet?

  /// Targets a function deployed within a specific Firebase Kit.
  const factory FunctionScope.kit(String kit) = _KitFunctionScope;


class _GlobalFunctionScope extends FunctionScope {
const _GlobalFunctionScope() : super._();

@override
String toString() => 'FunctionScope.global()';
}

class _ExtensionFunctionScope extends FunctionScope {
const _ExtensionFunctionScope(this.extensionId) : super._();

final String extensionId;

@override
String toString() => 'FunctionScope.extension($extensionId)';
}

class _KitFunctionScope extends FunctionScope {
const _KitFunctionScope(this.kit) : super._();

final String kit;

@override
String toString() => 'FunctionScope.kit($kit)';
}

class _CurrentFunctionScope extends FunctionScope {
const _CurrentFunctionScope() : super._();

@override
String toString() => 'FunctionScope.current()';
}

class _ExtensionOrKitFunctionScope extends FunctionScope {
const _ExtensionOrKitFunctionScope(this.instance) : super._();

final String instance;

@override
String toString() => 'FunctionScope.extensionOrKit($instance)';
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class FunctionsRequestHandler {
Future<void> enqueue(
Map<String, dynamic> data,
String functionName,
String? extensionId,
FunctionScope scope,
TaskOptions? options,
) async {
validateNonEmptyString(functionName, 'functionName');
Expand All @@ -54,17 +54,16 @@ class FunctionsRequestHandler {

validateNonEmptyString(resources.resourceId, 'resourceId');

// Apply extension ID prefix if provided
var queueId = resources.resourceId;
if (extensionId != null && extensionId.isNotEmpty) {
queueId = 'ext-$extensionId-$queueId';
}
final (queueId, extensionOrKitId) = _resolveResourceId(
resources.resourceId,
scope,
);

// Build the task
final task = _buildTask(data, resources, queueId, options);

// Update task with proper authentication (OIDC token or Authorization header)
await _updateTaskAuth(task, await _httpClient.client, extensionId);
await _updateTaskAuth(task, await _httpClient.client, extensionOrKitId);

final parent = _httpClient.buildTasksParent(
projectId: resources.projectId!,
Expand Down Expand Up @@ -94,7 +93,7 @@ class FunctionsRequestHandler {
Future<void> delete(
String id,
String functionName,
String? extensionId,
FunctionScope scope,
) async {
validateNonEmptyString(functionName, 'functionName');
validateNonEmptyString(id, 'id');
Expand All @@ -117,11 +116,7 @@ class FunctionsRequestHandler {

validateNonEmptyString(resources.resourceId, 'resourceId');

// Apply extension ID prefix if provided
var queueId = resources.resourceId;
if (extensionId != null && extensionId.isNotEmpty) {
queueId = 'ext-$extensionId-$queueId';
}
final (queueId, _) = _resolveResourceId(resources.resourceId, scope);

// Build the full task name
final taskName = _httpClient.buildTaskName(
Expand All @@ -143,6 +138,28 @@ class FunctionsRequestHandler {
});
}

(String resourceId, String? extensionOrKitId) _resolveResourceId(
String resourceId,
FunctionScope scope,
) {
switch (scope) {
case _CurrentFunctionScope():
final kitInstanceId = Environment.getKitInstanceId();
if (kitInstanceId != null && kitInstanceId.isNotEmpty) {
return ('kit-$kitInstanceId-$resourceId', kitInstanceId);
}
return (resourceId, null);
case _GlobalFunctionScope():
return (resourceId, null);
case _ExtensionFunctionScope(:final extensionId):
return ('ext-$extensionId-$resourceId', extensionId);
case _KitFunctionScope(:final kit):
return ('kit-$kit-$resourceId', kit);
case _ExtensionOrKitFunctionScope(:final instance):
return ('ext-$instance-$resourceId', instance);
}
}

/// Parses a resource name into its components.
///
/// Supports:
Expand Down Expand Up @@ -255,7 +272,7 @@ class FunctionsRequestHandler {
Future<void> _updateTaskAuth(
tasks2.Task task,
googleapis_auth.AuthClient authClient,
String? extensionId,
String? extensionOrKitId,
) async {
final httpRequest = task.httpRequest!;

Expand All @@ -271,7 +288,9 @@ class FunctionsRequestHandler {
final isComputeEngine =
_httpClient.app.options.credential?.serviceAccountCredentials == null;

if (extensionId != null && extensionId.isNotEmpty && isComputeEngine) {
if (extensionOrKitId != null &&
extensionOrKitId.isNotEmpty &&
isComputeEngine) {
// Running as extension with ComputeEngine - use ID token with Authorization header.
final idToken = authClient.credentials.idToken;
if (idToken != null && idToken.isNotEmpty) {
Expand Down
97 changes: 88 additions & 9 deletions packages/firebase_admin_sdk/lib/src/functions/task_queue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,44 @@ class TaskQueue {
required String functionName,
required FunctionsRequestHandler requestHandler,
String? extensionId,
FunctionScope? scope,
}) : _functionName = functionName,
_requestHandler = requestHandler,
_extensionId = extensionId {
_requestHandler = requestHandler {
validateNonEmptyString(_functionName, 'functionName');
if (_extensionId != null) {
validateString(_extensionId, 'extensionId');

if (extensionId != null && scope != null) {
throw ArgumentError('Cannot set both extensionId and scope.');
}

if (extensionId != null) {
validateString(extensionId, 'extensionId');
_scope = extensionId.isEmpty
? const FunctionScope.current()
: _ExtensionOrKitFunctionScope(extensionId);
} else {
_scope = scope ?? const FunctionScope.current();
}

if (_scope is _ExtensionOrKitFunctionScope) {
final instance = (_scope as _ExtensionOrKitFunctionScope).instance;
final kitInstanceId = Environment.getKitInstanceId();
if (kitInstanceId != null &&
kitInstanceId.isNotEmpty &&
kitInstanceId == instance) {
_scope = _KitFunctionScope(instance);
print(
'Targeting your own extension or kit no longer requires a second parameter, '
'which can have performance implications. Please change the call '
"taskQueue('$_functionName', '$instance') to taskQueue('$_functionName') "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should this be a named argument? extensionId: '$instance'

"or taskQueue('$_functionName', scope: FunctionScope.current())",
);
Comment thread
inlined marked this conversation as resolved.
}
}
}

final String _functionName;
final FunctionsRequestHandler _requestHandler;
final String? _extensionId;
late FunctionScope _scope;

/// Enqueues a task with the given [data] payload.
///
Expand All @@ -59,8 +85,28 @@ class TaskQueue {
/// ```
///
/// Throws [FirebaseFunctionsAdminException] if the request fails.
Future<void> enqueue(Map<String, dynamic> data, [TaskOptions? options]) {
return _requestHandler.enqueue(data, _functionName, _extensionId, options);
Future<void> enqueue(
Map<String, dynamic> data, [
TaskOptions? options,
]) async {
final currentScope = _scope;
if (currentScope is! _ExtensionOrKitFunctionScope) {
await _requestHandler.enqueue(data, _functionName, currentScope, options);
return;
}

try {
await _requestHandler.enqueue(data, _functionName, currentScope, options);
} on FirebaseFunctionsAdminException catch (err) {
if (err.errorCode != FunctionsClientErrorCode.notFound) {
rethrow;
}
final tempKitScope = _KitFunctionScope(currentScope.instance);
await _requestHandler.enqueue(data, _functionName, tempKitScope, options);
// Only upgrade the stateful scope to kit if the retry request succeeds
_scope = tempKitScope;
_logFallbackWarning(_functionName, currentScope.instance);
}
}

/// Deletes a task from the queue by its [id].
Expand All @@ -74,7 +120,40 @@ class TaskQueue {
/// ```
///
/// Throws [FirebaseFunctionsAdminException] if the request fails.
Future<void> delete(String id) {
return _requestHandler.delete(id, _functionName, _extensionId);
Future<void> delete(String id) async {
try {
final currentScope = _scope;
if (currentScope is! _ExtensionOrKitFunctionScope) {
await _requestHandler.delete(id, _functionName, currentScope);
return;
}

try {
await _requestHandler.delete(id, _functionName, currentScope);
} on FirebaseFunctionsAdminException catch (err) {
if (err.errorCode != FunctionsClientErrorCode.notFound) {
rethrow;
}
// Not found, try fallback to kit scope.
final tempKitScope = _KitFunctionScope(currentScope.instance);
await _requestHandler.delete(id, _functionName, tempKitScope);
// Only upgrade the stateful scope to kit if the retry request succeeds
_scope = tempKitScope;
_logFallbackWarning(_functionName, currentScope.instance);
}
} on FirebaseFunctionsAdminException catch (err) {
if (err.errorCode != FunctionsClientErrorCode.notFound) {
rethrow;
}
// Swallow notFound errors as delete is idempotent.
}
}
Comment thread
inlined marked this conversation as resolved.

void _logFallbackWarning(String functionName, String instance) {
print(
'Targeting kit $instance with the legacy extensions API, '
'which has performance implications. Please change the call '
"taskQueue('$functionName', '$instance') to taskQueue('$functionName')",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should this be a named argument? extensionId: '$instance'

);
Comment thread
inlined marked this conversation as resolved.
}
}
Loading
Loading