mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-29 04:22:24 -05:00 
			
		
		
		
	[bugfix] Lock when checking/creating notifs to avoid race (#2890)
* [bugfix] Lock when checking/creating notifs to avoid race * test notif spam
This commit is contained in:
		
					parent
					
						
							
								725a21b027
							
						
					
				
			
			
				commit
				
					
						ebec95a522
					
				
			
		
					 15 changed files with 290 additions and 133 deletions
				
			
		|  | @ -36,14 +36,14 @@ import ( | |||
| // It will also handle notifications for any mentions attached to | ||||
| // the account, and notifications for any local accounts that want | ||||
| // to know when this account posts. | ||||
| func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error { | ||||
| func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error { | ||||
| 	// Ensure status fully populated; including account, mentions, etc. | ||||
| 	if err := s.state.DB.PopulateStatus(ctx, status); err != nil { | ||||
| 	if err := s.State.DB.PopulateStatus(ctx, status); err != nil { | ||||
| 		return gtserror.Newf("error populating status with id %s: %w", status.ID, err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get all local followers of the account that posted the status. | ||||
| 	follows, err := s.state.DB.GetAccountLocalFollowers(ctx, status.AccountID) | ||||
| 	follows, err := s.State.DB.GetAccountLocalFollowers(ctx, status.AccountID) | ||||
| 	if err != nil { | ||||
| 		return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err) | ||||
| 	} | ||||
|  | @ -79,7 +79,7 @@ func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel. | |||
| // adding the status to list timelines + home timelines of each | ||||
| // follower, as appropriate, and notifying each follower of the | ||||
| // new status, if the status is eligible for notification. | ||||
| func (s *surface) timelineAndNotifyStatusForFollowers( | ||||
| func (s *Surface) timelineAndNotifyStatusForFollowers( | ||||
| 	ctx context.Context, | ||||
| 	status *gtsmodel.Status, | ||||
| 	follows []*gtsmodel.Follow, | ||||
|  | @ -98,7 +98,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | |||
| 		// If it's not timelineable, we can just stop early, since lists | ||||
| 		// are prettymuch subsets of the home timeline, so if it shouldn't | ||||
| 		// appear there, it shouldn't appear in lists either. | ||||
| 		timelineable, err := s.filter.StatusHomeTimelineable( | ||||
| 		timelineable, err := s.Filter.StatusHomeTimelineable( | ||||
| 			ctx, follow.Account, status, | ||||
| 		) | ||||
| 		if err != nil { | ||||
|  | @ -124,7 +124,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | |||
| 		// of this follow, if applicable. | ||||
| 		homeTimelined, err := s.timelineStatus( | ||||
| 			ctx, | ||||
| 			s.state.Timelines.Home.IngestOne, | ||||
| 			s.State.Timelines.Home.IngestOne, | ||||
| 			follow.AccountID, // home timelines are keyed by account ID | ||||
| 			follow.Account, | ||||
| 			status, | ||||
|  | @ -160,7 +160,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | |||
| 		//   - This is a top-level post (not a reply or boost). | ||||
| 		// | ||||
| 		// That means we can officially notify this one. | ||||
| 		if err := s.notify(ctx, | ||||
| 		if err := s.Notify(ctx, | ||||
| 			gtsmodel.NotificationStatus, | ||||
| 			follow.Account, | ||||
| 			status.Account, | ||||
|  | @ -175,7 +175,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | |||
| 
 | ||||
| // listTimelineStatusForFollow puts the given status | ||||
| // in any eligible lists owned by the given follower. | ||||
| func (s *surface) listTimelineStatusForFollow( | ||||
| func (s *Surface) listTimelineStatusForFollow( | ||||
| 	ctx context.Context, | ||||
| 	status *gtsmodel.Status, | ||||
| 	follow *gtsmodel.Follow, | ||||
|  | @ -189,7 +189,7 @@ func (s *surface) listTimelineStatusForFollow( | |||
| 	// inclusion in the list. | ||||
| 
 | ||||
| 	// Get every list entry that targets this follow's ID. | ||||
| 	listEntries, err := s.state.DB.GetListEntriesForFollowID( | ||||
| 	listEntries, err := s.State.DB.GetListEntriesForFollowID( | ||||
| 		// We only need the list IDs. | ||||
| 		gtscontext.SetBarebones(ctx), | ||||
| 		follow.ID, | ||||
|  | @ -217,7 +217,7 @@ func (s *surface) listTimelineStatusForFollow( | |||
| 		// list that this list entry belongs to. | ||||
| 		if _, err := s.timelineStatus( | ||||
| 			ctx, | ||||
| 			s.state.Timelines.List.IngestOne, | ||||
| 			s.State.Timelines.List.IngestOne, | ||||
| 			listEntry.ListID, // list timelines are keyed by list ID | ||||
| 			follow.Account, | ||||
| 			status, | ||||
|  | @ -232,7 +232,7 @@ func (s *surface) listTimelineStatusForFollow( | |||
| // listEligible checks if the given status is eligible | ||||
| // for inclusion in the list that that the given listEntry | ||||
| // belongs to, based on the replies policy of the list. | ||||
| func (s *surface) listEligible( | ||||
| func (s *Surface) listEligible( | ||||
| 	ctx context.Context, | ||||
| 	listEntry *gtsmodel.ListEntry, | ||||
| 	status *gtsmodel.Status, | ||||
|  | @ -253,7 +253,7 @@ func (s *surface) listEligible( | |||
| 	// We need to fetch the list that this | ||||
| 	// entry belongs to, in order to check | ||||
| 	// the list's replies policy. | ||||
| 	list, err := s.state.DB.GetListByID( | ||||
| 	list, err := s.State.DB.GetListByID( | ||||
| 		ctx, listEntry.ListID, | ||||
| 	) | ||||
| 	if err != nil { | ||||
|  | @ -273,7 +273,7 @@ func (s *surface) listEligible( | |||
| 		// | ||||
| 		// Check if replied-to account is | ||||
| 		// also included in this list. | ||||
| 		includes, err := s.state.DB.ListIncludesAccount( | ||||
| 		includes, err := s.State.DB.ListIncludesAccount( | ||||
| 			ctx, | ||||
| 			list.ID, | ||||
| 			status.InReplyToAccountID, | ||||
|  | @ -295,7 +295,7 @@ func (s *surface) listEligible( | |||
| 		// | ||||
| 		// Check if replied-to account is | ||||
| 		// followed by list owner account. | ||||
| 		follows, err := s.state.DB.IsFollowing( | ||||
| 		follows, err := s.State.DB.IsFollowing( | ||||
| 			ctx, | ||||
| 			list.AccountID, | ||||
| 			status.InReplyToAccountID, | ||||
|  | @ -325,7 +325,7 @@ func (s *surface) listEligible( | |||
| // | ||||
| // If the status was inserted into the timeline, true will be returned | ||||
| // + it will also be streamed to the user using the given streamType. | ||||
| func (s *surface) timelineStatus( | ||||
| func (s *Surface) timelineStatus( | ||||
| 	ctx context.Context, | ||||
| 	ingest func(context.Context, string, timeline.Timelineable) (bool, error), | ||||
| 	timelineID string, | ||||
|  | @ -343,26 +343,26 @@ func (s *surface) timelineStatus( | |||
| 	} | ||||
| 
 | ||||
| 	// The status was inserted so stream it to the user. | ||||
| 	apiStatus, err := s.converter.StatusToAPIStatus(ctx, status, account) | ||||
| 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account) | ||||
| 	if err != nil { | ||||
| 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | ||||
| 		return true, err | ||||
| 	} | ||||
| 	s.stream.Update(ctx, account, apiStatus, streamType) | ||||
| 	s.Stream.Update(ctx, account, apiStatus, streamType) | ||||
| 
 | ||||
| 	return true, nil | ||||
| } | ||||
| 
 | ||||
| // deleteStatusFromTimelines completely removes the given status from all timelines. | ||||
| // It will also stream deletion of the status to all open streams. | ||||
| func (s *surface) deleteStatusFromTimelines(ctx context.Context, statusID string) error { | ||||
| 	if err := s.state.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| func (s *Surface) deleteStatusFromTimelines(ctx context.Context, statusID string) error { | ||||
| 	if err := s.State.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := s.state.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| 	if err := s.State.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	s.stream.Delete(ctx, statusID) | ||||
| 	s.Stream.Delete(ctx, statusID) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  | @ -370,15 +370,15 @@ func (s *surface) deleteStatusFromTimelines(ctx context.Context, statusID string | |||
| // unpreparing it from all timelines, forcing it to be prepared again (with updated | ||||
| // stats, boost counts, etc) next time it's fetched by the timeline owner. This goes | ||||
| // both for the status itself, and for any boosts of the status. | ||||
| func (s *surface) invalidateStatusFromTimelines(ctx context.Context, statusID string) { | ||||
| 	if err := s.state.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| func (s *Surface) invalidateStatusFromTimelines(ctx context.Context, statusID string) { | ||||
| 	if err := s.State.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| 		log. | ||||
| 			WithContext(ctx). | ||||
| 			WithField("statusID", statusID). | ||||
| 			Errorf("error unpreparing status from home timelines: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := s.state.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| 	if err := s.State.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | ||||
| 		log. | ||||
| 			WithContext(ctx). | ||||
| 			WithField("statusID", statusID). | ||||
|  | @ -392,14 +392,14 @@ func (s *surface) invalidateStatusFromTimelines(ctx context.Context, statusID st | |||
| // Note that calling invalidateStatusFromTimelines takes care of the | ||||
| // state in general, we just need to do this for any streams that are | ||||
| // open right now. | ||||
| func (s *surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Status) error { | ||||
| func (s *Surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Status) error { | ||||
| 	// Ensure status fully populated; including account, mentions, etc. | ||||
| 	if err := s.state.DB.PopulateStatus(ctx, status); err != nil { | ||||
| 	if err := s.State.DB.PopulateStatus(ctx, status); err != nil { | ||||
| 		return gtserror.Newf("error populating status with id %s: %w", status.ID, err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Get all local followers of the account that posted the status. | ||||
| 	follows, err := s.state.DB.GetAccountLocalFollowers(ctx, status.AccountID) | ||||
| 	follows, err := s.State.DB.GetAccountLocalFollowers(ctx, status.AccountID) | ||||
| 	if err != nil { | ||||
| 		return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err) | ||||
| 	} | ||||
|  | @ -427,7 +427,7 @@ func (s *surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Sta | |||
| // slice of followers of the account that posted the given status, | ||||
| // pushing update messages into open list/home streams of each | ||||
| // follower. | ||||
| func (s *surface) timelineStatusUpdateForFollowers( | ||||
| func (s *Surface) timelineStatusUpdateForFollowers( | ||||
| 	ctx context.Context, | ||||
| 	status *gtsmodel.Status, | ||||
| 	follows []*gtsmodel.Follow, | ||||
|  | @ -444,7 +444,7 @@ func (s *surface) timelineStatusUpdateForFollowers( | |||
| 		// If it's not timelineable, we can just stop early, since lists | ||||
| 		// are prettymuch subsets of the home timeline, so if it shouldn't | ||||
| 		// appear there, it shouldn't appear in lists either. | ||||
| 		timelineable, err := s.filter.StatusHomeTimelineable( | ||||
| 		timelineable, err := s.Filter.StatusHomeTimelineable( | ||||
| 			ctx, follow.Account, status, | ||||
| 		) | ||||
| 		if err != nil { | ||||
|  | @ -485,7 +485,7 @@ func (s *surface) timelineStatusUpdateForFollowers( | |||
| 
 | ||||
| // listTimelineStatusUpdateForFollow pushes edits of the given status | ||||
| // into any eligible lists streams opened by the given follower. | ||||
| func (s *surface) listTimelineStatusUpdateForFollow( | ||||
| func (s *Surface) listTimelineStatusUpdateForFollow( | ||||
| 	ctx context.Context, | ||||
| 	status *gtsmodel.Status, | ||||
| 	follow *gtsmodel.Follow, | ||||
|  | @ -499,7 +499,7 @@ func (s *surface) listTimelineStatusUpdateForFollow( | |||
| 	// inclusion in the list. | ||||
| 
 | ||||
| 	// Get every list entry that targets this follow's ID. | ||||
| 	listEntries, err := s.state.DB.GetListEntriesForFollowID( | ||||
| 	listEntries, err := s.State.DB.GetListEntriesForFollowID( | ||||
| 		// We only need the list IDs. | ||||
| 		gtscontext.SetBarebones(ctx), | ||||
| 		follow.ID, | ||||
|  | @ -539,17 +539,17 @@ func (s *surface) listTimelineStatusUpdateForFollow( | |||
| 
 | ||||
| // timelineStatusUpdate streams the edited status to the user using the | ||||
| // given streamType. | ||||
| func (s *surface) timelineStreamStatusUpdate( | ||||
| func (s *Surface) timelineStreamStatusUpdate( | ||||
| 	ctx context.Context, | ||||
| 	account *gtsmodel.Account, | ||||
| 	status *gtsmodel.Status, | ||||
| 	streamType string, | ||||
| ) error { | ||||
| 	apiStatus, err := s.converter.StatusToAPIStatus(ctx, status, account) | ||||
| 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account) | ||||
| 	if err != nil { | ||||
| 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | ||||
| 		return err | ||||
| 	} | ||||
| 	s.stream.StatusUpdate(ctx, account, apiStatus, streamType) | ||||
| 	s.Stream.StatusUpdate(ctx, account, apiStatus, streamType) | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue