22from collections import defaultdict
33from datetime import datetime , timedelta
44
5- from django .db import models
5+ from django .db import connection , models
66from django .db .models import Case , Value , When
77from django .utils import timezone
88
3636logger = logging .getLogger (__name__ )
3737
3838EnqueuedAction = tuple [DataConditionGroup , list [DataCondition ]]
39+ UpdatedStatuses = int
40+ CreatedStatuses = int
41+ ConflictedStatuses = list [tuple [int , int ]] # (workflow_id, action_id)
3942
4043
4144def get_workflow_action_group_statuses (
@@ -71,13 +74,13 @@ def process_workflow_action_group_statuses(
7174 workflows : BaseQuerySet [Workflow ],
7275 group : Group ,
7376 now : datetime ,
74- ) -> tuple [dict [int , int ], set [int ], list [WorkflowActionGroupStatus ]]:
77+ ) -> tuple [dict [int , set [ int ] ], set [int ], list [WorkflowActionGroupStatus ]]:
7578 """
7679 Determine which workflow actions should be fired based on their statuses.
7780 Prepare the statuses to update and create.
7881 """
7982
80- action_to_workflow_ids : dict [int , int ] = {} # will dedupe because there can be only 1
83+ updated_action_to_workflows_ids : dict [int , set [ int ]] = defaultdict ( set )
8184 workflow_frequencies : dict [int , timedelta ] = {
8285 workflow .id : workflow .config .get ("frequency" , 0 ) * timedelta (minutes = 1 )
8386 for workflow in workflows
@@ -91,7 +94,7 @@ def process_workflow_action_group_statuses(
9194 status .workflow_id , zero_timedelta
9295 ):
9396 # we should fire the workflow for this action
94- action_to_workflow_ids [action_id ] = status .workflow_id
97+ updated_action_to_workflows_ids [action_id ]. add ( status .workflow_id )
9598 statuses_to_update .add (status .id )
9699
97100 missing_statuses : list [WorkflowActionGroupStatus ] = []
@@ -107,31 +110,51 @@ def process_workflow_action_group_statuses(
107110 workflow_id = workflow_id , action_id = action_id , group = group , date_updated = now
108111 )
109112 )
110- action_to_workflow_ids [action_id ] = workflow_id
113+ updated_action_to_workflows_ids [action_id ]. add ( workflow_id )
111114
112- return action_to_workflow_ids , statuses_to_update , missing_statuses
115+ return updated_action_to_workflows_ids , statuses_to_update , missing_statuses
113116
114117
115118def update_workflow_action_group_statuses (
116119 now : datetime , statuses_to_update : set [int ], missing_statuses : list [WorkflowActionGroupStatus ]
117- ) -> None :
118- WorkflowActionGroupStatus .objects .filter (
120+ ) -> tuple [ UpdatedStatuses , CreatedStatuses , ConflictedStatuses ] :
121+ updated_count = WorkflowActionGroupStatus .objects .filter (
119122 id__in = statuses_to_update , date_updated__lt = now
120123 ).update (date_updated = now )
121124
122- all_statuses = WorkflowActionGroupStatus .objects .bulk_create (
123- missing_statuses ,
124- batch_size = 1000 ,
125- ignore_conflicts = True ,
126- )
127- missing_status_pairs = [
128- (status .workflow_id , status .action_id ) for status in all_statuses if status .id is None
125+ if not missing_statuses :
126+ return updated_count , 0 , []
127+
128+ # Use raw SQL: only returns successfully created rows
129+ # XXX: the query does not currently include batch size limit like bulk_create does
130+ with connection .cursor () as cursor :
131+ # Build values for batch insert
132+ values_placeholders = []
133+ values_data = []
134+ for s in missing_statuses :
135+ values_placeholders .append ("(%s, %s, %s, %s, %s)" )
136+ values_data .extend ([s .workflow_id , s .action_id , s .group_id , now , now ])
137+
138+ sql = f"""
139+ INSERT INTO workflow_engine_workflowactiongroupstatus
140+ (workflow_id, action_id, group_id, date_added, date_updated)
141+ VALUES { ', ' .join (values_placeholders )}
142+ ON CONFLICT (workflow_id, action_id, group_id) DO NOTHING
143+ RETURNING workflow_id, action_id
144+ """
145+
146+ cursor .execute (sql , values_data )
147+ created_rows = set (cursor .fetchall ()) # Only returns newly inserted rows
148+
149+ # Figure out which ones conflicted (weren't returned)
150+ conflicted_statuses = [
151+ (s .workflow_id , s .action_id )
152+ for s in missing_statuses
153+ if (s .workflow_id , s .action_id ) not in created_rows
129154 ]
130- if missing_status_pairs :
131- logger .warning (
132- "Failed to create WorkflowActionGroupStatus objects" ,
133- extra = {"missing_status_pairs" : missing_status_pairs },
134- )
155+
156+ created_count = len (created_rows )
157+ return updated_count , created_count , conflicted_statuses
135158
136159
137160def get_unique_active_actions (
@@ -190,7 +213,7 @@ def filter_recently_fired_workflow_actions(
190213 workflow_ids = workflow_ids ,
191214 )
192215 now = timezone .now ()
193- action_to_workflow_ids , statuses_to_update , missing_statuses = (
216+ action_to_workflows_ids , statuses_to_update , missing_statuses = (
194217 process_workflow_action_group_statuses (
195218 action_to_workflows_ids = action_to_workflows_ids ,
196219 action_to_statuses = action_to_statuses ,
@@ -199,14 +222,24 @@ def filter_recently_fired_workflow_actions(
199222 now = now ,
200223 )
201224 )
202- update_workflow_action_group_statuses (now , statuses_to_update , missing_statuses )
225+ _ , _ , conflicted_statuses = update_workflow_action_group_statuses (
226+ now , statuses_to_update , missing_statuses
227+ )
228+
229+ # if statuses were not created for some reason, we should not fire for them
230+ for workflow_id , action_id in conflicted_statuses :
231+ action_to_workflows_ids [action_id ].remove (workflow_id )
232+ if not action_to_workflows_ids [action_id ]:
233+ action_to_workflows_ids .pop (action_id )
203234
204- actions_queryset = Action .objects .filter (id__in = list (action_to_workflow_ids .keys ()))
235+ actions_queryset = Action .objects .filter (id__in = list (action_to_workflows_ids .keys ()))
205236
206237 # annotate actions with workflow_id they are firing for (deduped)
207238 workflow_id_cases = [
208- When (id = action_id , then = Value (workflow_id ))
209- for action_id , workflow_id in action_to_workflow_ids .items ()
239+ When (
240+ id = action_id , then = Value (min (list (workflow_ids )))
241+ ) # select 1 workflow to fire for, this is arbitrary but deterministic
242+ for action_id , workflow_ids in action_to_workflows_ids .items ()
210243 ]
211244
212245 return actions_queryset .annotate (
0 commit comments