Copilot commented on code in PR #13743:
URL: https://github.com/apache/cloudstack/pull/13743#discussion_r3682943831


##########
server/src/main/java/com/cloud/storage/snapshot/SnapshotSchedulerImpl.java:
##########
@@ -297,6 +354,155 @@ protected void scheduleSnapshots() {
         }
     }
 
+    /**
+     * Handles a synchronous failure to dispatch the CreateSnapshotCmd (e.g. 
an allocation error) for a recurring
+     * snapshot. Without this, the schedule's {@code scheduledTimestamp} is 
never advanced, so it gets retried on
+     * every poll (every {@code snapshot.poll.interval} seconds) forever. 
Instead: log a failure event keyed by the
+     * volume (so consecutive failures can be counted from event history), and 
either back off by the configured
+     * retry interval, or - once the configured maximum consecutive failures 
is reached - give up until the next
+     * regularly scheduled run and raise a WARN notification event.
+     */
+    protected void handleFailedSnapshotDispatch(final SnapshotScheduleVO 
snapshotToBeExecuted, final VolumeVO volume,
+            final SnapshotScheduleVO lockedSchedule, final Long eventId, final 
Exception cause) {
+        final long volumeId = volume.getId();
+        final Account account = _acctDao.findById(volume.getAccountId());
+        final int maxFailures = 
getScopedConfigValue(SnapshotManager.SnapshotRecurringMaxFailures, volume, 
account);
+        final int retryInterval = 
getScopedConfigValue(SnapshotManager.SnapshotRecurringRetryInterval, volume, 
account);
+        final int totalFailures = countConsecutiveFailedAttempts(volumeId, 
maxFailures) + 1;
+
+        final String failureMessage = String.format("Failed to create 
scheduled snapshot for volume [%s]: %s", volume, cause.getMessage());
+        if (eventId != null) {
+            ActionEventUtils.onCompletedActionEvent(User.UID_SYSTEM, 
volume.getAccountId(), EventVO.LEVEL_ERROR,
+                    EventTypes.EVENT_SNAPSHOT_CREATE, failureMessage, 
volumeId, ApiCommandResourceType.Volume.toString(), eventId);
+        } else {
+            ActionEventUtils.onCreatedActionEvent(User.UID_SYSTEM, 
volume.getAccountId(), EventVO.LEVEL_ERROR,
+                    EventTypes.EVENT_SNAPSHOT_CREATE, true, failureMessage, 
volumeId, ApiCommandResourceType.Volume.toString());
+        }
+
+        if (maxFailures > 0 && totalFailures >= maxFailures) {
+            final Date nextRegularRun = 
getNextScheduledTime(snapshotToBeExecuted.getPolicyId(), _currentTimestamp);
+            lockedSchedule.setScheduledTimestamp(nextRegularRun);
+            logger.warn("Snapshot schedule [{}] for volume [{}] has failed 
[{}] consecutive times; it will not be retried until its next regularly 
scheduled run at [{}].",
+                    snapshotToBeExecuted, volume, totalFailures, 
nextRegularRun);
+            ActionEventUtils.onCreatedActionEvent(User.UID_SYSTEM, 
volume.getAccountId(), EventVO.LEVEL_WARN, 
EventTypes.EVENT_SNAPSHOT_RECURRING_FAILURE_LIMIT_REACHED, true,
+                    String.format("Recurring snapshot for volume [%s] has 
failed %d consecutive times and will not be retried until its next regularly 
scheduled run.", volume, totalFailures),
+                    volumeId, ApiCommandResourceType.Volume.toString());
+        } else {
+            final Date nextRetry = new Date(_currentTimestamp.getTime() + 
retryInterval * 1000L);
+            lockedSchedule.setScheduledTimestamp(nextRetry);
+            logger.debug("Snapshot schedule [{}] for volume [{}] failed [{}] 
time(s); retrying at [{}].",
+                    snapshotToBeExecuted, volume, totalFailures, nextRetry);
+        }
+        _snapshotScheduleDao.update(lockedSchedule.getId(), lockedSchedule);
+    }
+
+    /**
+     * Counts how many of the most recent {@code EVENT_SNAPSHOT_CREATE} events 
logged for this volume are
+     * consecutive failures (level ERROR), starting from the most recent event 
and stopping at the first
+     * non-failure (or absent) event. This derives the "number of failed 
attempts" from event history instead of a
+     * dedicated counter column.
+     */
+    protected int countConsecutiveFailedAttempts(final long volumeId, final 
int limit) {
+        if (limit <= 0) {
+            return 0;
+        }
+        final List<EventVO> recentEvents = 
_eventDao.listLatestEventsByResource(volumeId, 
ApiCommandResourceType.Volume.toString(),
+                EventTypes.EVENT_SNAPSHOT_CREATE, limit);
+        int count = 0;
+        for (final EventVO event : recentEvents) {
+            if (!EventVO.LEVEL_ERROR.equals(event.getLevel())) {
+                break;
+            }
+            count++;
+        }
+        return count;
+    }
+
+    /**
+     * Resolves a config value in order of most to least specific scope: 
account, domain, zone, then global. A
+     * {@link ConfigKey} can only walk a single scope-parent chain 
automatically (Account-&gt;Domain-&gt;Global, or
+     * Zone-&gt;Global), so the four scopes are resolved manually here.
+     */
+    protected <T> T getScopedConfigValue(final ConfigKey<T> key, final 
VolumeVO volume, final Account account) {
+        T value = key.valueInScope(ConfigKey.Scope.Account, 
volume.getAccountId(), true);
+        if (value == null && account != null) {
+            value = key.valueInScope(ConfigKey.Scope.Domain, 
account.getDomainId(), true);
+        }
+        if (value == null) {
+            value = key.valueInScope(ConfigKey.Scope.Zone, 
volume.getDataCenterId(), true);
+        }
+        if (value == null) {
+            value = key.value();
+        }
+        return value;
+    }

Review Comment:
   The scoped fallback logic is likely bypassed by calling `valueInScope(..., 
true)` for Account/Domain/Zone. If the boolean enables parent/default fallback, 
`value` will rarely (or never) be null, so Domain/Zone/global precedence won’t 
work as intended (e.g., a Zone override could be ignored because Account-scope 
lookup already returned a default). Use a mode that returns null when not 
explicitly set for that scope (or otherwise detect ‘unset’) for 
Account/Domain/Zone, and only fall back to `key.value()` at the end.



##########
server/src/main/java/com/cloud/storage/snapshot/SnapshotSchedulerImpl.java:
##########
@@ -206,6 +220,40 @@ protected void 
scheduleNextSnapshotJobIfNecessary(SnapshotScheduleVO snapshotSch
         scheduleNextSnapshotJob(snapshotSchedule);
     }
 
+    /**
+     * Logs an event for the outcome of a recurring snapshot job (keyed by the 
volume, since a fresh snapshot entity
+     * ID is minted on every attempt) so that consecutive failures can be 
counted from event history, and raises a
+     * WARN notification once {@link 
SnapshotManager#SnapshotRecurringMaxFailures} consecutive failures are reached.
+     */
+    protected void recordSnapshotAttemptOutcome(final SnapshotScheduleVO 
snapshotSchedule, final boolean succeeded, final String failureResult) {
+        final VolumeVO volume = 
_volsDao.findByIdIncludingRemoved(snapshotSchedule.getVolumeId());
+        if (volume == null) {
+            return;
+        }
+
+        if (succeeded) {
+            ActionEventUtils.onCreatedActionEvent(User.UID_SYSTEM, 
volume.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_SNAPSHOT_CREATE, 
true,
+                    String.format("Scheduled snapshot creation job for volume 
[%s] succeeded.", volume),
+                    volume.getId(), ApiCommandResourceType.Volume.toString());
+            return;
+        }
+
+        final Account account = _acctDao.findById(volume.getAccountId());
+        final int maxFailures = 
getScopedConfigValue(SnapshotManager.SnapshotRecurringMaxFailures, volume, 
account);
+        final int totalFailures = 
countConsecutiveFailedAttempts(volume.getId(), maxFailures) + 1;
+
+        ActionEventUtils.onCreatedActionEvent(User.UID_SYSTEM, 
volume.getAccountId(), EventVO.LEVEL_ERROR, EventTypes.EVENT_SNAPSHOT_CREATE, 
true,
+                String.format("Scheduled snapshot creation job for volume [%s] 
failed: %s", volume, failureResult),
+                volume.getId(), ApiCommandResourceType.Volume.toString());
+
+        if (maxFailures > 0 && totalFailures >= maxFailures) {
+            logger.warn("Snapshot schedule [{}] for volume [{}] has failed 
[{}] consecutive times.", snapshotSchedule, volume, totalFailures);
+            ActionEventUtils.onCreatedActionEvent(User.UID_SYSTEM, 
volume.getAccountId(), EventVO.LEVEL_WARN, EventTypes.EVENT_SNAPSHOT_CREATE, 
true,
+                    String.format("Recurring snapshot for volume [%s] has 
failed %d consecutive times.", volume, totalFailures),
+                    volume.getId(), ApiCommandResourceType.Volume.toString());
+        }

Review Comment:
   The WARN notification is logged with `EventTypes.EVENT_SNAPSHOT_CREATE`, 
which means it becomes part of the same event stream that 
`countConsecutiveFailedAttempts(..., EVENT_SNAPSHOT_CREATE, ...)` scans. Since 
it’s not LEVEL_ERROR, it will stop the scan and effectively reset the 
consecutive-failure count on the next attempt. Log this WARN with a distinct 
event type (e.g. `EVENT_SNAPSHOT_RECURRING_FAILURE_LIMIT_REACHED`, consistent 
with `handleFailedSnapshotDispatch`) so failure counting remains accurate.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to