mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 13:25:11 +00:00
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:
@@ -2,16 +2,6 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="section">
|
<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">
|
<h2 class="section-title">
|
||||||
<img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
|
<img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
|
||||||
<span class="card-title-text">Automate (Preview)</span>
|
<span class="card-title-text">Automate (Preview)</span>
|
||||||
@@ -218,32 +208,6 @@
|
|||||||
grid-gap: 8px;
|
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 {
|
div.subject-wrapper p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@@ -273,7 +237,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<!-- <script>
|
<script>
|
||||||
function deleteAutomation(automationId) {
|
function deleteAutomation(automationId) {
|
||||||
const AutomationList = document.getElementById("automations");
|
const AutomationList = document.getElementById("automations");
|
||||||
fetch(`/api/automation?automation_id=${automationId}`, {
|
fetch(`/api/automation?automation_id=${automationId}`, {
|
||||||
@@ -336,7 +300,7 @@
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Network response was not ok');
|
throw new Error('Network response was not ok');
|
||||||
}
|
}
|
||||||
return response.json();
|
return response;
|
||||||
})
|
})
|
||||||
.then(automations => {
|
.then(automations => {
|
||||||
notificationEl.style.display = 'block';
|
notificationEl.style.display = 'block';
|
||||||
@@ -875,9 +839,9 @@
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automations.`;
|
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automation.`;
|
||||||
notificationEl.style.display = "block";
|
notificationEl.style.display = "block";
|
||||||
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automations`;
|
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automation`;
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
saveButtonEl.textContent = `${actOn}e`;
|
saveButtonEl.textContent = `${actOn}e`;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -915,5 +879,5 @@
|
|||||||
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
|
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
|
||||||
setupScheduleViewListener("* * * * *", placeholderId);
|
setupScheduleViewListener("* * * * *", placeholderId);
|
||||||
})
|
})
|
||||||
</script> -->
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -134,17 +134,17 @@ def run(should_start_server=True):
|
|||||||
poll_task_scheduler()
|
poll_task_scheduler()
|
||||||
|
|
||||||
# Setup Background Scheduler
|
# Setup Background Scheduler
|
||||||
# from django_apscheduler.jobstores import DjangoJobStore
|
from django_apscheduler.jobstores import DjangoJobStore
|
||||||
|
|
||||||
# state.scheduler = BackgroundScheduler(
|
state.scheduler = BackgroundScheduler(
|
||||||
# {
|
{
|
||||||
# "apscheduler.timezone": "UTC",
|
"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.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
|
"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.add_jobstore(DjangoJobStore(), "default")
|
||||||
# state.scheduler.start()
|
state.scheduler.start()
|
||||||
|
|
||||||
# Start Server
|
# Start Server
|
||||||
configure_routes(app)
|
configure_routes(app)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import os
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from random import random
|
||||||
from typing import Any, Callable, List, Optional, Union
|
from typing import Any, Callable, List, Optional, Union
|
||||||
|
|
||||||
import cron_descriptor
|
import cron_descriptor
|
||||||
@@ -467,8 +468,15 @@ async def post_automation(
|
|||||||
crontime = " ".join(crontime.split(" ")[:5])
|
crontime = " ".join(crontime.split(" ")[:5])
|
||||||
# Convert crontime to standard unix crontime
|
# Convert crontime to standard unix crontime
|
||||||
crontime = crontime.replace("?", "*")
|
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)
|
subject = await acreate_title_from_query(q)
|
||||||
|
|
||||||
# Create new Conversation Session associated with this new task
|
# Create new Conversation Session associated with this new task
|
||||||
@@ -560,6 +568,14 @@ def edit_job(
|
|||||||
# Convert crontime to standard unix crontime
|
# Convert crontime to standard unix crontime
|
||||||
crontime = crontime.replace("?", "*")
|
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
|
# Construct updated automation metadata
|
||||||
automation_metadata = json.loads(automation.name)
|
automation_metadata = json.loads(automation.name)
|
||||||
automation_metadata["scheduling_request"] = q
|
automation_metadata["scheduling_request"] = q
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import hashlib
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from random import random
|
||||||
from typing import (
|
from typing import (
|
||||||
Annotated,
|
Annotated,
|
||||||
Any,
|
Any,
|
||||||
@@ -1014,8 +1016,6 @@ def scheduled_chat(
|
|||||||
|
|
||||||
async def create_automation(q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}):
|
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)
|
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)
|
job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url)
|
||||||
return job, crontime, query_to_run, subject
|
return job, crontime, query_to_run, subject
|
||||||
|
|
||||||
@@ -1029,9 +1029,13 @@ async def schedule_automation(
|
|||||||
user: KhojUser,
|
user: KhojUser,
|
||||||
calling_url: URL,
|
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)
|
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 = CronTrigger.from_crontab(crontime, user_timezone)
|
||||||
trigger.jitter = 60
|
trigger.jitter = 60
|
||||||
# Generate id and metadata used by task scheduler and process locks for the task runs
|
# Generate id and metadata used by task scheduler and process locks for the task runs
|
||||||
|
|||||||
Reference in New Issue
Block a user