@@ -405,7 +405,7 @@ func (s *PostgresStore) ListLobbies(ctx context.Context, game string, country, r
405405 return lobbies , nil
406406}
407407
408- func (s * PostgresStore ) CreatePeer (ctx context.Context , peerID , secret , gameID string ) error {
408+ func (s * PostgresStore ) CreatePeer (ctx context.Context , peerID , secret , gameID , connectionID string ) error {
409409 if len (peerID ) > 20 {
410410 logger := logging .GetLogger (ctx )
411411 logger .Warn ("peer id too long" , zap .String ("peerID" , peerID ))
@@ -414,9 +414,9 @@ func (s *PostgresStore) CreatePeer(ctx context.Context, peerID, secret, gameID s
414414
415415 now := util .NowUTC (ctx )
416416 _ , err := s .DB .Exec (ctx , `
417- INSERT INTO peers (peer, secret, game, last_seen, updated_at)
418- VALUES ($1, $2, $3, $4, $4)
419- ` , peerID , secret , gameID , now )
417+ INSERT INTO peers (peer, secret, game, last_seen, updated_at, connection_id )
418+ VALUES ($1, $2, $3, $4, $4, $5 )
419+ ` , peerID , secret , gameID , now , connectionID )
420420 if err != nil {
421421 return err
422422 }
@@ -440,46 +440,49 @@ func (s *PostgresStore) UpdatePeerGeo(ctx context.Context, peerID string, countr
440440 return err
441441}
442442
443- func (s * PostgresStore ) MarkPeerAsActive (ctx context.Context , peerID string ) error {
443+ func (s * PostgresStore ) MarkPeerAsActive (ctx context.Context , peerID , connectionID string ) error {
444+ // Only an authenticated reconnect may reactivate a disconnected peer.
445+ // Late heartbeats must not revive it or postpone timeout cleanup.
444446 now := util .NowUTC (ctx )
445447
446448 _ , err := s .DB .Exec (ctx , `
447449 UPDATE peers
448450 SET
449- disconnected = FALSE,
450451 last_seen = $1,
451452 updated_at = $1
452- WHERE peer = $2
453- ` , now , peerID )
453+ WHERE peer = $2 AND connection_id = $3 AND disconnected = FALSE
454+ ` , now , peerID , connectionID )
454455 return err
455456}
456457
457- func (s * PostgresStore ) MarkPeerAsDisconnected (ctx context.Context , peerID string ) error {
458+ // MarkPeerAsDisconnected ignores handlers superseded by an authenticated reconnect.
459+ func (s * PostgresStore ) MarkPeerAsDisconnected (ctx context.Context , peerID , connectionID string ) (bool , error ) {
458460 now := util .NowUTC (ctx )
459-
460- _ , err := s .DB .Exec (ctx , `
461+ result , err := s .DB .Exec (ctx , `
461462 UPDATE peers
462- SET
463- disconnected = TRUE,
464- updated_at = $1
465- WHERE peer = $2
466- ` , now , peerID )
467- return err
463+ SET disconnected = TRUE, updated_at = $1
464+ WHERE peer = $2 AND connection_id = $3
465+ ` , now , peerID , connectionID )
466+ if err != nil {
467+ return false , err
468+ }
469+ return result .RowsAffected () > 0 , nil
468470}
469471
470- func (s * PostgresStore ) MarkPeerAsReconnected (ctx context.Context , peerID , secret , gameID string ) (bool , []string , error ) {
472+ func (s * PostgresStore ) MarkPeerAsReconnected (ctx context.Context , peerID , secret , gameID , connectionID string ) (bool , []string , error ) {
471473 now := util .NowUTC (ctx )
472474
473475 result , err := s .DB .Exec (ctx , `
474476 UPDATE peers
475477 SET
476478 disconnected = FALSE,
479+ connection_id = $5,
477480 last_seen = $1,
478481 updated_at = $1
479482 WHERE peer = $2
480483 AND secret = $3
481484 AND game = $4
482- ` , now , peerID , secret , gameID )
485+ ` , now , peerID , secret , gameID , connectionID )
483486 if err != nil {
484487 return false , nil , err
485488 }
@@ -516,86 +519,93 @@ func (s *PostgresStore) MarkPeerAsReconnected(ctx context.Context, peerID, secre
516519 return true , lobbies , nil
517520}
518521
519- func (s * PostgresStore ) ClaimNextTimedOutPeer (ctx context.Context , threshold time.Duration ) (string , bool , map [string ][]string , error ) {
522+ func (s * PostgresStore ) ClaimTimedOutPeers (ctx context.Context , threshold time.Duration , limit int ) ([]TimedOutPeer , error ) {
523+ if limit <= 0 {
524+ return nil , fmt .Errorf ("cleanup limit must be positive" )
525+ }
526+ ctx , cancel := context .WithTimeout (ctx , 10 * time .Second )
527+ defer cancel ()
520528 now := util .NowUTC (ctx )
521-
522529 tx , err := s .DB .Begin (ctx )
523530 if err != nil {
524- return "" , false , nil , err
531+ return nil , err
525532 }
526533 defer tx .Rollback (context .Background ()) //nolint:errcheck
527534
528- var peerID string
529- var disconnected bool
530- err = tx .QueryRow (ctx , `
531- WITH d AS (
532- SELECT peer, disconnected
533- FROM peers
535+ rows , err := tx .Query (ctx , `
536+ WITH expired AS (
537+ SELECT peer FROM peers
534538 WHERE last_seen < $1
535- LIMIT 1
539+ ORDER BY last_seen, peer
540+ LIMIT $2
541+ FOR UPDATE SKIP LOCKED
536542 )
537- DELETE FROM peers
538- USING d
539- WHERE peers.peer = d.peer
540- RETURNING d.peer, d.disconnected
541- ` , now .Add (- threshold )).Scan (& peerID , & disconnected )
543+ DELETE FROM peers p USING expired e
544+ WHERE p.peer = e.peer
545+ RETURNING p.peer
546+ ` , now .Add (- threshold ), limit )
542547 if err != nil {
543- if errors .Is (err , pgx .ErrNoRows ) {
544- return "" , false , nil , nil
548+ return nil , err
549+ }
550+ var result []TimedOutPeer
551+ var ids []string
552+ indexes := make (map [string ]int )
553+ for rows .Next () {
554+ var id string
555+ if err := rows .Scan (& id ); err != nil {
556+ rows .Close ()
557+ return nil , err
545558 }
546- return "" , false , nil , err
559+ indexes [id ] = len (result )
560+ ids = append (ids , id )
561+ result = append (result , TimedOutPeer {ID : id , GameLobbies : make (map [string ][]string )})
562+ }
563+ if err := rows .Err (); err != nil {
564+ return nil , err
565+ }
566+ if len (result ) == 0 {
567+ return nil , nil
547568 }
548569
549- gameLobbies := make (map [string ][]string )
550-
551- rows , err := tx .Query (ctx , `
552- UPDATE lobbies
553- SET
554- peers = array_remove(peers, $1),
570+ // Lock each affected lobby once, retaining its old membership for notifications.
571+ // Array overlap uses lobbies_peers; ANY(peers) cannot use that GIN index.
572+ rows , err = tx .Query (ctx , `
573+ WITH affected AS MATERIALIZED (
574+ SELECT game, code, peers FROM lobbies
575+ WHERE peers && $1::varchar(20)[]
576+ ORDER BY game, code
577+ FOR UPDATE
578+ )
579+ UPDATE lobbies l
580+ SET peers = ARRAY(SELECT p FROM unnest(l.peers) AS p WHERE p <> ALL($1::varchar(20)[])),
555581 updated_at = $2
556- WHERE $1 = ANY(peers)
557- RETURNING game, code
558- ` , peerID , now )
582+ FROM affected a
583+ WHERE l.game = a.game AND l.code = a.code
584+ RETURNING a.game, a.code, a.peers
585+ ` , ids , now )
559586 if err != nil {
560- return "" , false , nil , err
587+ return nil , err
561588 }
562-
563589 for rows .Next () {
564- var game string
565- var lobby string
566-
567- err = rows .Scan (& game , & lobby )
568- if err != nil {
569- return "" , false , nil , err
590+ var game , code string
591+ var members []string
592+ if err := rows .Scan (& game , & code , & members ); err != nil {
593+ rows .Close ()
594+ return nil , err
595+ }
596+ for _ , id := range members {
597+ if i , ok := indexes [id ]; ok {
598+ result [i ].GameLobbies [game ] = append (result [i ].GameLobbies [game ], code )
599+ }
570600 }
571-
572- gameLobbies [game ] = append (gameLobbies [game ], lobby )
573601 }
574-
575- if err = rows .Err (); err != nil {
576- return "" , false , nil , err
602+ if err := rows .Err (); err != nil {
603+ return nil , err
577604 }
578-
579- if err = tx .Commit (ctx ); err != nil {
580- return "" , false , nil , err
605+ if err := tx .Commit (ctx ); err != nil {
606+ return nil , err
581607 }
582-
583- return peerID , disconnected , gameLobbies , nil
584- }
585-
586- // ResetAllPeerLastSeen will reset all last_seen.
587- // This is being called when the process restarts so it doesn't matter
588- // how long the process was down.
589- func (s * PostgresStore ) ResetAllPeerLastSeen (ctx context.Context ) error {
590- now := util .NowUTC (ctx )
591-
592- _ , err := s .DB .Exec (ctx , `
593- UPDATE peers
594- SET
595- last_seen = $1,
596- updated_at = $1
597- ` , now )
598- return err
608+ return result , nil
599609}
600610
601611func (s * PostgresStore ) CleanEmptyLobbies (ctx context.Context , olderThan time.Time ) error {
0 commit comments