Disable Minutely Recurrence for Automations (#781)

* Disable automation recurrence at minute level frequency

* Set a max lifetime for django's connections to the db

* Disable any automation that has a non-numeric first digit (i.e., recuring on the minute level)

* Re-enable automations

---------

Co-authored-by: sabaimran <narmiabas@gmail.com>
This commit is contained in:
Debanjum 2024-05-31 12:50:19 +05:30 committed by GitHub
parent 5dca48d9fc
commit 3090b84252
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 41 additions and 57 deletions

View file

@ -2,16 +2,6 @@
{% block content %}
<div class="page">
<div class="section">
<div id="maintenance-notice">
<div class="urgent-notice">
<p>
Hold up! Our Automations are taking a well-deserved break for some maintenance magic. They'll be back in action soon, better than ever. Thanks for your patience!
</p>
<div>
<a class="maintenance-action" href="/">Back to Chat</a>
</div>
</div>
</div>
<h2 class="section-title">
<img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
<span class="card-title-text">Automate (Preview)</span>
@ -218,32 +208,6 @@
grid-gap: 8px;
}
div#maintenance-notice {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
background-image: linear-gradient(to bottom, rgba(250, 130, 130, 0.8), rgba(255, 165, 0, 0.8));
}
a.maintenance-action{
color: white;
text-decoration: none;
}
div.urgent-notice p,
div.urgent-notice a {
font-size: 24px;
font-weight: bold;
padding: 20px;
text-align: left;
}
div.subject-wrapper p {
margin: 0;
}
@ -273,7 +237,7 @@
}
</style>
<!-- <script>
<script>
function deleteAutomation(automationId) {
const AutomationList = document.getElementById("automations");
fetch(`/api/automation?automation_id=${automationId}`, {
@ -336,7 +300,7 @@
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
return response;
})
.then(automations => {
notificationEl.style.display = 'block';
@ -875,9 +839,9 @@
}, 2000);
})
.catch(error => {
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automations.`;
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automation.`;
notificationEl.style.display = "block";
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automations`;
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automation`;
setTimeout(function() {
saveButtonEl.textContent = `${actOn}e`;
}, 2000);
@ -915,5 +879,5 @@
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
setupScheduleViewListener("* * * * *", placeholderId);
})
</script> -->
</script>
{% endblock %}

View file

@ -134,17 +134,17 @@ def run(should_start_server=True):
poll_task_scheduler()
# Setup Background Scheduler
# from django_apscheduler.jobstores import DjangoJobStore
from django_apscheduler.jobstores import DjangoJobStore
# state.scheduler = BackgroundScheduler(
# {
# "apscheduler.timezone": "UTC",
# "apscheduler.job_defaults.misfire_grace_time": "60", # Useful to run scheduled jobs even when worker delayed because it was busy or down
# "apscheduler.job_defaults.coalesce": "true", # Combine multiple jobs into one if they are scheduled at the same time
# }
# )
# state.scheduler.add_jobstore(DjangoJobStore(), "default")
# state.scheduler.start()
state.scheduler = BackgroundScheduler(
{
"apscheduler.timezone": "UTC",
"apscheduler.job_defaults.misfire_grace_time": "60", # Useful to run scheduled jobs even when worker delayed because it was busy or down
"apscheduler.job_defaults.coalesce": "true", # Combine multiple jobs into one if they are scheduled at the same time
}
)
state.scheduler.add_jobstore(DjangoJobStore(), "default")
state.scheduler.start()
# Start Server
configure_routes(app)

View file

@ -6,6 +6,7 @@ import os
import threading
import time
import uuid
from random import random
from typing import Any, Callable, List, Optional, Union
import cron_descriptor
@ -467,8 +468,15 @@ async def post_automation(
crontime = " ".join(crontime.split(" ")[:5])
# Convert crontime to standard unix crontime
crontime = crontime.replace("?", "*")
if crontime == "* * * * *":
return Response(content="Invalid crontime. Please create a more specific schedule.", status_code=400)
# Disallow minute level automation recurrence
minute_value = crontime.split(" ")[0]
if not minute_value.isdigit():
return Response(
content="Recurrence of every X minutes is unsupported. Please create a less frequent schedule.",
status_code=400,
)
subject = await acreate_title_from_query(q)
# Create new Conversation Session associated with this new task
@ -560,6 +568,14 @@ def edit_job(
# Convert crontime to standard unix crontime
crontime = crontime.replace("?", "*")
# Disallow minute level automation recurrence
minute_value = crontime.split(" ")[0]
if not minute_value.isdigit():
return Response(
content="Recurrence of every X minutes is unsupported. Please create a less frequent schedule.",
status_code=400,
)
# Construct updated automation metadata
automation_metadata = json.loads(automation.name)
automation_metadata["scheduling_request"] = q

View file

@ -4,10 +4,12 @@ import hashlib
import io
import json
import logging
import math
import re
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
from functools import partial
from random import random
from typing import (
Annotated,
Any,
@ -1014,8 +1016,6 @@ def scheduled_chat(
async def create_automation(q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}):
crontime, query_to_run, subject = await schedule_query(q, meta_log)
if crontime == "* * * * *":
raise HTTPException(status_code=400, detail="Cannot run jobs constantly. Please provide a valid crontime.")
job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url)
return job, crontime, query_to_run, subject
@ -1029,9 +1029,13 @@ async def schedule_automation(
user: KhojUser,
calling_url: URL,
):
# Disable minute level automation recurrence
minute_value = crontime.split(" ")[0]
if not minute_value.isdigit():
# Run automation at some random minute (to distribute request load) instead of running every X minutes
crontime = " ".join([str(math.floor(random() * 60))] + crontime.split(" ")[1:])
user_timezone = pytz.timezone(timezone)
if crontime == "* * * * *":
raise HTTPException(status_code=400, detail="Cannot run jobs constantly. Please provide a valid crontime.")
trigger = CronTrigger.from_crontab(crontime, user_timezone)
trigger.jitter = 60
# Generate id and metadata used by task scheduler and process locks for the task runs