diff options
23 files changed, 478 insertions, 19 deletions
diff --git a/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php b/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php index 2a48452d426..fd370ce60e5 100644 --- a/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php +++ b/apps/contactsinteraction/lib/BackgroundJob/CleanupJob.php @@ -27,6 +27,7 @@ namespace OCA\ContactsInteraction\BackgroundJob; use OCA\ContactsInteraction\Db\RecentContactMapper; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\TimedJob; class CleanupJob extends TimedJob { @@ -38,7 +39,8 @@ class CleanupJob extends TimedJob { RecentContactMapper $mapper) { parent::__construct($time); - $this->setInterval(12 * 60 * 60); + $this->setInterval(24 * 60 * 60); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); $this->mapper = $mapper; } diff --git a/apps/dav/lib/BackgroundJob/UploadCleanup.php b/apps/dav/lib/BackgroundJob/UploadCleanup.php index 70f4d6b9565..76906becb54 100644 --- a/apps/dav/lib/BackgroundJob/UploadCleanup.php +++ b/apps/dav/lib/BackgroundJob/UploadCleanup.php @@ -29,6 +29,7 @@ namespace OCA\DAV\BackgroundJob; use OC\User\NoUserException; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\TimedJob; use OCP\Files\File; @@ -51,6 +52,7 @@ class UploadCleanup extends TimedJob { // Run once a day $this->setInterval(60 * 60 * 24); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); } protected function run($argument) { diff --git a/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php b/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php index 07b66e2ecb6..138d4d7de2d 100644 --- a/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php +++ b/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php @@ -29,6 +29,7 @@ use OCA\Files_External\Lib\Auth\Password\LoginCredentials; use OCA\Files_External\Lib\StorageConfig; use OCA\Files_External\Service\UserGlobalStoragesService; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\TimedJob; use OCP\Security\ICredentialsManager; use OCP\IUser; @@ -53,6 +54,7 @@ class CredentialsCleanup extends TimedJob { // run every day $this->setInterval(24 * 60 * 60); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); } protected function run($argument) { diff --git a/apps/files_sharing/lib/ExpireSharesJob.php b/apps/files_sharing/lib/ExpireSharesJob.php index 1e56602a0bf..dd0979e4b0b 100644 --- a/apps/files_sharing/lib/ExpireSharesJob.php +++ b/apps/files_sharing/lib/ExpireSharesJob.php @@ -25,6 +25,7 @@ namespace OCA\Files_Sharing; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\TimedJob; use OCP\IDBConnection; use OCP\Share\Exceptions\ShareNotFound; @@ -50,6 +51,7 @@ class ExpireSharesJob extends TimedJob { // Run once a day $this->setInterval(24 * 60 * 60); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); } diff --git a/apps/settings/templates/settings/personal/personal.info.php b/apps/settings/templates/settings/personal/personal.info.php index c1a5735aa8f..959f59eaf34 100644 --- a/apps/settings/templates/settings/personal/personal.info.php +++ b/apps/settings/templates/settings/personal/personal.info.php @@ -65,6 +65,7 @@ script('settings', [ <div id="cropper" class="hidden"> <div class="inner-container"> + <p style="width: 300px; margin-top: 0.5rem"><?php p($l->t('Please note that it can take up to 24 hours for the avatar to get updated everywhere.')); ?></p> <div class="inlineblock button" id="abortcropperbutton"><?php p($l->t('Cancel')); ?></div> <div class="inlineblock button primary" id="sendcropperbutton"><?php p($l->t('Choose as profile picture')); ?></div> </div> diff --git a/apps/twofactor_backupcodes/lib/BackgroundJob/RememberBackupCodesJob.php b/apps/twofactor_backupcodes/lib/BackgroundJob/RememberBackupCodesJob.php index 51428f9b996..3eed1bb8dea 100644 --- a/apps/twofactor_backupcodes/lib/BackgroundJob/RememberBackupCodesJob.php +++ b/apps/twofactor_backupcodes/lib/BackgroundJob/RememberBackupCodesJob.php @@ -28,6 +28,7 @@ namespace OCA\TwoFactorBackupCodes\BackgroundJob; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\TimedJob; use OCP\IUserManager; @@ -60,6 +61,7 @@ class RememberBackupCodesJob extends TimedJob { $this->jobList = $jobList; $this->setInterval(60 * 60 * 24 * 14); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); } protected function run($argument) { diff --git a/build/integration/features/bootstrap/FederationContext.php b/build/integration/features/bootstrap/FederationContext.php index 2754cf668e6..423708adc10 100644 --- a/build/integration/features/bootstrap/FederationContext.php +++ b/build/integration/features/bootstrap/FederationContext.php @@ -28,6 +28,7 @@ */ use Behat\Behat\Context\Context; use Behat\Behat\Context\SnippetAcceptingContext; +use Behat\Gherkin\Node\TableNode; require __DIR__ . '/../../vendor/autoload.php'; @@ -39,6 +40,29 @@ class FederationContext implements Context, SnippetAcceptingContext { use AppConfiguration; use CommandLine; + /** @var string */ + private static $phpFederatedServerPid = ''; + + /** @var string */ + private $lastAcceptedRemoteShareId; + + /** + * @BeforeScenario + * @AfterScenario + * + * The server is started also after the scenarios to ensure that it is + * properly cleaned up if stopped. + */ + public function startFederatedServer() { + if (self::$phpFederatedServerPid !== '') { + return; + } + + $port = getenv('PORT_FED'); + + self::$phpFederatedServerPid = exec('php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!'); + } + /** * @BeforeScenario */ @@ -94,6 +118,37 @@ class FederationContext implements Context, SnippetAcceptingContext { } /** + * @Then remote share :count is returned with + * + * @param int $number + * @param TableNode $body + */ + public function remoteShareXIsReturnedWith(int $number, TableNode $body) { + $this->theHTTPStatusCodeShouldBe('200'); + $this->theOCSStatusCodeShouldBe('100'); + + if (!($body instanceof TableNode)) { + return; + } + + $returnedShare = $this->getXmlResponse()->data[0]; + if ($returnedShare->element) { + $returnedShare = $returnedShare->element[$number]; + } + + $defaultExpectedFields = [ + 'id' => 'A_NUMBER', + 'remote_id' => 'A_NUMBER', + 'accepted' => '1', + ]; + $expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash()); + + foreach ($expectedFields as $field => $value) { + $this->assertFieldIsInReturnedShare($field, $value, $returnedShare); + } + } + + /** * @When /^User "([^"]*)" from server "(LOCAL|REMOTE)" accepts last pending share$/ * @param string $user * @param string $server @@ -109,6 +164,30 @@ class FederationContext implements Context, SnippetAcceptingContext { $this->theHTTPStatusCodeShouldBe('200'); $this->theOCSStatusCodeShouldBe('100'); $this->usingServer($previous); + + $this->lastAcceptedRemoteShareId = $share_id; + } + + /** + * @When /^user "([^"]*)" deletes last accepted remote share$/ + * @param string $user + */ + public function deleteLastAcceptedRemoteShare($user) { + $this->asAn($user); + $this->sendingToWith('DELETE', "/apps/files_sharing/api/v1/remote_shares/" . $this->lastAcceptedRemoteShareId, null); + } + + /** + * @When /^remote server is stopped$/ + */ + public function remoteServerIsStopped() { + if (self::$phpFederatedServerPid === '') { + return; + } + + exec('kill ' . self::$phpFederatedServerPid); + + self::$phpFederatedServerPid = ''; } protected function resetAppConfigs() { diff --git a/build/integration/federation_features/federated.feature b/build/integration/federation_features/federated.feature index 17ec6b4b43e..8b627e55a42 100644 --- a/build/integration/federation_features/federated.feature +++ b/build/integration/federation_features/federated.feature @@ -278,13 +278,230 @@ Feature: federated + Scenario: List federated share from another server not accepted yet + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And Using server "LOCAL" + When As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + Then the list of returned shares has 0 shares + Scenario: List federated share from another server + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + When As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + Then the list of returned shares has 1 shares + And remote share 0 is returned with + | remote | http://localhost:8180/ | + | name | /remote-share.txt | + | owner | user1 | + | user | user0 | + | mountpoint | /remote-share.txt | + | mimetype | text/plain | + | mtime | A_NUMBER | + | permissions | 27 | + | type | file | + | file_id | A_NUMBER | + Scenario: List federated share from another server no longer reachable + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + And remote server is stopped + When As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + Then the list of returned shares has 1 shares + And remote share 0 is returned with + | remote | http://localhost:8180/ | + | name | /remote-share.txt | + | owner | user1 | + | user | user0 | + | mountpoint | /remote-share.txt | + Scenario: List federated share from another server no longer reachable after caching the file entry + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + # Checking that the file exists caches the file entry, which causes an + # exception to be thrown when getting the file info if the remote server is + # unreachable. + And as "user0" the file "/remote-share.txt" exists + And remote server is stopped + When As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + Then the list of returned shares has 1 shares + And remote share 0 is returned with + | remote | http://localhost:8180/ | + | name | /remote-share.txt | + | owner | user1 | + | user | user0 | + | mountpoint | /remote-share.txt | + Scenario: Delete federated share with another server + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/shares" + And the list of returned shares has 1 shares + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + And as "user0" the file "/remote-share.txt" exists + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + And Using server "REMOTE" + When As an "user1" + And Deleting last share + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/shares" + And the list of returned shares has 0 shares + And Using server "LOCAL" + And as "user0" the file "/remote-share.txt" does not exist + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + Scenario: Delete federated share from another server + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/shares" + And the list of returned shares has 1 shares + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + And as "user0" the file "/remote-share.txt" exists + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + When user "user0" deletes last accepted remote share + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And as "user0" the file "/remote-share.txt" does not exist + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + And Using server "REMOTE" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/shares" + And the list of returned shares has 0 shares + Scenario: Delete federated share from another server no longer reachable + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + And as "user0" the file "/remote-share.txt" exists + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + And remote server is stopped + When user "user0" deletes last accepted remote share + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And as "user0" the file "/remote-share.txt" does not exist + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + Scenario: Delete federated share file from another server + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/shares" + And the list of returned shares has 1 shares + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + And as "user0" the file "/remote-share.txt" exists + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + When User "user0" deletes file "/remote-share.txt" + Then the HTTP status code should be "204" + And as "user0" the file "/remote-share.txt" does not exist + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares + And Using server "REMOTE" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/shares" + And the list of returned shares has 0 shares + Scenario: Delete federated share file from another server no longer reachable + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And Using server "LOCAL" + And User "user0" from server "LOCAL" accepts last pending share + And as "user0" the file "/remote-share.txt" exists + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 1 shares + And remote server is stopped + When User "user0" deletes file "/remote-share.txt" + Then the HTTP status code should be "204" + And as "user0" the file "/remote-share.txt" does not exist + And As an "user0" + And sending "GET" to "/apps/files_sharing/api/v1/remote_shares" + And the list of returned shares has 0 shares diff --git a/build/integration/run.sh b/build/integration/run.sh index 4808ab58ef5..a20398e8ee9 100755 --- a/build/integration/run.sh +++ b/build/integration/run.sh @@ -38,11 +38,10 @@ php -S localhost:$PORT -t ../.. & PHPPID=$! echo $PHPPID +# The federated server is started and stopped by the tests themselves PORT_FED=$((8180 + $EXECUTOR_NUMBER)) echo $PORT_FED -php -S localhost:$PORT_FED -t ../.. & -PHPPID_FED=$! -echo $PHPPID_FED +export PORT_FED export TEST_SERVER_URL="http://localhost:$PORT/ocs/" export TEST_SERVER_FED_URL="http://localhost:$PORT_FED/ocs/" @@ -65,7 +64,6 @@ vendor/bin/behat --strict -f junit -f pretty $TAGS $SCENARIO_TO_RUN RESULT=$? kill $PHPPID -kill $PHPPID_FED if [ "$INSTALLED" == "true" ]; then diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 0b816e7f1ea..0d4328ae716 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -3943,7 +3943,7 @@ </NoInterfaceProperties> </file> <file src="lib/private/Files/Storage/Wrapper/Availability.php"> - <InvalidNullableReturnType occurrences="34"> + <InvalidNullableReturnType occurrences="33"> <code>copy</code> <code>copyFromStorage</code> <code>file_exists</code> @@ -3960,7 +3960,6 @@ <code>getMimeType</code> <code>getOwner</code> <code>getPermissions</code> - <code>hasUpdated</code> <code>hash</code> <code>isCreatable</code> <code>isDeletable</code> diff --git a/config/config.sample.php b/config/config.sample.php index 505728dc47d..4bb976fbcad 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -1213,6 +1213,20 @@ $CONFIG = [ */ 'maintenance' => false, +/** + * UTC Hour for maintenance windows + * + * Some background jobs only run once a day. When an hour is defined for this config, + * the background jobs which advertise themselves as not time sensitive will be + * delayed during the "working" hours and only run in the 4 hours after the given time. + * This is e.g. used for activity expiration, suspicious login training and update checks. + * + * A value of 1 e.g. will only run these background jobs between 01:00am UTC and 05:00am UTC. + * + * Defaults to ``100`` which disables the feature + */ +'maintenance_window_start' => 1, + /** * SSL diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index 3b87bf15b2f..a567baf5867 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -134,7 +134,7 @@ class AvatarController extends Controller { } // Cache for 1 day - $response->cacheFor(60 * 60 * 24); + $response->cacheFor(60 * 60 * 24, false, true); return $response; } diff --git a/core/Migrations/Version24000Date20220131153041.php b/core/Migrations/Version24000Date20220131153041.php new file mode 100644 index 00000000000..fd2902c1713 --- /dev/null +++ b/core/Migrations/Version24000Date20220131153041.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@nextcloud.com> + * + * @author Joas Schilling <coding@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version24000Date20220131153041 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('jobs'); + if (!$table->hasColumn('time_sensitive')) { + $table->addColumn('time_sensitive', Types::SMALLINT, [ + 'default' => 1, + ]); + $table->addIndex(['time_sensitive'], 'jobs_time_sensitive'); + return $schema; + } + return null; + } +} @@ -109,6 +109,28 @@ try { $config->setAppValue('core', 'backgroundjobs_mode', 'cron'); } + // Low-load hours + $onlyTimeSensitive = false; + $startHour = $config->getSystemValueInt('maintenance_window_start', 100); + if ($startHour <= 23) { + $date = new \DateTime('now', new \DateTimeZone('UTC')); + $currentHour = (int) $date->format('G'); + $endHour = $startHour + 4; + + if ($startHour <= 20) { + // Start time: 01:00 + // End time: 05:00 + // Only run sensitive tasks when it's before the start or after the end + $onlyTimeSensitive = $currentHour < $startHour || $currentHour > $endHour; + } else { + // Start time: 23:00 + // End time: 03:00 + $endHour -= 24; // Correct the end time from 27:00 to 03:00 + // Only run sensitive tasks when it's after the end and before the start + $onlyTimeSensitive = $currentHour > $endHour && $currentHour < $startHour; + } + } + // Work $jobList = \OC::$server->getJobList(); @@ -118,7 +140,7 @@ try { $endTime = time() + 14 * 60; $executedJobs = []; - while ($job = $jobList->getNext()) { + while ($job = $jobList->getNext($onlyTimeSensitive)) { if (isset($executedJobs[$job->getId()])) { $jobList->unlockJob($job); break; diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index c16cfbf0dfb..d71334752a3 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -974,6 +974,7 @@ return array( 'OC\\Core\\Migrations\\Version24000Date20211213081506' => $baseDir . '/core/Migrations/Version24000Date20211213081506.php', 'OC\\Core\\Migrations\\Version24000Date20211213081604' => $baseDir . '/core/Migrations/Version24000Date20211213081604.php', 'OC\\Core\\Migrations\\Version24000Date20211230140012' => $baseDir . '/core/Migrations/Version24000Date20211230140012.php', + 'OC\\Core\\Migrations\\Version24000Date20220131153041' => $baseDir . '/core/Migrations/Version24000Date20220131153041.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index dc08e6ed639..d57947dc501 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1003,6 +1003,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version24000Date20211213081506' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20211213081506.php', 'OC\\Core\\Migrations\\Version24000Date20211213081604' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20211213081604.php', 'OC\\Core\\Migrations\\Version24000Date20211230140012' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20211230140012.php', + 'OC\\Core\\Migrations\\Version24000Date20220131153041' => __DIR__ . '/../../..' . '/core/Migrations/Version24000Date20220131153041.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', diff --git a/lib/private/BackgroundJob/JobList.php b/lib/private/BackgroundJob/JobList.php index 8dd9e4466f1..887a3343166 100644 --- a/lib/private/BackgroundJob/JobList.php +++ b/lib/private/BackgroundJob/JobList.php @@ -184,9 +184,10 @@ class JobList implements IJobList { /** * get the next job in the list * + * @param bool $onlyTimeSensitive * @return IJob|null */ - public function getNext() { + public function getNext(bool $onlyTimeSensitive = true): ?IJob { $query = $this->connection->getQueryBuilder(); $query->select('*') ->from('jobs') @@ -195,6 +196,10 @@ class JobList implements IJobList { ->orderBy('last_checked', 'ASC') ->setMaxResults(1); + if ($onlyTimeSensitive) { + $query->andWhere($query->expr()->eq('time_sensitive', $query->createNamedParameter(IJob::TIME_SENSITIVE, IQueryBuilder::PARAM_INT))); + } + $update = $this->connection->getQueryBuilder(); $update->update('jobs') ->set('reserved_at', $update->createNamedParameter($this->timeFactory->getTime())) @@ -215,7 +220,7 @@ class JobList implements IJobList { if ($count === 0) { // Background job already executed elsewhere, try again. - return $this->getNext(); + return $this->getNext($onlyTimeSensitive); } $job = $this->buildJob($row); @@ -229,7 +234,7 @@ class JobList implements IJobList { $reset->execute(); // Background job from disabled app, try again. - return $this->getNext(); + return $this->getNext($onlyTimeSensitive); } return $job; @@ -323,6 +328,12 @@ class JobList implements IJobList { $query->update('jobs') ->set('last_run', $query->createNamedParameter(time(), IQueryBuilder::PARAM_INT)) ->where($query->expr()->eq('id', $query->createNamedParameter($job->getId(), IQueryBuilder::PARAM_INT))); + + if ($job instanceof \OCP\BackgroundJob\TimedJob + && !$job->isTimeSensitive()) { + $query->set('time_sensitive', $query->createNamedParameter(IJob::TIME_INSENSITIVE)); + } + $query->execute(); } diff --git a/lib/private/Files/Storage/Wrapper/Availability.php b/lib/private/Files/Storage/Wrapper/Availability.php index b6d1ba2178b..910ea369757 100644 --- a/lib/private/Files/Storage/Wrapper/Availability.php +++ b/lib/private/Files/Storage/Wrapper/Availability.php @@ -379,11 +379,15 @@ class Availability extends Wrapper { /** {@inheritdoc} */ public function hasUpdated($path, $time) { - $this->checkAvailability(); + if (!$this->isAvailable()) { + return false; + } try { return parent::hasUpdated($path, $time); } catch (StorageNotAvailableException $e) { - $this->setUnavailable($e); + // set unavailable but don't rethrow + $this->setUnavailable(null); + return false; } } @@ -449,7 +453,7 @@ class Availability extends Wrapper { /** * @throws StorageNotAvailableException */ - protected function setUnavailable(StorageNotAvailableException $e) { + protected function setUnavailable(?StorageNotAvailableException $e): void { $delay = self::RECHECK_TTL_SEC; if ($e instanceof StorageAuthException) { $delay = max( @@ -459,7 +463,9 @@ class Availability extends Wrapper { ); } $this->getStorageCache()->setAvailability(false, $delay); - throw $e; + if ($e !== null) { + throw $e; + } } diff --git a/lib/private/Security/Bruteforce/CleanupJob.php b/lib/private/Security/Bruteforce/CleanupJob.php index 1e5f83360d5..6faf853760a 100644 --- a/lib/private/Security/Bruteforce/CleanupJob.php +++ b/lib/private/Security/Bruteforce/CleanupJob.php @@ -26,6 +26,7 @@ declare(strict_types=1); namespace OC\Security\Bruteforce; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\TimedJob; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -41,6 +42,7 @@ class CleanupJob extends TimedJob { // Run once a day $this->setInterval(3600 * 24); + $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); } protected function run($argument) { diff --git a/lib/public/BackgroundJob/IJob.php b/lib/public/BackgroundJob/IJob.php index 341ae2ac545..5e8d4317505 100644 --- a/lib/public/BackgroundJob/IJob.php +++ b/lib/public/BackgroundJob/IJob.php @@ -35,6 +35,15 @@ use OCP\ILogger; */ interface IJob { /** + * @since 22.2.6 + */ + public const TIME_INSENSITIVE = 0; + /** + * @since 22.2.6 + */ + public const TIME_SENSITIVE = 1; + + /** * Run the background job with the registered argument * * @param IJobList $jobList The job list that manages the state of this job diff --git a/lib/public/BackgroundJob/IJobList.php b/lib/public/BackgroundJob/IJobList.php index 299d6725229..2a925999331 100644 --- a/lib/public/BackgroundJob/IJobList.php +++ b/lib/public/BackgroundJob/IJobList.php @@ -85,10 +85,11 @@ interface IJobList { /** * get the next job in the list * + * @param bool $onlyTimeSensitive * @return \OCP\BackgroundJob\IJob|null - * @since 7.0.0 + * @since 7.0.0 - In 22.2.6 parameter $onlyTimeSensitive got added */ - public function getNext(); + public function getNext(bool $onlyTimeSensitive = false): ?IJob; /** * @param int $id diff --git a/lib/public/BackgroundJob/TimedJob.php b/lib/public/BackgroundJob/TimedJob.php index 2cc9ea8e293..cc2d42dcd32 100644 --- a/lib/public/BackgroundJob/TimedJob.php +++ b/lib/public/BackgroundJob/TimedJob.php @@ -38,6 +38,8 @@ use OCP\ILogger; abstract class TimedJob extends Job { /** @var int */ protected $interval = 0; + /** @var int */ + protected $timeSensitivity = IJob::TIME_SENSITIVE; /** * set the interval for the job @@ -51,6 +53,36 @@ abstract class TimedJob extends Job { } /** + * Whether the background job is time sensitive and needs to run soon after + * the scheduled interval, of if it is okay to be delayed until a later time. + * + * @return bool + * @since 22.2.6 + */ + public function isTimeSensitive(): bool { + return $this->timeSensitivity === IJob::TIME_SENSITIVE; + } + + /** + * If your background job is not time sensitive (sending instant email + * notifications, etc.) it would be nice to set it to IJob::TIME_INSENSITIVE + * This way the execution can be delayed during high usage times. + * + * @param int $sensitivity + * @psalm-param IJob::TIME_* $sensitivity + * @return void + * @since 22.2.6 + */ + public function setTimeSensitivity(int $sensitivity): void { + if ($sensitivity !== IJob::TIME_SENSITIVE && + $sensitivity !== IJob::TIME_INSENSITIVE) { + throw new \InvalidArgumentException('Invalid sensitivity'); + } + + $this->timeSensitivity = $sensitivity; + } + + /** * run the job if the last run is is more than the interval ago * * @param JobList $jobList diff --git a/tests/lib/BackgroundJob/DummyJobList.php b/tests/lib/BackgroundJob/DummyJobList.php index 452b9bb98ed..1c899f35e2a 100644 --- a/tests/lib/BackgroundJob/DummyJobList.php +++ b/tests/lib/BackgroundJob/DummyJobList.php @@ -75,9 +75,10 @@ class DummyJobList extends \OC\BackgroundJob\JobList { /** * get the next job in the list * + * @param bool $onlyTimeSensitive * @return IJob|null */ - public function getNext() { + public function getNext(bool $onlyTimeSensitive = true): ?IJob { if (count($this->jobs) > 0) { if ($this->last < (count($this->jobs) - 1)) { $i = $this->last + 1; |