-
Notifications
You must be signed in to change notification settings - Fork 20
PE-9210: A share dialog that hangs, keys shown in the clear, and reads that never time out #2186
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
base: dev
Are you sure you want to change the base?
Changes from 3 commits
17fae94
6edd4e4
081e515
8e17162
6ea535a
7fc8539
b3aa88b
65c5c24
9401f03
2445727
e24bdc4
818b312
16f4aad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,6 +100,35 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
|
|
||
| Future<void>? _backgroundWork; | ||
|
|
||
| /// The longest a single network read here may take before it is abandoned. | ||
| /// | ||
| /// The data path has been bounded for a long time - [DataGatewayFallback] | ||
| /// gives every fetch a request timeout, a total timeout and a hedge. The | ||
| /// GraphQL reads that run *in front of* it had nothing: [GraphQLRetry] | ||
| /// retries a call that fails, but sets no timeout, so a connection that | ||
| /// errors is retried and a connection that simply hangs is not. This page | ||
| /// would sit on its skeleton forever. | ||
| /// | ||
| /// Sized well above a healthy read and well below a recipient's patience. | ||
| /// Anything that trips it lands in the load failure state, which already | ||
| /// offers Retry. | ||
| static const defaultReadTimeout = Duration(seconds: 15); | ||
|
|
||
| final Duration _readTimeout; | ||
|
|
||
| /// Bounds [future], naming [what] so a timeout is legible in the log. | ||
| /// | ||
| /// A [TimeoutException] is deliberately left to propagate: every caller on | ||
| /// the critical path already handles a failed read, either by degrading to | ||
| /// another resolution path or by emitting the failure state. | ||
| Future<T> _bounded<T>(Future<T> future, String what) => future.timeout( | ||
| _readTimeout, | ||
| onTimeout: () => throw TimeoutException( | ||
| 'Timed out after ${_readTimeout.inSeconds}s while $what', | ||
| _readTimeout, | ||
| ), | ||
| ); | ||
|
|
||
|
Comment on lines
+103
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 5 \
'Future\.timeout|\.timeout\(|getLatestFileEntityWithId|getOwnerForFileEntityWithId|getTransactionDetails|fetchLicenseForRevision' \
lib/blocs/shared_file/shared_file_cubit.dart \
lib/services/arweave/arweave_service.dartRepository: ardriveapp/ardrive-web Length of output: 19109 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- shared-file cubit structure ---'
ast-grep outline lib/blocs/shared_file/shared_file_cubit.dart
printf '%s\n' '--- relevant cubit sections ---'
sed -n '480,565p;780,825p;930,980p;1260,1405p;1505,1585p' \
lib/blocs/shared_file/shared_file_cubit.dart
printf '%s\n' '--- Arweave service query and retry definitions ---'
rg -n -C 8 \
'graphQLRetry|Future<.*execute|execute\(|getOwnerForFileEntityWithId|getLatestFileEntityWithId|getTransactionDetailsWithSignature' \
lib/services/arweave/arweave_service.dart lib
printf '%s\n' '--- dependency and SDK declarations ---'
rg -n -C 3 'graphql|dart:async|environment:|sdk:' pubspec.yaml pubspec.lock libRepository: ardriveapp/ardrive-web Length of output: 50379 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- GraphQLRetry files ---'
fd -i 'graphql.*retry|retry.*graphql' lib test
rg -l 'class GraphQLRetry|GraphQLRetry' lib test
printf '%s\n' '--- GraphQLRetry implementation ---'
files=$(rg -l 'class GraphQLRetry|GraphQLRetry' lib test | tr '\n' ' ')
for file in $files; do
echo "### $file"
rg -n -C 12 'class GraphQLRetry|Future<|execute\(|maxAttempts|timeout|cancel|dispose' "$file"
done
printf '%s\n' '--- shared-file async entry points and call sites ---'
rg -n -C 6 \
'await _resolveTargetRevision|await _fetchSharedRevision|await _arweave\.getLatestFileEntityWithId|await _arweave\.getOwnerForFileEntityWithId|await _arweave\.getTransactionDetails|await _arweave\.getEntityDataFromNetwork|await fetchLicenseForRevision|await _fetchLicense|await _checkFreshness|await _fileOwnerAddress|_runBackgroundWork|loadFileDetails|submit\(' \
lib/blocs/shared_file/shared_file_cubit.dart
printf '%s\n' '--- service method bodies ---'
sed -n '240,275p;1430,1595p' lib/services/arweave/arweave_service.dartRepository: ardriveapp/ardrive-web Length of output: 26667 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- GraphQLRetry implementation ---'
cat -n lib/utils/graphql_retry.dart
printf '%s\n' '--- timeout-related shared-file tests ---'
sed -n '150,235p' test/blocs/shared_file/shared_file_cubit_test.dart
rg -n -C 8 \
'readTimeout|hang|backgroundWork|retry|SharedFileLoadFailure|Completer' \
test/blocs/shared_file/shared_file_cubit_test.dart
printf '%s\n' '--- foreground resolution body ---'
sed -n '740,935p' lib/blocs/shared_file/shared_file_cubit.dart
printf '%s\n' '--- service request bodies ---'
sed -n '1433,1595p' lib/services/arweave/arweave_service.dart
printf '%s\n' '--- GraphQL package versions ---'
rg -n -C 3 'graphql|artemis|http:' pubspec.yaml pubspec.lockRepository: ardriveapp/ardrive-web Length of output: 49462 Route every shared-file network read through a cancellable timeout.
🤖 Prompt for AI Agents |
||
| SharedFileCubit({ | ||
| required this.fileId, | ||
| this.fileKey, | ||
|
|
@@ -109,10 +138,12 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
| required licenseService, | ||
| ArDriveCrypto? crypto, | ||
| Duration propagationRetryDelay = const Duration(seconds: 3), | ||
| Duration readTimeout = defaultReadTimeout, | ||
| }) : _arweave = arweave, | ||
| _licenseService = licenseService, | ||
| _crypto = crypto ?? ArDriveCrypto(), | ||
| _propagationRetryDelay = propagationRetryDelay, | ||
| _readTimeout = readTimeout, | ||
| // A v2 link can paint its skeleton with the real name and size before | ||
| // a single byte has been fetched. | ||
| super(SharedFileLoadInProgress(payload: linkPayload)) { | ||
|
|
@@ -276,9 +307,9 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
| emit(current.copyWith(activityStatus: SharedFileActivityStatus.loading)); | ||
|
|
||
| try { | ||
| final entities = await _arweave.getAllFileEntitiesWithId( | ||
| fileId, | ||
| fileKey, | ||
| final entities = await _bounded( | ||
| _arweave.getAllFileEntitiesWithId(fileId, fileKey), | ||
| 'reading the file\'s version history', | ||
| ); | ||
|
|
||
| if (_isStale(resolution)) { | ||
|
|
@@ -357,7 +388,10 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
| FileEntity? latest; | ||
|
|
||
| try { | ||
| latest = await _arweave.getLatestFileEntityWithId(fileId, fileKey); | ||
| latest = await _bounded( | ||
| _arweave.getLatestFileEntityWithId(fileId, fileKey), | ||
| 'checking for a newer revision', | ||
| ); | ||
| } catch (e, stacktrace) { | ||
| logger.e( | ||
| 'Failed to load the newest revision of the shared file', | ||
|
|
@@ -596,7 +630,10 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
| _SharedRevision? shared; | ||
|
|
||
| try { | ||
| shared = await _fetchSharedRevision(metadataTxId, fileKey); | ||
| shared = await _bounded( | ||
| _fetchSharedRevision(metadataTxId, fileKey), | ||
| 'reading the metadata transaction the link names', | ||
| ); | ||
| } on EntityTransactionParseException { | ||
| if (_isStale(resolution)) { | ||
| return true; | ||
|
|
@@ -725,7 +762,10 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
| emit(SharedFileLoadInProgress(payload: linkPayload)); | ||
| } | ||
|
|
||
| final privacy = await _arweave.getFilePrivacyForId(fileId); | ||
| final privacy = await _bounded( | ||
| _arweave.getFilePrivacyForId(fileId), | ||
| 'looking up whether the file is private', | ||
| ); | ||
|
|
||
| if (_isStale(resolution)) { | ||
| return; | ||
|
|
@@ -735,9 +775,9 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
| _emitLocked(linkPayload); | ||
| return; | ||
| } | ||
| final allEntities = await _arweave.getAllFileEntitiesWithId( | ||
| fileId, | ||
| fileKey, | ||
| final allEntities = await _bounded( | ||
| _arweave.getAllFileEntitiesWithId(fileId, fileKey), | ||
| 'reading the file\'s revisions', | ||
| ); | ||
|
|
||
| if (_isStale(resolution)) { | ||
|
|
@@ -764,9 +804,9 @@ class SharedFileCubit extends Cubit<SharedFileState> { | |
| // revisions are in reverse chronological order, so first is most recent | ||
| final target = _targetRevision(revisions); | ||
| final latestLicense = target.licenseTxId != null | ||
| ? await fetchLicenseForRevision( | ||
| target, | ||
| owner: ownerAddress, | ||
| ? await _bounded( | ||
| fetchLicenseForRevision(target, owner: ownerAddress), | ||
| 'reading the file\'s license', | ||
| ) | ||
| : null; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Guard the new
driveNameLoadercall against failures.This code runs inside the
Future.microtaskstarted at line 70.initializeFormis called from the constructor at line 55 and its future is never awaited.driveNameLoaderusestry/finallywith nocatch, and_arweave.getLatestDriveEntityWithIdperforms network work. A network or decode failure therefore escapes as an unhandled asynchronous error, and the cubit emits no failure state.driveNameLoaderalso returnsfalsewhen the key is invalid or the entity is missing. In that case the name stays empty, the auto-submit at line 109 is skipped, and the user gets no feedback.Wrap the call and emit a failure state when name resolution fails.
🛡️ Proposed fix
if (driveNameController.text.isEmpty && driveKeyController.text.isNotEmpty) { - await driveNameLoader(); - - if (isClosed) return; + bool resolved = false; + + try { + resolved = await driveNameLoader(); + } catch (e, stacktrace) { + logger.e( + 'Failed to resolve the name of drive ' + '${driveIdController.text}', + e, + stacktrace, + ); + } + + if (isClosed) return; + + if (!resolved) { + emit(DriveAttachDriveNotFound()); + return; + } }📝 Committable suggestion
🤖 Prompt for AI Agents