Skip to content

Commit 4a4a7b7

Browse files
committed
Track removed tokens and throw TaskAbortedException only when a matching token is detected.
1 parent f4fa758 commit 4a4a7b7

File tree

3 files changed

+124
-30
lines changed

3 files changed

+124
-30
lines changed

app/lib/task/backend.dart

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import 'package:pub_dev/task/global_lock.dart';
3838
import 'package:pub_dev/task/handlers.dart';
3939
import 'package:pub_dev/task/models.dart'
4040
show
41+
AbortedTokenInfo,
4142
PackageState,
4243
PackageStateInfo,
4344
PackageVersionStateInfo,
@@ -436,9 +437,19 @@ class TaskBackend {
436437

437438
// List of versions that are tracked, but don't exist. These have
438439
// probably been deselected by _versionsToTrack.
439-
final deselectedVersions = [
440-
...state.versions!.keys.whereNot(versions.contains),
441-
];
440+
final deselectedVersions = <String>[];
441+
for (final e in state.versions!.entries) {
442+
if (!versions.contains(e.key)) {
443+
deselectedVersions.add(e.key);
444+
445+
final token = e.value.secretToken;
446+
if (token != null) {
447+
state.abortedTokens ??= <AbortedTokenInfo>[];
448+
state.abortedTokens!.add(AbortedTokenInfo(
449+
token: token, expires: clock.fromNow(days: 1)));
450+
}
451+
}
452+
}
442453

443454
// There should never be an overlap between versions untracked and
444455
// versions that tracked by now deselected.
@@ -467,6 +478,7 @@ class TaskBackend {
467478
),
468479
});
469480
state.derivePendingAt();
481+
state.abortedTokens?.removeWhere((t) => t.expires.isAfter(clock.now()));
470482

471483
_log.info('Update state tracking for $packageName');
472484
tx.insert(state);
@@ -611,18 +623,8 @@ class TaskBackend {
611623

612624
final key = PackageState.createKey(_db, runtimeVersion, package);
613625
final state = await _db.lookupOrNull<PackageState>(key);
614-
if (state == null || state.versions![version] == null) {
615-
throw NotFoundException.resource('$package/$version');
616-
}
617-
final versionState = state.versions![version]!;
618-
619-
// Check the secret token
620-
if (!versionState.isAuthorized(_extractBearerToken(request))) {
621-
throw AuthenticationException.authenticationRequired();
622-
}
623-
assert(versionState.scheduled != initialTimestamp);
624-
assert(versionState.instance != null);
625-
assert(versionState.zone != null);
626+
final versionState =
627+
_extractAndVerifyVersionState(package, version, state, request);
626628

627629
// Set expiration of signed URLs to remaining execution time + 5 min to
628630
// allow for clock skew.
@@ -685,24 +687,13 @@ class TaskBackend {
685687
await withRetryTransaction(_db, (tx) async {
686688
final key = PackageState.createKey(_db, runtimeVersion, package);
687689
final state = await tx.lookupOrNull<PackageState>(key);
688-
if (state == null || state.versions![version] == null) {
689-
throw TaskAbortedException(
690-
'$package/$version is no longer selected for analysis.');
691-
}
692-
final versionState = state.versions![version]!;
693-
694-
// Check the secret token
695-
if (!versionState.isAuthorized(_extractBearerToken(request))) {
696-
throw TaskAbortedException('Secret token is no longer accepted.');
697-
}
698-
assert(versionState.scheduled != initialTimestamp);
699-
assert(versionState.instance != null);
700-
assert(versionState.zone != null);
690+
final versionState =
691+
_extractAndVerifyVersionState(package, version, state, request);
701692

702693
// Update dependencies, if pana summary has dependencies
703694
if (summary != null && summary.allDependencies != null) {
704695
final updatedDependencies = _updatedDependencies(
705-
state.dependencies,
696+
state!.dependencies,
706697
summary.allDependencies,
707698
// for logging only
708699
package: package,
@@ -720,7 +711,7 @@ class TaskBackend {
720711
instance = versionState.instance!;
721712

722713
// Remove instanceName, zone, secretToken, and set attempts = 0
723-
state.versions![version] = PackageVersionStateInfo(
714+
state!.versions![version] = PackageVersionStateInfo(
724715
scheduled: versionState.scheduled,
725716
docs: hasDocIndexHtml,
726717
pana: summary != null,
@@ -1170,6 +1161,41 @@ String? _extractBearerToken(shelf.Request request) {
11701161
return parts.last.trim();
11711162
}
11721163

1164+
PackageVersionStateInfo _extractAndVerifyVersionState(
1165+
String package,
1166+
String version,
1167+
PackageState? state,
1168+
shelf.Request request,
1169+
) {
1170+
final token = _extractBearerToken(request);
1171+
if (token == null) {
1172+
throw AuthenticationException.authenticationRequired();
1173+
}
1174+
if (state == null) {
1175+
throw NotFoundException.resource('$package/$version');
1176+
}
1177+
final versionState = state.versions![version];
1178+
if (versionState == null) {
1179+
// check if the task was aborted
1180+
final abortedToken =
1181+
state.abortedTokens?.firstWhereOrNull((t) => t.token == token);
1182+
if (abortedToken != null && abortedToken.expires.isBefore(clock.now())) {
1183+
throw TaskAbortedException('$package/$version has been aborted.');
1184+
}
1185+
// otherwise throw a generic not found error
1186+
throw NotFoundException.resource('$package/$version');
1187+
}
1188+
1189+
// Check the secret token
1190+
if (!versionState.isAuthorized(token)) {
1191+
throw AuthenticationException.authenticationRequired();
1192+
}
1193+
assert(versionState.scheduled != initialTimestamp);
1194+
assert(versionState.instance != null);
1195+
assert(versionState.zone != null);
1196+
return versionState;
1197+
}
1198+
11731199
/// Given a list of versions return the list of versions that should be
11741200
/// tracked for analysis.
11751201
///

app/lib/task/models.dart

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:convert' show json;
66

77
import 'package:clock/clock.dart';
88
import 'package:json_annotation/json_annotation.dart';
9+
import 'package:pub_dev/admin/actions/actions.dart';
910

1011
import '../shared/datastore.dart' as db;
1112
import '../shared/versions.dart' as shared_versions;
@@ -107,6 +108,12 @@ class PackageState extends db.ExpandoModel<String> {
107108
@PackageVersionStateMapProperty(required: true)
108109
Map<String, PackageVersionStateInfo>? versions;
109110

111+
/// The list of tokens that were removed from this [PackageState].
112+
/// When a worker reports back using one of these tokens, they will
113+
/// recieve a [TaskAbortedException].
114+
@AbortedTokenListProperty()
115+
List<AbortedTokenInfo>? abortedTokens;
116+
110117
/// Next [DateTime] at which point some package version becomes pending.
111118
@db.DateTimeProperty(required: true, indexed: true)
112119
DateTime? pendingAt;
@@ -407,3 +414,52 @@ enum PackageVersionStatus {
407414
/// Analysis failed to report a result.
408415
failed,
409416
}
417+
418+
/// Tracks a token that was removed from the [PackageState], but a worker
419+
/// may still use it to report a completed task. Such workers may recieve
420+
/// an error code that says they shouldn't really panic on the rejection.
421+
@JsonSerializable()
422+
class AbortedTokenInfo {
423+
final String token;
424+
final DateTime expires;
425+
426+
AbortedTokenInfo({
427+
required this.token,
428+
required this.expires,
429+
});
430+
431+
factory AbortedTokenInfo.fromJson(Map<String, dynamic> m) =>
432+
_$AbortedTokenInfoFromJson(m);
433+
Map<String, dynamic> toJson() => _$AbortedTokenInfoToJson(this);
434+
}
435+
436+
/// A [db.Property] encoding a List os [AbortedTokenInfo] as JSON.
437+
class AbortedTokenListProperty extends db.Property {
438+
const AbortedTokenListProperty({String? propertyName, bool required = false})
439+
: super(propertyName: propertyName, required: required, indexed: false);
440+
441+
@override
442+
Object? encodeValue(
443+
db.ModelDB mdb,
444+
Object? value, {
445+
bool forComparison = false,
446+
}) =>
447+
json.encode(
448+
(value as List<AbortedTokenInfo>?)?.map((e) => e.toJson()).toList());
449+
450+
@override
451+
Object? decodePrimitiveValue(
452+
db.ModelDB mdb,
453+
Object? value,
454+
) =>
455+
value == null
456+
? null
457+
: (json.decode(value as String) as List?)
458+
?.map((e) => AbortedTokenInfo.fromJson(e as Map<String, dynamic>))
459+
.toList();
460+
461+
@override
462+
bool validate(db.ModelDB mdb, Object? value) =>
463+
super.validate(mdb, value) &&
464+
(value == null || value is List<AbortedTokenInfo>);
465+
}

app/lib/task/models.g.dart

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

0 commit comments

Comments
 (0)