@@ -274,11 +274,11 @@ def _on_expired_window(
274
274
class SessionWindow (Window ):
275
275
"""
276
276
Session window groups events that occur within a specified timeout period.
277
-
277
+
278
278
A session starts with the first event and extends each time a new event arrives
279
279
within the timeout period. The session closes after the timeout period with no
280
280
new events.
281
-
281
+
282
282
Each session window can have different start and end times based on the actual
283
283
events, making sessions dynamic rather than fixed-time intervals.
284
284
"""
@@ -400,23 +400,25 @@ def process_window(
400
400
late_by_ms = late_by_ms ,
401
401
)
402
402
return [], []
403
-
403
+
404
404
# Look for an existing session that can be extended
405
405
session_start = None
406
406
session_end = None
407
407
can_extend_session = False
408
408
existing_aggregated = None
409
409
old_window_to_delete = None
410
-
410
+
411
411
# Search for active sessions that can accommodate the new event
412
412
search_start = max (0 , timestamp_ms - timeout_ms * 2 )
413
- windows = state .get_windows (search_start , timestamp_ms + timeout_ms + 1 , backwards = True )
414
-
413
+ windows = state .get_windows (
414
+ search_start , timestamp_ms + timeout_ms + 1 , backwards = True
415
+ )
416
+
415
417
for (window_start , window_end ), aggregated_value , _ in windows :
416
418
# Calculate the time gap between the new event and the session's last activity
417
419
session_last_activity = window_end - timeout_ms
418
420
time_gap = timestamp_ms - session_last_activity
419
-
421
+
420
422
# Check if this session can be extended
421
423
if time_gap <= timeout_ms + grace_ms and timestamp_ms >= window_start :
422
424
session_start = window_start
@@ -433,12 +435,12 @@ def process_window(
433
435
434
436
# Process the event for this session
435
437
updated_windows : list [WindowKeyResult ] = []
436
-
438
+
437
439
# Delete the old window if extending an existing session
438
440
if can_extend_session and old_window_to_delete :
439
441
old_start , old_end = old_window_to_delete
440
- transaction .delete_window (old_start , old_end , prefix = key )
441
-
442
+ transaction .delete_window (old_start , old_end , prefix = state . _prefix ) # type: ignore # noqa: SLF001
443
+
442
444
# Add to collection if needed
443
445
if collect :
444
446
state .add_to_collection (
@@ -454,7 +456,11 @@ def process_window(
454
456
current_value = self ._initialize_value ()
455
457
456
458
aggregated = self ._aggregate_value (current_value , value , timestamp_ms )
457
-
459
+
460
+ # By this point, session_start and session_end are guaranteed to be set
461
+ assert session_start is not None # noqa: S101
462
+ assert session_end is not None # noqa: S101
463
+
458
464
# Output intermediate results for aggregations
459
465
if aggregate :
460
466
updated_windows .append (
@@ -463,8 +469,10 @@ def process_window(
463
469
self ._results (aggregated , [], session_start , session_end ),
464
470
)
465
471
)
466
-
467
- state .update_window (session_start , session_end , value = aggregated , timestamp_ms = timestamp_ms )
472
+
473
+ state .update_window (
474
+ session_start , session_end , value = aggregated , timestamp_ms = timestamp_ms
475
+ )
468
476
469
477
# Expire old sessions
470
478
if self ._closing_strategy == ClosingStrategy .PARTITION :
@@ -489,13 +497,13 @@ def expire_sessions_by_partition(
489
497
490
498
# Import the parsing function to extract message keys from window keys
491
499
from quixstreams .state .rocksdb .windowed .serialization import parse_window_key
492
-
500
+
493
501
expired_results = []
494
-
502
+
495
503
# Collect all keys and extract unique prefixes to avoid iteration conflicts
496
504
all_keys = list (transaction .keys ())
497
505
seen_prefixes = set ()
498
-
506
+
499
507
for key_bytes in all_keys :
500
508
try :
501
509
prefix , start_ms , end_ms = parse_window_key (key_bytes )
@@ -504,21 +512,23 @@ def expire_sessions_by_partition(
504
512
except (ValueError , IndexError ):
505
513
# Skip invalid window key formats
506
514
continue
507
-
515
+
508
516
# Expire sessions for each unique prefix
509
517
for prefix in seen_prefixes :
510
518
state = transaction .as_state (prefix = prefix )
511
- prefix_expired = list (self . expire_sessions_by_key (
512
- prefix , state , expiry_threshold , collect
513
- ))
519
+ prefix_expired = list (
520
+ self . expire_sessions_by_key ( prefix , state , expiry_threshold , collect )
521
+ )
514
522
expired_results .extend (prefix_expired )
515
523
count += len (prefix_expired )
516
524
517
525
if count :
518
526
logger .debug (
519
- "Expired %s session windows in %ss" , count , round (time .monotonic () - start , 2 )
527
+ "Expired %s session windows in %ss" ,
528
+ count ,
529
+ round (time .monotonic () - start , 2 ),
520
530
)
521
-
531
+
522
532
return expired_results
523
533
524
534
def expire_sessions_by_key (
@@ -532,29 +542,40 @@ def expire_sessions_by_key(
532
542
count = 0
533
543
534
544
# Get all windows and check which ones have expired
535
- all_windows = list (state .get_windows (0 , expiry_threshold + self ._timeout_ms , backwards = False ))
536
-
545
+ all_windows = list (
546
+ state .get_windows (0 , expiry_threshold + self ._timeout_ms , backwards = False )
547
+ )
548
+
537
549
windows_to_delete = []
538
550
for (window_start , window_end ), aggregated , _ in all_windows :
539
551
# Session expires when the session end time has passed the expiry threshold
540
552
if window_end <= expiry_threshold :
541
553
collected = []
542
554
if collect :
543
555
collected = state .get_from_collection (window_start , window_end )
544
-
556
+
545
557
windows_to_delete .append ((window_start , window_end ))
546
558
count += 1
547
- yield (key , self ._results (aggregated , collected , window_start , window_end ))
559
+ yield (
560
+ key ,
561
+ self ._results (aggregated , collected , window_start , window_end ),
562
+ )
548
563
549
564
# Clean up expired windows
550
565
for window_start , window_end in windows_to_delete :
551
- state ._transaction .delete_window (window_start , window_end , prefix = state ._prefix )
566
+ state ._transaction .delete_window ( # type: ignore # noqa: SLF001
567
+ window_start ,
568
+ window_end ,
569
+ prefix = state ._prefix , # type: ignore # noqa: SLF001
570
+ )
552
571
if collect :
553
572
state .delete_from_collection (window_end , start = window_start )
554
573
555
574
if count :
556
575
logger .debug (
557
- "Expired %s session windows in %ss" , count , round (time .monotonic () - start , 2 )
576
+ "Expired %s session windows in %ss" ,
577
+ count ,
578
+ round (time .monotonic () - start , 2 ),
558
579
)
559
580
560
581
def _on_expired_session (
@@ -576,9 +597,9 @@ def _on_expired_session(
576
597
topic = "unknown"
577
598
partition = - 1
578
599
offset = - 1
579
-
600
+
580
601
to_log = True
581
-
602
+
582
603
# Trigger the "on_late" callback if provided
583
604
if self ._on_late :
584
605
to_log = self ._on_late (
0 commit comments