From cd40995ba809ea43d04769e68170f967a2720dd5 Mon Sep 17 00:00:00 2001 From: Josh Stark Date: Sun, 22 Dec 2019 17:37:56 +0000 Subject: [PATCH] Schedules - Added runNow API endpoint - Ensure no duplication allowed in async queue. --- .../DefaultAuthenticationDelegate.java | 2 +- .../fleet/v2/cache/BasicItemCache.java | 25 ++++++ .../v2/client/docker/queue/TaskQueue.java | 6 ++ .../fleet/v2/service/ScheduleService.java | 80 +++++++++++++++---- .../fleet/v2/service/UserService.java | 27 +++++++ .../fleet/v2/thread/AbstractAppTask.java | 22 ++++- .../v2/types/api/ApiScheduleWrapper.java | 31 +++++++ .../fleet/v2/web/WebRouteController.java | 4 + .../v2/web/routes/InternalApiController.java | 17 ++++ src/main/resources/static/assets/js/admin.js | 38 ++++++++- .../resources/views/pages/admin/schedules.ftl | 4 +- 11 files changed, 234 insertions(+), 22 deletions(-) create mode 100644 src/main/java/io/linuxserver/fleet/v2/cache/BasicItemCache.java create mode 100644 src/main/java/io/linuxserver/fleet/v2/service/UserService.java create mode 100644 src/main/java/io/linuxserver/fleet/v2/types/api/ApiScheduleWrapper.java diff --git a/src/main/java/io/linuxserver/fleet/delegate/DefaultAuthenticationDelegate.java b/src/main/java/io/linuxserver/fleet/delegate/DefaultAuthenticationDelegate.java index 775dca6..1e73540 100644 --- a/src/main/java/io/linuxserver/fleet/delegate/DefaultAuthenticationDelegate.java +++ b/src/main/java/io/linuxserver/fleet/delegate/DefaultAuthenticationDelegate.java @@ -25,7 +25,7 @@ public class DefaultAuthenticationDelegate implements AuthenticationDelegate { private final UserAuthenticator authenticator; - public DefaultAuthenticationDelegate(UserAuthenticator authenticator) { + public DefaultAuthenticationDelegate(final UserAuthenticator authenticator) { this.authenticator = authenticator; } diff --git a/src/main/java/io/linuxserver/fleet/v2/cache/BasicItemCache.java b/src/main/java/io/linuxserver/fleet/v2/cache/BasicItemCache.java new file mode 100644 index 0000000..3983dfa --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/v2/cache/BasicItemCache.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019 LinuxServer.io + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.linuxserver.fleet.v2.cache; + +import io.linuxserver.fleet.v2.key.HasKey; +import io.linuxserver.fleet.v2.key.Key; + +public class BasicItemCache> extends AbstractItemCache { + // Default implementation +} diff --git a/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/TaskQueue.java b/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/TaskQueue.java index 0855792..4a98b82 100644 --- a/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/TaskQueue.java +++ b/src/main/java/io/linuxserver/fleet/v2/client/docker/queue/TaskQueue.java @@ -40,6 +40,12 @@ public class TaskQueue> { public final boolean submitTask(final TASK task) { LOGGER.info("Task submitted: {}", task); + if (activeTaskQueue.contains(task)) { + + LOGGER.warn("Task {} is already queued so will not duplicate the request.", task); + return false; + } + return activeTaskQueue.add(task); } diff --git a/src/main/java/io/linuxserver/fleet/v2/service/ScheduleService.java b/src/main/java/io/linuxserver/fleet/v2/service/ScheduleService.java index 818e18c..a68cd32 100644 --- a/src/main/java/io/linuxserver/fleet/v2/service/ScheduleService.java +++ b/src/main/java/io/linuxserver/fleet/v2/service/ScheduleService.java @@ -18,8 +18,9 @@ package io.linuxserver.fleet.v2.service; import io.linuxserver.fleet.core.FleetAppController; -import io.linuxserver.fleet.v2.cache.ScheduleCache; +import io.linuxserver.fleet.v2.cache.BasicItemCache; import io.linuxserver.fleet.v2.db.ScheduleDAO; +import io.linuxserver.fleet.v2.key.AbstractHasKey; import io.linuxserver.fleet.v2.key.ScheduleKey; import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; import io.linuxserver.fleet.v2.thread.schedule.ScheduleSpec; @@ -27,24 +28,26 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Constructor; -import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.stream.Collectors; public class ScheduleService extends AbstractAppService { private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleService.class); + private final BasicItemCache scheduleCache; + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); - private final ScheduleCache scheduleCache; private final ScheduleDAO scheduleDAO; public ScheduleService(final FleetAppController controller, final ScheduleDAO scheduleDAO) { super(controller); - this.scheduleCache = new ScheduleCache(); + this.scheduleCache = new BasicItemCache<>(); this.scheduleDAO = scheduleDAO; } @@ -53,29 +56,42 @@ public class ScheduleService extends AbstractAppService { final Set specs = scheduleDAO.fetchScheduleSpecs(); for (ScheduleSpec spec : specs) { - final AppSchedule schedule = loadSchedule(spec); + try { - LOGGER.info("Schedule loaded: {}", schedule); - executorService.scheduleAtFixedRate(schedule, - 0, - schedule.getInterval().getTimeDuration(), - schedule.getInterval().getTimeUnitAsTimeUnit()); + final AppSchedule schedule = loadSchedule(spec); + LOGGER.info("Schedule loaded: {}", schedule); - scheduleCache.addItem(schedule); + loadOneSchedule(schedule); + + } catch (Exception e) { + LOGGER.error("Unable to load schedule", e); + } } } - public final void forceRun(final ScheduleKey scheduleKey) { + public final AppSchedule forceRun(final ScheduleKey scheduleKey) { + + if (scheduleCache.isItemCached(scheduleKey)) { + + final ScheduleWrapper wrapper = scheduleCache.findItem(scheduleKey); + + LOGGER.info("Cancelling current run of schedule {}", wrapper.getName()); + wrapper.getFuture().cancel(false); + + LOGGER.info("Triggering re-run of schedule {}", wrapper.getName()); + loadOneSchedule(wrapper.getSchedule()); + + return wrapper.getSchedule(); - if (scheduleCache.isScheduleRunning(scheduleKey)) { - scheduleCache.findItem(scheduleKey).executeSchedule(); } else { + + LOGGER.warn("Did not find cached schedule with key {}", scheduleKey); throw new IllegalArgumentException("No schedule found with key " + scheduleKey); } } public final List getLoadedSchedules() { - return new ArrayList<>(scheduleCache.getAllItems()); + return scheduleCache.getAllItems().stream().map(ScheduleWrapper::getSchedule).collect(Collectors.toList()); } private AppSchedule loadSchedule(final ScheduleSpec spec) { @@ -92,4 +108,38 @@ public class ScheduleService extends AbstractAppService { throw new RuntimeException(e); } } + + private void loadOneSchedule(AppSchedule schedule) { + + final ScheduledFuture future = executorService.scheduleAtFixedRate(schedule, + 0, + schedule.getInterval().getTimeDuration(), + schedule.getInterval().getTimeUnitAsTimeUnit()); + + scheduleCache.addItem(new ScheduleWrapper(schedule, future)); + } + + public static class ScheduleWrapper extends AbstractHasKey { + + private final ScheduledFuture future; + private final AppSchedule schedule; + + public ScheduleWrapper(final AppSchedule schedule, final ScheduledFuture future) { + super(schedule.getKey()); + this.future = future; + this.schedule = schedule; + } + + public final ScheduledFuture getFuture() { + return future; + } + + public final AppSchedule getSchedule() { + return schedule; + } + + public final String getName() { + return getSchedule().getName(); + } + } } diff --git a/src/main/java/io/linuxserver/fleet/v2/service/UserService.java b/src/main/java/io/linuxserver/fleet/v2/service/UserService.java new file mode 100644 index 0000000..e4c0518 --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/v2/service/UserService.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 LinuxServer.io + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.linuxserver.fleet.v2.service; + +import io.linuxserver.fleet.core.FleetAppController; + +public class UserService extends AbstractAppService { + + public UserService(final FleetAppController controller) { + super(controller); + } +} diff --git a/src/main/java/io/linuxserver/fleet/v2/thread/AbstractAppTask.java b/src/main/java/io/linuxserver/fleet/v2/thread/AbstractAppTask.java index 58fafb5..5bd4f45 100644 --- a/src/main/java/io/linuxserver/fleet/v2/thread/AbstractAppTask.java +++ b/src/main/java/io/linuxserver/fleet/v2/thread/AbstractAppTask.java @@ -28,6 +28,11 @@ public abstract class AbstractAppTask) o).name.equals(name); } } diff --git a/src/main/java/io/linuxserver/fleet/v2/types/api/ApiScheduleWrapper.java b/src/main/java/io/linuxserver/fleet/v2/types/api/ApiScheduleWrapper.java new file mode 100644 index 0000000..55b574f --- /dev/null +++ b/src/main/java/io/linuxserver/fleet/v2/types/api/ApiScheduleWrapper.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 LinuxServer.io + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.linuxserver.fleet.v2.types.api; + +import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; + +public class ApiScheduleWrapper extends AbstractApiWrapper { + + public ApiScheduleWrapper(final AppSchedule schedule) { + super(schedule); + } + + public final String getName() { + return getOriginalObject().getName(); + } +} diff --git a/src/main/java/io/linuxserver/fleet/v2/web/WebRouteController.java b/src/main/java/io/linuxserver/fleet/v2/web/WebRouteController.java index 7b88522..ceafd20 100644 --- a/src/main/java/io/linuxserver/fleet/v2/web/WebRouteController.java +++ b/src/main/java/io/linuxserver/fleet/v2/web/WebRouteController.java @@ -68,6 +68,10 @@ public class WebRouteController { put(apiController::updateRepository, roles(FleetRole.AdminOnly)); post(apiController::addNewRepository, roles(FleetRole.Anyone)); }); + + path(Locations.Internal.Schedule, () -> { + put(apiController::runSchedule, roles(FleetRole.Anyone)); + }); }); }); } diff --git a/src/main/java/io/linuxserver/fleet/v2/web/routes/InternalApiController.java b/src/main/java/io/linuxserver/fleet/v2/web/routes/InternalApiController.java index c3d8d8c..ba7a9fb 100644 --- a/src/main/java/io/linuxserver/fleet/v2/web/routes/InternalApiController.java +++ b/src/main/java/io/linuxserver/fleet/v2/web/routes/InternalApiController.java @@ -19,9 +19,12 @@ package io.linuxserver.fleet.v2.web.routes; import io.javalin.http.Context; import io.linuxserver.fleet.core.FleetAppController; +import io.linuxserver.fleet.v2.key.ScheduleKey; import io.linuxserver.fleet.v2.service.AbstractAppService; +import io.linuxserver.fleet.v2.thread.schedule.AppSchedule; import io.linuxserver.fleet.v2.types.Repository; import io.linuxserver.fleet.v2.types.api.ApiRepositoryWrapper; +import io.linuxserver.fleet.v2.types.api.ApiScheduleWrapper; import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest; import io.linuxserver.fleet.v2.web.ApiException; import io.linuxserver.fleet.v2.web.request.NewRepositoryRequest; @@ -52,4 +55,18 @@ public class InternalApiController extends AbstractAppService { throw new ApiException(e.getMessage(), e); } } + + public void runSchedule(final Context ctx) { + + try { + + final Integer scheduleKey = ctx.formParam("scheduleKey", Integer.class).get(); + final AppSchedule schedule = getController().getScheduleService().forceRun(new ScheduleKey(scheduleKey)); + + ctx.json(new ApiScheduleWrapper(schedule)); + + } catch (IllegalArgumentException e) { + throw new ApiException(e.getMessage(), e); + } + } } diff --git a/src/main/resources/static/assets/js/admin.js b/src/main/resources/static/assets/js/admin.js index f8ad098..57e19cc 100644 --- a/src/main/resources/static/assets/js/admin.js +++ b/src/main/resources/static/assets/js/admin.js @@ -17,6 +17,34 @@ var adminManager = (function($) { + var toggleButtonLoadingState = function(button) { + button.prop('disabled', !button.prop('disabled')).toggleClass('is-loading'); + }; + + var runSchedule = function(trigger) { + + var scheduleKey = trigger.data('schedule-key'); + + var request = { + + url: '/internalApi/schedule', + method: 'put', + data: { + 'scheduleKey': scheduleKey + } + }; + + toggleButtonLoadingState(trigger); + ajaxManager.call(request, function() { + + notificationManager.makeNotification('Schedule run submitted successfully. The "Last Run" value will be updated once the task has completed.', 'success'); + toggleButtonLoadingState(trigger); + + }, function() { + toggleButtonLoadingState(trigger); + }); + }; + var addRepository = function(repositoryName) { var trimmedName = $.trim(repositoryName); @@ -37,7 +65,7 @@ var adminManager = (function($) { ajaxManager.call(request, function() { window.location.reload(); }, function() { - $('#SubmitNewRepository').prop('disabled', false).removeClass('is-loading'); + toggleButtonLoadingState($('#SubmitNewRepository')); $('#NewRepositoryName').val(''); }); } @@ -50,9 +78,13 @@ var adminManager = (function($) { var $button = $(this); var repositoryName = $('#NewRepositoryName').val(); - $button.prop('disabled', true).addClass('is-loading'); + toggleButtonLoadingState($button); addRepository(repositoryName); - }) + }); + + $('.force-schedule-run').on('click', function() { + runSchedule($(this)); + }); }; return { diff --git a/src/main/resources/views/pages/admin/schedules.ftl b/src/main/resources/views/pages/admin/schedules.ftl index cbdfc2c..fae5fd1 100644 --- a/src/main/resources/views/pages/admin/schedules.ftl +++ b/src/main/resources/views/pages/admin/schedules.ftl @@ -39,7 +39,7 @@ Name Last Run - Next Run + Next Run (Est.) Interval @@ -53,7 +53,7 @@ ${schedule.interval.timeDuration} ${schedule.interval.timeUnitAsTimeUnit?lower_case} <@button.buttons isGrouped=true isRightAligned=true> - <@button.button extraClasses="force-schedule-run" colour="normal-colour" size="small" title="Run this schedule now"> + <@button.button extraClasses="force-schedule-run" colour="normal-colour" size="small" title="Run this schedule now" extraAttributes='data-schedule-key="${schedule.key}"'>