1616
1717USER_ACTIVITIES_GENERATE_LIMIT = 50
1818
19+ ACTIVITY_MAX_AGGREGATE_ITEMS = 200
20+
1921
2022class UserEventDetail (object ):
2123 """Regular objects which can be used by seahub without worrying about ORM"""
@@ -46,8 +48,23 @@ def __init__(self, event, username=None):
4648 self .path = event .path
4749
4850 dt = json .loads (event .detail )
49- for key in dt :
50- self .__dict__ [key ] = dt [key ]
51+
52+ # Handle batch operations (detail is an array)
53+ if isinstance (dt , list ):
54+ self .details = dt
55+ self .count = len (dt )
56+ if dt :
57+ first_item = dt [0 ]
58+ if isinstance (first_item , dict ):
59+ for key in first_item :
60+ if key not in self .__dict__ :
61+ self .__dict__ [key ] = first_item [key ]
62+ else :
63+ # Single operation (detail is a dict)
64+ self .details = [dt ] if dt else []
65+ self .count = 1
66+ for key in dt :
67+ self .__dict__ [key ] = dt [key ]
5168
5269 def __getitem__ (self , key ):
5370 return self .__dict__ [key ]
@@ -62,16 +79,19 @@ def _get_user_activities(session, username, start, limit):
6279 logger .error ('limit must be positive' )
6380 raise RuntimeError ('limit must be positive' )
6481
65- sub_query = (
82+ # Optimize: Use two-step query to leverage idx_username_timestamp index
83+ # MariaDB doesn't support LIMIT in IN subquery
84+ activity_ids = session .scalars (
6685 select (UserActivity .activity_id )
6786 .where (UserActivity .username == username )
68- )
87+ .order_by (desc (UserActivity .timestamp ))
88+ .slice (start , start + limit )
89+ ).all ()
6990
7091 stmt = (
7192 select (Activity )
72- .where (Activity .id .in_ (sub_query ))
93+ .where (Activity .id .in_ (activity_ids ))
7394 .order_by (desc (Activity .timestamp ))
74- .slice (start , start + limit )
7595 )
7696 events = session .scalars (stmt ).all ()
7797
@@ -86,16 +106,19 @@ def _get_user_activities_by_op_user(session, username, op_user, start, limit):
86106 logger .error ('limit must be positive' )
87107 raise RuntimeError ('limit must be positive' )
88108
89- sub_query = (
109+ # Optimize: Use two-step query to leverage idx_username_timestamp index
110+ # MariaDB doesn't support LIMIT in IN subquery
111+ activity_ids = session .scalars (
90112 select (UserActivity .activity_id )
91113 .where (UserActivity .username == username )
92- )
114+ .order_by (desc (UserActivity .timestamp ))
115+ .slice (start , start + limit )
116+ ).all ()
93117
94118 stmt = (
95119 select (Activity )
96- .where (Activity .id .in_ (sub_query ) & (Activity .op_user == op_user ))
120+ .where (Activity .id .in_ (activity_ids ) & (Activity .op_user == op_user ))
97121 .order_by (desc (Activity .timestamp ))
98- .slice (start , start + limit )
99122 )
100123 events = session .scalars (stmt ).all ()
101124
@@ -263,7 +286,103 @@ def get_file_daily_history_detail(session, repo_id, path, start_time, end_time,
263286def not_include_all_keys (record , keys ):
264287 return any (record .get (k , None ) is None for k in keys )
265288
289+
290+ BATCH_AGGREGATE_TIME_THRESHOLD = 5
291+ BATCH_AGGREGATE_OP_TYPES = ('create' , 'delete' )
292+
293+
294+ def _find_recent_batch_activity (session , repo_id , op_user , obj_type , op_type ):
295+ """Find aggregatable Activity records within 5 minutes"""
296+ time_limit = datetime .datetime .utcnow () - timedelta (minutes = BATCH_AGGREGATE_TIME_THRESHOLD )
297+
298+ batch_op_type = f'batch_{ op_type } '
299+
300+ stmt = (
301+ select (Activity )
302+ .where (
303+ Activity .repo_id == repo_id ,
304+ Activity .op_user == op_user ,
305+ Activity .obj_type == obj_type ,
306+ Activity .op_type .in_ ([op_type , batch_op_type ]),
307+ Activity .timestamp > time_limit
308+ )
309+ .order_by (desc (Activity .timestamp ))
310+ .limit (1 )
311+ )
312+
313+ return session .scalars (stmt ).first ()
314+
315+ def _extract_detail_item (detail_dict ):
316+ """Extract array item from single Activity and detail dict"""
317+
318+ item = {}
319+ for key in ['obj_id' , 'size' , 'old_path' , 'repo_name' , 'obj_id' , 'old_repo_name' , 'path' ]:
320+ if key in detail_dict and detail_dict [key ] is not None :
321+ item [key ] = detail_dict [key ]
322+ return item
323+
324+ def _update_batch_activity (session , activity , new_record ):
325+ """Append new operation to existing aggregated record"""
326+ # 1. Determine op_type (convert to batch type if not already)
327+ base_op_type = new_record ['op_type' ]
328+ new_op_type = f'batch_{ base_op_type } ' if not activity .op_type .startswith ('batch_' ) else activity .op_type
329+
330+ # 2. Parse existing detail field
331+ try :
332+ current_detail = json .loads (activity .detail )
333+ except json .JSONDecodeError as e :
334+ raise Exception (f'Invalid JSON in Activity.detail: { e } ' )
335+
336+ # 3. Convert to array format (if not already)
337+ detail_array = [_extract_detail_item (current_detail )] if isinstance (current_detail , dict ) else current_detail
338+ if len (detail_array ) >= ACTIVITY_MAX_AGGREGATE_ITEMS :
339+ raise Exception (f"Too many items aggregated in Activity.detail" )
340+ new_detail_item = _extract_detail_item (new_record )
341+ detail_array .append (new_detail_item )
342+
343+ # 5. Update database record
344+ stmt = (
345+ update (Activity )
346+ .where (Activity .id == activity .id )
347+ .values (
348+ op_type = new_op_type ,
349+ timestamp = new_record ['timestamp' ],
350+ detail = json .dumps (detail_array )
351+ )
352+ )
353+ session .execute (stmt )
354+
355+ # 6. Synchronously update UserActivity timestamp
356+ user_activity_stmt = (
357+ update (UserActivity )
358+ .where (UserActivity .activity_id == activity .id )
359+ .values (timestamp = new_record ['timestamp' ])
360+ )
361+ session .execute (user_activity_stmt )
362+
363+ session .commit ()
364+
365+
266366def save_user_activity (session , record ):
367+ """Save or aggregate user activity record"""
368+ op_type = record .get ('op_type' , '' )
369+
370+ if op_type in BATCH_AGGREGATE_OP_TYPES :
371+ try :
372+ recent_activity = _find_recent_batch_activity (
373+ session ,
374+ record ['repo_id' ],
375+ record ['op_user' ],
376+ record ['obj_type' ],
377+ op_type
378+ )
379+
380+ if recent_activity :
381+ _update_batch_activity (session , recent_activity , record )
382+ return
383+ except Exception as e :
384+ logger .warning ('Failed to aggregate activity, creating new record: %s' , e )
385+
267386 activity = Activity (record )
268387 session .add (activity )
269388 session .commit ()
0 commit comments