Skip to content

Commit f6ba5dd

Browse files
committed
i really hope this fixes stale
1 parent 5e24c0f commit f6ba5dd

File tree

1 file changed

+80
-42
lines changed

1 file changed

+80
-42
lines changed

nephthys/tasks/close_stale.py

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,38 +12,59 @@
1212
from prisma.enums import TicketStatus
1313

1414

15-
async def get_is_stale(ts: str) -> bool:
16-
try:
17-
replies = await env.slack_client.conversations_replies(
18-
channel=env.slack_help_channel, ts=ts, limit=1000
19-
)
20-
last_reply = (
21-
replies.get("messages", [])[-1] if replies.get("messages") else None
22-
)
23-
if not last_reply:
24-
logging.error("No replies found - this should never happen")
25-
await send_heartbeat(f"No replies found for ticket {ts}")
26-
return False
27-
return (
28-
datetime.now(tz=timezone.utc)
29-
- datetime.fromtimestamp(float(last_reply["ts"]), tz=timezone.utc)
30-
) > timedelta(days=3)
31-
except SlackApiError as e:
32-
if e.response["error"] == "ratelimited":
33-
retry_after = int(e.response.headers.get("Retry-After", 1))
34-
logging.warning(
35-
f"Rate limited while fetching replies for ticket {ts}. Retrying after {retry_after} seconds."
36-
)
37-
await asyncio.sleep(retry_after)
38-
return await get_is_stale(ts)
39-
else:
40-
logging.error(
41-
f"Error fetching replies for ticket {ts}: {e.response['error']}"
15+
async def get_is_stale(ts: str, max_retries: int = 3) -> bool:
16+
for attempt in range(max_retries):
17+
try:
18+
replies = await env.slack_client.conversations_replies(
19+
channel=env.slack_help_channel, ts=ts, limit=1000
4220
)
43-
await send_heartbeat(
44-
f"Error fetching replies for ticket {ts}: {e.response['error']}"
21+
last_reply = (
22+
replies.get("messages", [])[-1] if replies.get("messages") else None
4523
)
46-
return False
24+
if not last_reply:
25+
logging.error("No replies found - this should never happen")
26+
await send_heartbeat(f"No replies found for ticket {ts}")
27+
return False
28+
return (
29+
datetime.now(tz=timezone.utc)
30+
- datetime.fromtimestamp(float(last_reply["ts"]), tz=timezone.utc)
31+
) > timedelta(days=3)
32+
except SlackApiError as e:
33+
if e.response["error"] == "ratelimited":
34+
retry_after = int(e.response.headers.get("Retry-After", 1))
35+
# Exponential backoff: wait longer on each retry
36+
wait_time = retry_after * (2**attempt)
37+
logging.warning(
38+
f"Rate limited while fetching replies for ticket {ts}. "
39+
f"Attempt {attempt + 1}/{max_retries}. Retrying after {wait_time} seconds."
40+
)
41+
await asyncio.sleep(wait_time)
42+
if attempt == max_retries - 1:
43+
logging.error(f"Max retries exceeded for ticket {ts}")
44+
return False
45+
if e.response["error"] == "thread_not_found":
46+
logging.warning(
47+
f"Thread not found for ticket {ts}. This might be a deleted thread."
48+
)
49+
await send_heartbeat(f"Thread not found for ticket {ts}.")
50+
await env.db.ticket.update(
51+
where={"msgTs": ts},
52+
data={
53+
"status": TicketStatus.CLOSED,
54+
"closedAt": datetime.now(),
55+
"closedBy": {"connect": {"slackId": env.slack_maintainer_id}},
56+
},
57+
)
58+
return False
59+
else:
60+
logging.error(
61+
f"Error fetching replies for ticket {ts}: {e.response['error']}"
62+
)
63+
await send_heartbeat(
64+
f"Error fetching replies for ticket {ts}: {e.response['error']}"
65+
)
66+
return False
67+
return False
4768

4869

4970
async def close_stale_tickets():
@@ -58,21 +79,38 @@ async def close_stale_tickets():
5879
try:
5980
tickets = await env.db.ticket.find_many(
6081
where={"NOT": [{"status": TicketStatus.CLOSED}]},
61-
include={
62-
"openedBy": True,
63-
},
82+
include={"openedBy": True, "assignedTo": True},
6483
)
6584
stale = 0
6685

67-
for ticket in tickets:
68-
if await get_is_stale(ticket.msgTs):
69-
stale += 1
70-
await resolve(
71-
ticket.msgTs,
72-
ticket.openedBy.slackId, # type: ignore (this is valid - see include above)
73-
env.slack_client,
74-
stale=True,
75-
)
86+
# Process tickets in batches to avoid overwhelming the API
87+
batch_size = 10
88+
for i in range(0, len(tickets), batch_size):
89+
batch = tickets[i : i + batch_size]
90+
logging.info(
91+
f"Processing batch {i // batch_size + 1}/{(len(tickets) + batch_size - 1) // batch_size}"
92+
)
93+
94+
for ticket in batch:
95+
await asyncio.sleep(1.2) # Rate limiting delay
96+
97+
if await get_is_stale(ticket.msgTs):
98+
stale += 1
99+
resolver = (
100+
ticket.assignedToId
101+
if ticket.assignedToId
102+
else ticket.openedById
103+
)
104+
await resolve(
105+
ticket.msgTs,
106+
resolver, # type: ignore (this is explicitly fetched in the db call)
107+
env.slack_client,
108+
stale=True,
109+
)
110+
111+
# Longer delay between batches
112+
if i + batch_size < len(tickets):
113+
await asyncio.sleep(5)
76114

77115
await send_heartbeat(f"Closed {stale} stale tickets.")
78116

0 commit comments

Comments
 (0)