We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Cron server script runs twice
<!--
Welcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to Frappe
    - For questions and general support, use https://stackoverflow.com/questions/tagged/frappe
    - For documentation issues, refer to https://frappeframework.com/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
   maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->

## Description of the issue
Created a cron server script with "13 11 17 10 tue" cron format.
So it should run at 11:13 17/10/2023.
But it runs once after save and runs again at correct time.

## Context information (for bug reports)
![image](https://github.com/frappe/frappe/assets/710051/1531ef89-3fc0-4b0a-a7af-2fe77d626e11)

![image](https://github.com/frappe/frappe/assets/710051/21fd224d-e841-4598-967f-b79c9895009b)


**Output of `bench version`**
```
Bench 5.17.2
Frappe 14.52.0
```

## Steps to reproduce the issue

1.Create a server script as "Scheduler Event", "Event Frequency" as "Cron"
2.Make cron format as ```59 23 * * *```
3.Set script as
```frappe.log_error("Cron Test", "Test")```

### Observed result
Watch the Error Log. 
It creates a "Cron Test" log once and at the midnight.

### Expected result
It should only create the error log at the midnight.
### Stacktrace / full error message

```
None
```

## Additional information

OS version / distribution, `Frappe` install method, etc.
Manual install


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `frappe/core/doctype/scheduled_job_type/scheduled_job_type.py`
Content:
```
1 # Copyright (c) 2021, Frappe Technologies and contributors
2 # License: MIT. See LICENSE
3 
4 import json
5 from datetime import datetime
6 
7 import click
8 from croniter import croniter
9 
10 import frappe
11 from frappe.model.document import Document
12 from frappe.utils import get_datetime, now_datetime
13 from frappe.utils.background_jobs import enqueue, is_job_enqueued
14 
15 
16 class ScheduledJobType(Document):
17 	# begin: auto-generated types
18 	# This code is auto-generated. Do not modify anything in this block.
19 
20 	from typing import TYPE_CHECKING
21 
22 	if TYPE_CHECKING:
23 		from frappe.types import DF
24 
25 		create_log: DF.Check
26 		cron_format: DF.Data | None
27 		frequency: DF.Literal[
28 			"All",
29 			"Hourly",
30 			"Hourly Long",
31 			"Daily",
32 			"Daily Long",
33 			"Weekly",
34 			"Weekly Long",
35 			"Monthly",
36 			"Monthly Long",
37 			"Cron",
38 			"Yearly",
39 			"Annual",
40 		]
41 		last_execution: DF.Datetime | None
42 		method: DF.Data
43 		next_execution: DF.Datetime | None
44 		server_script: DF.Link | None
45 		stopped: DF.Check
46 	# end: auto-generated types
47 	def autoname(self):
48 		self.name = ".".join(self.method.split(".")[-2:])
49 
50 	def validate(self):
51 		if self.frequency != "All":
52 			# force logging for all events other than continuous ones (ALL)
53 			self.create_log = 1
54 
55 	def enqueue(self, force=False) -> bool:
56 		# enqueue event if last execution is done
57 		if self.is_event_due() or force:
58 			if not self.is_job_in_queue():
59 				enqueue(
60 					"frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job",
61 					queue=self.get_queue_name(),
62 					job_type=self.method,
63 					job_id=self.rq_job_id,
64 				)
65 				return True
66 			else:
67 				frappe.logger("scheduler").error(
68 					f"Skipped queueing {self.method} because it was found in queue for {frappe.local.site}"
69 				)
70 
71 		return False
72 
73 	def is_event_due(self, current_time=None):
74 		"""Return true if event is due based on time lapsed since last execution"""
75 		# if the next scheduled event is before NOW, then its due!
76 		return self.get_next_execution() <= (current_time or now_datetime())
77 
78 	def is_job_in_queue(self) -> bool:
79 		return is_job_enqueued(self.rq_job_id)
80 
81 	@property
82 	def rq_job_id(self):
83 		"""Unique ID created to deduplicate jobs with single RQ call."""
84 		return f"scheduled_job::{self.method}"
85 
86 	@property
87 	def next_execution(self):
88 		return self.get_next_execution()
89 
90 	def get_next_execution(self):
91 		CRON_MAP = {
92 			"Yearly": "0 0 1 1 *",
93 			"Annual": "0 0 1 1 *",
94 			"Monthly": "0 0 1 * *",
95 			"Monthly Long": "0 0 1 * *",
96 			"Weekly": "0 0 * * 0",
97 			"Weekly Long": "0 0 * * 0",
98 			"Daily": "0 0 * * *",
99 			"Daily Long": "0 0 * * *",
100 			"Hourly": "0 * * * *",
101 			"Hourly Long": "0 * * * *",
102 			"All": f"*/{(frappe.get_conf().scheduler_interval or 240) // 60} * * * *",
103 		}
104 
105 		if not self.cron_format:
106 			self.cron_format = CRON_MAP[self.frequency]
107 
108 		return croniter(
109 			self.cron_format, get_datetime(self.last_execution or datetime(2000, 1, 1))
110 		).get_next(datetime)
111 
112 	def execute(self):
113 		self.scheduler_log = None
114 		try:
115 			self.log_status("Start")
116 			if self.server_script:
117 				script_name = frappe.db.get_value("Server Script", self.server_script)
118 				if script_name:
119 					frappe.get_doc("Server Script", script_name).execute_scheduled_method()
120 			else:
121 				frappe.get_attr(self.method)()
122 			frappe.db.commit()
123 			self.log_status("Complete")
124 		except Exception:
125 			frappe.db.rollback()
126 			self.log_status("Failed")
127 
128 	def log_status(self, status):
129 		# log file
130 		frappe.logger("scheduler").info(f"Scheduled Job {status}: {self.method} for {frappe.local.site}")
131 		self.update_scheduler_log(status)
132 
133 	def update_scheduler_log(self, status):
134 		if not self.create_log:
135 			# self.get_next_execution will work properly iff self.last_execution is properly set
136 			if self.frequency == "All" and status == "Start":
137 				self.db_set("last_execution", now_datetime(), update_modified=False)
138 				frappe.db.commit()
139 			return
140 		if not self.scheduler_log:
141 			self.scheduler_log = frappe.get_doc(
142 				dict(doctype="Scheduled Job Log", scheduled_job_type=self.name)
143 			).insert(ignore_permissions=True)
144 		self.scheduler_log.db_set("status", status)
145 		if status == "Failed":
146 			self.scheduler_log.db_set("details", frappe.get_traceback())
147 		if status == "Start":
148 			self.db_set("last_execution", now_datetime(), update_modified=False)
149 		frappe.db.commit()
150 
151 	def get_queue_name(self):
152 		return "long" if ("Long" in self.frequency) else "default"
153 
154 	def on_trash(self):
155 		frappe.db.delete("Scheduled Job Log", {"scheduled_job_type": self.name})
156 
157 
158 @frappe.whitelist()
159 def execute_event(doc: str):
160 	frappe.only_for("System Manager")
161 	doc = json.loads(doc)
162 	frappe.get_doc("Scheduled Job Type", doc.get("name")).enqueue(force=True)
163 	return doc
164 
165 
166 def run_scheduled_job(job_type: str):
167 	"""This is a wrapper function that runs a hooks.scheduler_events method"""
168 	try:
169 		frappe.get_doc("Scheduled Job Type", dict(method=job_type)).execute()
170 	except Exception:
171 		print(frappe.get_traceback())
172 
173 
174 def sync_jobs(hooks: dict = None):
175 	frappe.reload_doc("core", "doctype", "scheduled_job_type")
176 	scheduler_events = hooks or frappe.get_hooks("scheduler_events")
177 	all_events = insert_events(scheduler_events)
178 	clear_events(all_events)
179 
180 
181 def insert_events(scheduler_events: dict) -> list:
182 	cron_jobs, event_jobs = [], []
183 	for event_type in scheduler_events:
184 		events = scheduler_events.get(event_type)
185 		if isinstance(events, dict):
186 			cron_jobs += insert_cron_jobs(events)
187 		else:
188 			# hourly, daily etc
189 			event_jobs += insert_event_jobs(events, event_type)
190 	return cron_jobs + event_jobs
191 
192 
193 def insert_cron_jobs(events: dict) -> list:
194 	cron_jobs = []
195 	for cron_format in events:
196 		for event in events.get(cron_format):
197 			cron_jobs.append(event)
198 			insert_single_event("Cron", event, cron_format)
199 	return cron_jobs
200 
201 
202 def insert_event_jobs(events: list, event_type: str) -> list:
203 	event_jobs = []
204 	for event in events:
205 		event_jobs.append(event)
206 		frequency = event_type.replace("_", " ").title()
207 		insert_single_event(frequency, event)
208 	return event_jobs
209 
210 
211 def insert_single_event(frequency: str, event: str, cron_format: str = None):
212 	cron_expr = {"cron_format": cron_format} if cron_format else {}
213 
214 	try:
215 		frappe.get_attr(event)
216 	except Exception as e:
217 		click.secho(f"{event} is not a valid method: {e}", fg="yellow")
218 
219 	doc = frappe.get_doc(
220 		{
221 			"doctype": "Scheduled Job Type",
222 			"method": event,
223 			"cron_format": cron_format,
224 			"frequency": frequency,
225 		}
226 	)
227 
228 	if not frappe.db.exists(
229 		"Scheduled Job Type", {"method": event, "frequency": frequency, **cron_expr}
230 	):
231 		savepoint = "scheduled_job_type_creation"
232 		try:
233 			frappe.db.savepoint(savepoint)
234 			doc.insert()
235 		except frappe.DuplicateEntryError:
236 			frappe.db.rollback(save_point=savepoint)
237 			doc.delete()
238 			doc.insert()
239 
240 
241 def clear_events(all_events: list):
242 	for event in frappe.get_all("Scheduled Job Type", fields=["name", "method", "server_script"]):
243 		is_server_script = event.server_script
244 		is_defined_in_hooks = event.method in all_events
245 
246 		if not (is_defined_in_hooks or is_server_script):
247 			frappe.delete_doc("Scheduled Job Type", event.name)
248
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -105,9 +105,12 @@
 		if not self.cron_format:
 			self.cron_format = CRON_MAP[self.frequency]
 
-		return croniter(
-			self.cron_format, get_datetime(self.last_execution or datetime(2000, 1, 1))
-		).get_next(datetime)
+		# If this is a cold start then last_execution will not be set.
+		# Creation is set as fallback because if very old fallback is set job might trigger
+		# immediately, even when it's meant to be daily.
+		# A dynamic fallback like current time might miss the scheduler interval and job will never start.
+		last_execution = get_datetime(self.last_execution or self.creation)
+		return croniter(self.cron_format, last_execution).get_next(datetime)
 
 	def execute(self):
 		self.scheduler_log = None

{"golden_diff": "diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py\n--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py\n+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py\n@@ -105,9 +105,12 @@\n \t\tif not self.cron_format:\n \t\t\tself.cron_format = CRON_MAP[self.frequency]\n \n-\t\treturn croniter(\n-\t\t\tself.cron_format, get_datetime(self.last_execution or datetime(2000, 1, 1))\n-\t\t).get_next(datetime)\n+\t\t# If this is a cold start then last_execution will not be set.\n+\t\t# Creation is set as fallback because if very old fallback is set job might trigger\n+\t\t# immediately, even when it's meant to be daily.\n+\t\t# A dynamic fallback like current time might miss the scheduler interval and job will never start.\n+\t\tlast_execution = get_datetime(self.last_execution or self.creation)\n+\t\treturn croniter(self.cron_format, last_execution).get_next(datetime)\n \n \tdef execute(self):\n \t\tself.scheduler_log = None\n", "issue": "Cron server script runs twice\n<!--\r\nWelcome to the Frappe Framework issue tracker! Before creating an issue, please heed the following:\r\n\r\n1. This tracker should only be used to report bugs and request features / enhancements to Frappe\r\n    - For questions and general support, use https://stackoverflow.com/questions/tagged/frappe\r\n    - For documentation issues, refer to https://frappeframework.com/docs/user/en or the developer cheetsheet https://github.com/frappe/frappe/wiki/Developer-Cheatsheet\r\n2. Use the search function before creating a new issue. Duplicates will be closed and directed to\r\n   the original discussion.\r\n3. When making a bug report, make sure you provide all required information. The easier it is for\r\n   maintainers to reproduce, the faster it'll be fixed.\r\n4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR \ud83d\ude09\r\n-->\r\n\r\n## Description of the issue\r\nCreated a cron server script with \"13 11 17 10 tue\" cron format.\r\nSo it should run at 11:13 17/10/2023.\r\nBut it runs once after save and runs again at correct time.\r\n\r\n## Context information (for bug reports)\r\n![image](https://github.com/frappe/frappe/assets/710051/1531ef89-3fc0-4b0a-a7af-2fe77d626e11)\r\n\r\n![image](https://github.com/frappe/frappe/assets/710051/21fd224d-e841-4598-967f-b79c9895009b)\r\n\r\n\r\n**Output of `bench version`**\r\n```\r\nBench 5.17.2\r\nFrappe 14.52.0\r\n```\r\n\r\n## Steps to reproduce the issue\r\n\r\n1.Create a server script as \"Scheduler Event\", \"Event Frequency\" as \"Cron\"\r\n2.Make cron format as ```59 23 * * *```\r\n3.Set script as\r\n```frappe.log_error(\"Cron Test\", \"Test\")```\r\n\r\n### Observed result\r\nWatch the Error Log. \r\nIt creates a \"Cron Test\" log once and at the midnight.\r\n\r\n### Expected result\r\nIt should only create the error log at the midnight.\r\n### Stacktrace / full error message\r\n\r\n```\r\nNone\r\n```\r\n\r\n## Additional information\r\n\r\nOS version / distribution, `Frappe` install method, etc.\r\nManual install\r\n\n", "before_files": [{"content": "# Copyright (c) 2021, Frappe Technologies and contributors\n# License: MIT. See LICENSE\n\nimport json\nfrom datetime import datetime\n\nimport click\nfrom croniter import croniter\n\nimport frappe\nfrom frappe.model.document import Document\nfrom frappe.utils import get_datetime, now_datetime\nfrom frappe.utils.background_jobs import enqueue, is_job_enqueued\n\n\nclass ScheduledJobType(Document):\n\t# begin: auto-generated types\n\t# This code is auto-generated. Do not modify anything in this block.\n\n\tfrom typing import TYPE_CHECKING\n\n\tif TYPE_CHECKING:\n\t\tfrom frappe.types import DF\n\n\t\tcreate_log: DF.Check\n\t\tcron_format: DF.Data | None\n\t\tfrequency: DF.Literal[\n\t\t\t\"All\",\n\t\t\t\"Hourly\",\n\t\t\t\"Hourly Long\",\n\t\t\t\"Daily\",\n\t\t\t\"Daily Long\",\n\t\t\t\"Weekly\",\n\t\t\t\"Weekly Long\",\n\t\t\t\"Monthly\",\n\t\t\t\"Monthly Long\",\n\t\t\t\"Cron\",\n\t\t\t\"Yearly\",\n\t\t\t\"Annual\",\n\t\t]\n\t\tlast_execution: DF.Datetime | None\n\t\tmethod: DF.Data\n\t\tnext_execution: DF.Datetime | None\n\t\tserver_script: DF.Link | None\n\t\tstopped: DF.Check\n\t# end: auto-generated types\n\tdef autoname(self):\n\t\tself.name = \".\".join(self.method.split(\".\")[-2:])\n\n\tdef validate(self):\n\t\tif self.frequency != \"All\":\n\t\t\t# force logging for all events other than continuous ones (ALL)\n\t\t\tself.create_log = 1\n\n\tdef enqueue(self, force=False) -> bool:\n\t\t# enqueue event if last execution is done\n\t\tif self.is_event_due() or force:\n\t\t\tif not self.is_job_in_queue():\n\t\t\t\tenqueue(\n\t\t\t\t\t\"frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job\",\n\t\t\t\t\tqueue=self.get_queue_name(),\n\t\t\t\t\tjob_type=self.method,\n\t\t\t\t\tjob_id=self.rq_job_id,\n\t\t\t\t)\n\t\t\t\treturn True\n\t\t\telse:\n\t\t\t\tfrappe.logger(\"scheduler\").error(\n\t\t\t\t\tf\"Skipped queueing {self.method} because it was found in queue for {frappe.local.site}\"\n\t\t\t\t)\n\n\t\treturn False\n\n\tdef is_event_due(self, current_time=None):\n\t\t\"\"\"Return true if event is due based on time lapsed since last execution\"\"\"\n\t\t# if the next scheduled event is before NOW, then its due!\n\t\treturn self.get_next_execution() <= (current_time or now_datetime())\n\n\tdef is_job_in_queue(self) -> bool:\n\t\treturn is_job_enqueued(self.rq_job_id)\n\n\t@property\n\tdef rq_job_id(self):\n\t\t\"\"\"Unique ID created to deduplicate jobs with single RQ call.\"\"\"\n\t\treturn f\"scheduled_job::{self.method}\"\n\n\t@property\n\tdef next_execution(self):\n\t\treturn self.get_next_execution()\n\n\tdef get_next_execution(self):\n\t\tCRON_MAP = {\n\t\t\t\"Yearly\": \"0 0 1 1 *\",\n\t\t\t\"Annual\": \"0 0 1 1 *\",\n\t\t\t\"Monthly\": \"0 0 1 * *\",\n\t\t\t\"Monthly Long\": \"0 0 1 * *\",\n\t\t\t\"Weekly\": \"0 0 * * 0\",\n\t\t\t\"Weekly Long\": \"0 0 * * 0\",\n\t\t\t\"Daily\": \"0 0 * * *\",\n\t\t\t\"Daily Long\": \"0 0 * * *\",\n\t\t\t\"Hourly\": \"0 * * * *\",\n\t\t\t\"Hourly Long\": \"0 * * * *\",\n\t\t\t\"All\": f\"*/{(frappe.get_conf().scheduler_interval or 240) // 60} * * * *\",\n\t\t}\n\n\t\tif not self.cron_format:\n\t\t\tself.cron_format = CRON_MAP[self.frequency]\n\n\t\treturn croniter(\n\t\t\tself.cron_format, get_datetime(self.last_execution or datetime(2000, 1, 1))\n\t\t).get_next(datetime)\n\n\tdef execute(self):\n\t\tself.scheduler_log = None\n\t\ttry:\n\t\t\tself.log_status(\"Start\")\n\t\t\tif self.server_script:\n\t\t\t\tscript_name = frappe.db.get_value(\"Server Script\", self.server_script)\n\t\t\t\tif script_name:\n\t\t\t\t\tfrappe.get_doc(\"Server Script\", script_name).execute_scheduled_method()\n\t\t\telse:\n\t\t\t\tfrappe.get_attr(self.method)()\n\t\t\tfrappe.db.commit()\n\t\t\tself.log_status(\"Complete\")\n\t\texcept Exception:\n\t\t\tfrappe.db.rollback()\n\t\t\tself.log_status(\"Failed\")\n\n\tdef log_status(self, status):\n\t\t# log file\n\t\tfrappe.logger(\"scheduler\").info(f\"Scheduled Job {status}: {self.method} for {frappe.local.site}\")\n\t\tself.update_scheduler_log(status)\n\n\tdef update_scheduler_log(self, status):\n\t\tif not self.create_log:\n\t\t\t# self.get_next_execution will work properly iff self.last_execution is properly set\n\t\t\tif self.frequency == \"All\" and status == \"Start\":\n\t\t\t\tself.db_set(\"last_execution\", now_datetime(), update_modified=False)\n\t\t\t\tfrappe.db.commit()\n\t\t\treturn\n\t\tif not self.scheduler_log:\n\t\t\tself.scheduler_log = frappe.get_doc(\n\t\t\t\tdict(doctype=\"Scheduled Job Log\", scheduled_job_type=self.name)\n\t\t\t).insert(ignore_permissions=True)\n\t\tself.scheduler_log.db_set(\"status\", status)\n\t\tif status == \"Failed\":\n\t\t\tself.scheduler_log.db_set(\"details\", frappe.get_traceback())\n\t\tif status == \"Start\":\n\t\t\tself.db_set(\"last_execution\", now_datetime(), update_modified=False)\n\t\tfrappe.db.commit()\n\n\tdef get_queue_name(self):\n\t\treturn \"long\" if (\"Long\" in self.frequency) else \"default\"\n\n\tdef on_trash(self):\n\t\tfrappe.db.delete(\"Scheduled Job Log\", {\"scheduled_job_type\": self.name})\n\n\n@frappe.whitelist()\ndef execute_event(doc: str):\n\tfrappe.only_for(\"System Manager\")\n\tdoc = json.loads(doc)\n\tfrappe.get_doc(\"Scheduled Job Type\", doc.get(\"name\")).enqueue(force=True)\n\treturn doc\n\n\ndef run_scheduled_job(job_type: str):\n\t\"\"\"This is a wrapper function that runs a hooks.scheduler_events method\"\"\"\n\ttry:\n\t\tfrappe.get_doc(\"Scheduled Job Type\", dict(method=job_type)).execute()\n\texcept Exception:\n\t\tprint(frappe.get_traceback())\n\n\ndef sync_jobs(hooks: dict = None):\n\tfrappe.reload_doc(\"core\", \"doctype\", \"scheduled_job_type\")\n\tscheduler_events = hooks or frappe.get_hooks(\"scheduler_events\")\n\tall_events = insert_events(scheduler_events)\n\tclear_events(all_events)\n\n\ndef insert_events(scheduler_events: dict) -> list:\n\tcron_jobs, event_jobs = [], []\n\tfor event_type in scheduler_events:\n\t\tevents = scheduler_events.get(event_type)\n\t\tif isinstance(events, dict):\n\t\t\tcron_jobs += insert_cron_jobs(events)\n\t\telse:\n\t\t\t# hourly, daily etc\n\t\t\tevent_jobs += insert_event_jobs(events, event_type)\n\treturn cron_jobs + event_jobs\n\n\ndef insert_cron_jobs(events: dict) -> list:\n\tcron_jobs = []\n\tfor cron_format in events:\n\t\tfor event in events.get(cron_format):\n\t\t\tcron_jobs.append(event)\n\t\t\tinsert_single_event(\"Cron\", event, cron_format)\n\treturn cron_jobs\n\n\ndef insert_event_jobs(events: list, event_type: str) -> list:\n\tevent_jobs = []\n\tfor event in events:\n\t\tevent_jobs.append(event)\n\t\tfrequency = event_type.replace(\"_\", \" \").title()\n\t\tinsert_single_event(frequency, event)\n\treturn event_jobs\n\n\ndef insert_single_event(frequency: str, event: str, cron_format: str = None):\n\tcron_expr = {\"cron_format\": cron_format} if cron_format else {}\n\n\ttry:\n\t\tfrappe.get_attr(event)\n\texcept Exception as e:\n\t\tclick.secho(f\"{event} is not a valid method: {e}\", fg=\"yellow\")\n\n\tdoc = frappe.get_doc(\n\t\t{\n\t\t\t\"doctype\": \"Scheduled Job Type\",\n\t\t\t\"method\": event,\n\t\t\t\"cron_format\": cron_format,\n\t\t\t\"frequency\": frequency,\n\t\t}\n\t)\n\n\tif not frappe.db.exists(\n\t\t\"Scheduled Job Type\", {\"method\": event, \"frequency\": frequency, **cron_expr}\n\t):\n\t\tsavepoint = \"scheduled_job_type_creation\"\n\t\ttry:\n\t\t\tfrappe.db.savepoint(savepoint)\n\t\t\tdoc.insert()\n\t\texcept frappe.DuplicateEntryError:\n\t\t\tfrappe.db.rollback(save_point=savepoint)\n\t\t\tdoc.delete()\n\t\t\tdoc.insert()\n\n\ndef clear_events(all_events: list):\n\tfor event in frappe.get_all(\"Scheduled Job Type\", fields=[\"name\", \"method\", \"server_script\"]):\n\t\tis_server_script = event.server_script\n\t\tis_defined_in_hooks = event.method in all_events\n\n\t\tif not (is_defined_in_hooks or is_server_script):\n\t\t\tfrappe.delete_doc(\"Scheduled Job Type\", event.name)\n", "path": "frappe/core/doctype/scheduled_job_type/scheduled_job_type.py"}], "after_files": [{"content": "# Copyright (c) 2021, Frappe Technologies and contributors\n# License: MIT. See LICENSE\n\nimport json\nfrom datetime import datetime\n\nimport click\nfrom croniter import croniter\n\nimport frappe\nfrom frappe.model.document import Document\nfrom frappe.utils import get_datetime, now_datetime\nfrom frappe.utils.background_jobs import enqueue, is_job_enqueued\n\n\nclass ScheduledJobType(Document):\n\t# begin: auto-generated types\n\t# This code is auto-generated. Do not modify anything in this block.\n\n\tfrom typing import TYPE_CHECKING\n\n\tif TYPE_CHECKING:\n\t\tfrom frappe.types import DF\n\n\t\tcreate_log: DF.Check\n\t\tcron_format: DF.Data | None\n\t\tfrequency: DF.Literal[\n\t\t\t\"All\",\n\t\t\t\"Hourly\",\n\t\t\t\"Hourly Long\",\n\t\t\t\"Daily\",\n\t\t\t\"Daily Long\",\n\t\t\t\"Weekly\",\n\t\t\t\"Weekly Long\",\n\t\t\t\"Monthly\",\n\t\t\t\"Monthly Long\",\n\t\t\t\"Cron\",\n\t\t\t\"Yearly\",\n\t\t\t\"Annual\",\n\t\t]\n\t\tlast_execution: DF.Datetime | None\n\t\tmethod: DF.Data\n\t\tnext_execution: DF.Datetime | None\n\t\tserver_script: DF.Link | None\n\t\tstopped: DF.Check\n\t# end: auto-generated types\n\tdef autoname(self):\n\t\tself.name = \".\".join(self.method.split(\".\")[-2:])\n\n\tdef validate(self):\n\t\tif self.frequency != \"All\":\n\t\t\t# force logging for all events other than continuous ones (ALL)\n\t\t\tself.create_log = 1\n\n\tdef enqueue(self, force=False) -> bool:\n\t\t# enqueue event if last execution is done\n\t\tif self.is_event_due() or force:\n\t\t\tif not self.is_job_in_queue():\n\t\t\t\tenqueue(\n\t\t\t\t\t\"frappe.core.doctype.scheduled_job_type.scheduled_job_type.run_scheduled_job\",\n\t\t\t\t\tqueue=self.get_queue_name(),\n\t\t\t\t\tjob_type=self.method,\n\t\t\t\t\tjob_id=self.rq_job_id,\n\t\t\t\t)\n\t\t\t\treturn True\n\t\t\telse:\n\t\t\t\tfrappe.logger(\"scheduler\").error(\n\t\t\t\t\tf\"Skipped queueing {self.method} because it was found in queue for {frappe.local.site}\"\n\t\t\t\t)\n\n\t\treturn False\n\n\tdef is_event_due(self, current_time=None):\n\t\t\"\"\"Return true if event is due based on time lapsed since last execution\"\"\"\n\t\t# if the next scheduled event is before NOW, then its due!\n\t\treturn self.get_next_execution() <= (current_time or now_datetime())\n\n\tdef is_job_in_queue(self) -> bool:\n\t\treturn is_job_enqueued(self.rq_job_id)\n\n\t@property\n\tdef rq_job_id(self):\n\t\t\"\"\"Unique ID created to deduplicate jobs with single RQ call.\"\"\"\n\t\treturn f\"scheduled_job::{self.method}\"\n\n\t@property\n\tdef next_execution(self):\n\t\treturn self.get_next_execution()\n\n\tdef get_next_execution(self):\n\t\tCRON_MAP = {\n\t\t\t\"Yearly\": \"0 0 1 1 *\",\n\t\t\t\"Annual\": \"0 0 1 1 *\",\n\t\t\t\"Monthly\": \"0 0 1 * *\",\n\t\t\t\"Monthly Long\": \"0 0 1 * *\",\n\t\t\t\"Weekly\": \"0 0 * * 0\",\n\t\t\t\"Weekly Long\": \"0 0 * * 0\",\n\t\t\t\"Daily\": \"0 0 * * *\",\n\t\t\t\"Daily Long\": \"0 0 * * *\",\n\t\t\t\"Hourly\": \"0 * * * *\",\n\t\t\t\"Hourly Long\": \"0 * * * *\",\n\t\t\t\"All\": f\"*/{(frappe.get_conf().scheduler_interval or 240) // 60} * * * *\",\n\t\t}\n\n\t\tif not self.cron_format:\n\t\t\tself.cron_format = CRON_MAP[self.frequency]\n\n\t\t# If this is a cold start then last_execution will not be set.\n\t\t# Creation is set as fallback because if very old fallback is set job might trigger\n\t\t# immediately, even when it's meant to be daily.\n\t\t# A dynamic fallback like current time might miss the scheduler interval and job will never start.\n\t\tlast_execution = get_datetime(self.last_execution or self.creation)\n\t\treturn croniter(self.cron_format, last_execution).get_next(datetime)\n\n\tdef execute(self):\n\t\tself.scheduler_log = None\n\t\ttry:\n\t\t\tself.log_status(\"Start\")\n\t\t\tif self.server_script:\n\t\t\t\tscript_name = frappe.db.get_value(\"Server Script\", self.server_script)\n\t\t\t\tif script_name:\n\t\t\t\t\tfrappe.get_doc(\"Server Script\", script_name).execute_scheduled_method()\n\t\t\telse:\n\t\t\t\tfrappe.get_attr(self.method)()\n\t\t\tfrappe.db.commit()\n\t\t\tself.log_status(\"Complete\")\n\t\texcept Exception:\n\t\t\tfrappe.db.rollback()\n\t\t\tself.log_status(\"Failed\")\n\n\tdef log_status(self, status):\n\t\t# log file\n\t\tfrappe.logger(\"scheduler\").info(f\"Scheduled Job {status}: {self.method} for {frappe.local.site}\")\n\t\tself.update_scheduler_log(status)\n\n\tdef update_scheduler_log(self, status):\n\t\tif not self.create_log:\n\t\t\t# self.get_next_execution will work properly iff self.last_execution is properly set\n\t\t\tif self.frequency == \"All\" and status == \"Start\":\n\t\t\t\tself.db_set(\"last_execution\", now_datetime(), update_modified=False)\n\t\t\t\tfrappe.db.commit()\n\t\t\treturn\n\t\tif not self.scheduler_log:\n\t\t\tself.scheduler_log = frappe.get_doc(\n\t\t\t\tdict(doctype=\"Scheduled Job Log\", scheduled_job_type=self.name)\n\t\t\t).insert(ignore_permissions=True)\n\t\tself.scheduler_log.db_set(\"status\", status)\n\t\tif status == \"Failed\":\n\t\t\tself.scheduler_log.db_set(\"details\", frappe.get_traceback())\n\t\tif status == \"Start\":\n\t\t\tself.db_set(\"last_execution\", now_datetime(), update_modified=False)\n\t\tfrappe.db.commit()\n\n\tdef get_queue_name(self):\n\t\treturn \"long\" if (\"Long\" in self.frequency) else \"default\"\n\n\tdef on_trash(self):\n\t\tfrappe.db.delete(\"Scheduled Job Log\", {\"scheduled_job_type\": self.name})\n\n\n@frappe.whitelist()\ndef execute_event(doc: str):\n\tfrappe.only_for(\"System Manager\")\n\tdoc = json.loads(doc)\n\tfrappe.get_doc(\"Scheduled Job Type\", doc.get(\"name\")).enqueue(force=True)\n\treturn doc\n\n\ndef run_scheduled_job(job_type: str):\n\t\"\"\"This is a wrapper function that runs a hooks.scheduler_events method\"\"\"\n\ttry:\n\t\tfrappe.get_doc(\"Scheduled Job Type\", dict(method=job_type)).execute()\n\texcept Exception:\n\t\tprint(frappe.get_traceback())\n\n\ndef sync_jobs(hooks: dict = None):\n\tfrappe.reload_doc(\"core\", \"doctype\", \"scheduled_job_type\")\n\tscheduler_events = hooks or frappe.get_hooks(\"scheduler_events\")\n\tall_events = insert_events(scheduler_events)\n\tclear_events(all_events)\n\n\ndef insert_events(scheduler_events: dict) -> list:\n\tcron_jobs, event_jobs = [], []\n\tfor event_type in scheduler_events:\n\t\tevents = scheduler_events.get(event_type)\n\t\tif isinstance(events, dict):\n\t\t\tcron_jobs += insert_cron_jobs(events)\n\t\telse:\n\t\t\t# hourly, daily etc\n\t\t\tevent_jobs += insert_event_jobs(events, event_type)\n\treturn cron_jobs + event_jobs\n\n\ndef insert_cron_jobs(events: dict) -> list:\n\tcron_jobs = []\n\tfor cron_format in events:\n\t\tfor event in events.get(cron_format):\n\t\t\tcron_jobs.append(event)\n\t\t\tinsert_single_event(\"Cron\", event, cron_format)\n\treturn cron_jobs\n\n\ndef insert_event_jobs(events: list, event_type: str) -> list:\n\tevent_jobs = []\n\tfor event in events:\n\t\tevent_jobs.append(event)\n\t\tfrequency = event_type.replace(\"_\", \" \").title()\n\t\tinsert_single_event(frequency, event)\n\treturn event_jobs\n\n\ndef insert_single_event(frequency: str, event: str, cron_format: str = None):\n\tcron_expr = {\"cron_format\": cron_format} if cron_format else {}\n\n\ttry:\n\t\tfrappe.get_attr(event)\n\texcept Exception as e:\n\t\tclick.secho(f\"{event} is not a valid method: {e}\", fg=\"yellow\")\n\n\tdoc = frappe.get_doc(\n\t\t{\n\t\t\t\"doctype\": \"Scheduled Job Type\",\n\t\t\t\"method\": event,\n\t\t\t\"cron_format\": cron_format,\n\t\t\t\"frequency\": frequency,\n\t\t}\n\t)\n\n\tif not frappe.db.exists(\n\t\t\"Scheduled Job Type\", {\"method\": event, \"frequency\": frequency, **cron_expr}\n\t):\n\t\tsavepoint = \"scheduled_job_type_creation\"\n\t\ttry:\n\t\t\tfrappe.db.savepoint(savepoint)\n\t\t\tdoc.insert()\n\t\texcept frappe.DuplicateEntryError:\n\t\t\tfrappe.db.rollback(save_point=savepoint)\n\t\t\tdoc.delete()\n\t\t\tdoc.insert()\n\n\ndef clear_events(all_events: list):\n\tfor event in frappe.get_all(\"Scheduled Job Type\", fields=[\"name\", \"method\", \"server_script\"]):\n\t\tis_server_script = event.server_script\n\t\tis_defined_in_hooks = event.method in all_events\n\n\t\tif not (is_defined_in_hooks or is_server_script):\n\t\t\tfrappe.delete_doc(\"Scheduled Job Type\", event.name)\n", "path": "frappe/core/doctype/scheduled_job_type/scheduled_job_type.py"}]}