Skip to content

Commit 78d8b5c

Browse files
author
Alex Damsted
committed
Fix: Transient s3 blips are being problematic
1 parent a0600a4 commit 78d8b5c

3 files changed

Lines changed: 70 additions & 28 deletions

File tree

classes/local/object_manipulator/manipulator.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ public function execute(array $objectrecords) {
115115
} else {
116116
manager::update_object_by_hash($objectrecord->contenthash, $newlocation);
117117
}
118+
} catch (\Exception $e) {
119+
// Transient storage errors (e.g. S3 connectivity blip) must not abort
120+
// the entire batch — log and continue with the next object.
121+
$this->logger->error_log(
122+
'Error processing object ' . $objectrecord->contenthash . ': ' . $e->getMessage()
123+
);
118124
} finally {
119125
$objectlock->release();
120126
}

classes/local/store/object_file_system.php

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -255,18 +255,26 @@ public function is_file_readable_externally_by_storedfile(stored_file $file) {
255255
}
256256

257257
$path = $this->get_external_path_from_storedfile($file);
258-
if (is_readable($path)) {
259-
return true;
258+
error_clear_last();
259+
$readable = @is_readable($path);
260+
if (!$readable) {
261+
$error = error_get_last();
262+
if ($error !== null) {
263+
throw new \Exception(
264+
'Transient external storage error: ' . ($error['message'] ?? 'Unknown error')
265+
);
266+
}
260267
}
261-
262-
return false;
268+
return $readable;
263269
}
264270

265271
/**
266272
* is_file_readable_externally_by_hash
267273
* @param mixed $contenthash
268274
*
269275
* @return bool
276+
* @throws \Exception On transient external storage errors (e.g. S3 connectivity failure).
277+
* Returns false only when the object is confirmed absent.
270278
*/
271279
public function is_file_readable_externally_by_hash($contenthash) {
272280
if ($contenthash === sha1('')) {
@@ -277,8 +285,22 @@ public function is_file_readable_externally_by_hash($contenthash) {
277285

278286
$path = $this->get_external_path_from_hash($contenthash, false);
279287

280-
// Note - it is not possible to perform a content recovery safely from a hash alone.
281-
return is_readable($path);
288+
// Suppress the PHP Warning from the S3 StreamWrapper so we can inspect it ourselves.
289+
// If is_readable returns false AND a PHP error was raised, the failure is transient
290+
// (e.g. a connectivity blip) rather than the object genuinely being absent. In that
291+
// case we throw so callers can retry rather than silently recording OBJECT_LOCATION_ERROR.
292+
error_clear_last();
293+
$readable = @is_readable($path);
294+
if (!$readable) {
295+
$error = error_get_last();
296+
if ($error !== null) {
297+
throw new \Exception(
298+
'Transient external storage error for ' . $contenthash . ': ' .
299+
($error['message'] ?? 'Unknown error')
300+
);
301+
}
302+
}
303+
return $readable;
282304
}
283305

284306
/**
@@ -520,20 +542,24 @@ public function xsendfile_file(stored_file $file): bool {
520542
}
521543

522544
$contenthash = $file->get_contenthash();
523-
if (
524-
$this->presigned_url_configured() &&
525-
$this->presigned_url_should_redirect_file($file) &&
526-
$this->is_file_stored_externally_by_hash($contenthash)
527-
) {
528-
return $this->redirect_to_presigned_url($contenthash, headers_list());
529-
}
545+
try {
546+
if (
547+
$this->presigned_url_configured() &&
548+
$this->presigned_url_should_redirect_file($file) &&
549+
$this->is_file_readable_externally_by_hash($contenthash)
550+
) {
551+
return $this->redirect_to_presigned_url($contenthash, headers_list());
552+
}
530553

531-
$ranges = $this->get_valid_http_ranges($file->get_filesize());
532-
if (
533-
$this->externalclient->support_presigned_urls() && !empty($ranges) &&
534-
$this->is_file_stored_externally_by_hash($contenthash)
535-
) {
536-
return $this->externalclient->proxy_range_request($file, $ranges);
554+
$ranges = $this->get_valid_http_ranges($file->get_filesize());
555+
if (
556+
$this->externalclient->support_presigned_urls() && !empty($ranges) &&
557+
$this->is_file_readable_externally_by_hash($contenthash)
558+
) {
559+
return $this->externalclient->proxy_range_request($file, $ranges);
560+
}
561+
} catch (\Exception $e) {
562+
// Transient external storage error - fall back to standard file serving.
537563
}
538564

539565
return false;
@@ -555,12 +581,16 @@ public function xsendfile($contenthash) {
555581
return parent::xsendfile($contenthash);
556582
}
557583
$headers = headers_list();
558-
if (
559-
$this->presigned_url_configured() &&
560-
$this->is_file_stored_externally_by_hash($contenthash) &&
561-
$this->presigned_url_should_redirect($contenthash, $headers)
562-
) {
563-
return $this->redirect_to_presigned_url($contenthash, $headers);
584+
try {
585+
if (
586+
$this->presigned_url_configured() &&
587+
$this->is_file_readable_externally_by_hash($contenthash) &&
588+
$this->presigned_url_should_redirect($contenthash, $headers)
589+
) {
590+
return $this->redirect_to_presigned_url($contenthash, $headers);
591+
}
592+
} catch (\Exception $e) {
593+
// Transient external storage error - fall back to standard file serving.
564594
}
565595
return false;
566596
}
@@ -609,7 +639,13 @@ protected function get_object_handle_for_path($path, $type = \stored_file::FILE_
609639
switch ($type) {
610640
case \stored_file::FILE_HANDLE_FOPEN:
611641
$context = $this->externalclient->get_seekable_stream_context();
612-
return fopen($path, 'rb', false, $context);
642+
$handle = @fopen($path, 'rb', false, $context);
643+
if ($handle === false) {
644+
$error = error_get_last();
645+
$message = $error['message'] ?? 'Unknown filesystem error occurred.';
646+
throw new \Exception('Failed to open object file handle: ' . $message);
647+
}
648+
return $handle;
613649
case \stored_file::FILE_HANDLE_GZOPEN:
614650
// Binary reading of file in gz format.
615651
return gzopen($path, 'rb');

version.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525

2626
defined('MOODLE_INTERNAL') || die();
2727

28-
$plugin->version = 2026041005; // The current plugin version (Date: YYYYMMDDXX).
29-
$plugin->release = 2026041005; // Same as version.
28+
$plugin->version = 2026041006; // The current plugin version (Date: YYYYMMDDXX).
29+
$plugin->release = 2026041006; // Same as version.
3030
$plugin->requires = 2024042200; // Requires 4.4.
3131
$plugin->component = "tool_objectfs";
3232
$plugin->maturity = MATURITY_STABLE;

0 commit comments

Comments
 (0)