merge: Reorder relay activities to avoid delivery race condition (resolves #989) (!986)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/986

Closes #989

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Marie 2025-05-08 09:17:07 +00:00
commit a72610c8d6
11 changed files with 40 additions and 44 deletions

View file

@ -95,11 +95,12 @@ export class AccountMoveService {
const srcPerson = await this.apRendererService.renderPerson(src); const srcPerson = await this.apRendererService.renderPerson(src);
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src)); const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
await this.apDeliverManagerService.deliverToFollowers(src, updateAct); await this.apDeliverManagerService.deliverToFollowers(src, updateAct);
this.relayService.deliverToRelays(src, updateAct); await this.relayService.deliverToRelays(src, updateAct);
// Deliver Move activity to the followers of the old account // Deliver Move activity to the followers of the old account
const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst)); const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst));
await this.apDeliverManagerService.deliverToFollowers(src, moveAct); await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
await this.relayService.deliverToRelays(src, moveAct);
// Publish meUpdated event // Publish meUpdated event
const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true }); const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true });

View file

@ -27,15 +27,12 @@ export class AccountUpdateService {
} }
@bindThis @bindThis
public async publishToFollowers(userId: MiUser['id']) { public async publishToFollowers(user: MiUser) {
const user = await this.usersRepository.findOneBy({ id: userId });
if (user == null) throw new Error('user not found');
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
this.apDeliverManagerService.deliverToFollowers(user, content); await this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content); await this.relayService.deliverToRelays(user, content);
} }
} }
} }

View file

@ -51,7 +51,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js'; import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackTask } from '@/misc/promise-tracker.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { LatestNoteService } from '@/core/LatestNoteService.js'; import { LatestNoteService } from '@/core/LatestNoteService.js';
@ -729,7 +729,7 @@ export class NoteCreateService implements OnApplicationShutdown {
//#region AP deliver //#region AP deliver
if (!data.localOnly && this.userEntityService.isLocalUser(user)) { if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
(async () => { trackTask(async () => {
const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user); const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user);
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
@ -755,12 +755,12 @@ export class NoteCreateService implements OnApplicationShutdown {
dm.addFollowersRecipe(); dm.addFollowersRecipe();
} }
if (['public'].includes(note.visibility)) { await dm.execute();
this.relayService.deliverToRelays(user, noteActivity);
}
trackPromise(dm.execute()); if (['public'].includes(note.visibility)) {
})(); await this.relayService.deliverToRelays(user, noteActivity);
}
});
} }
//#endregion //#endregion
} }

View file

@ -247,11 +247,11 @@ export class NoteDeleteService {
@bindThis @bindThis
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
this.apDeliverManagerService.deliverToFollowers(user, content); await this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content); await this.apDeliverManagerService.deliverToUsers(user, content, [
this.apDeliverManagerService.deliverToUsers(user, content, [
...await this.getMentionedRemoteUsers(note), ...await this.getMentionedRemoteUsers(note),
...await this.getRenotedOrRepliedRemoteUsers(note), ...await this.getRenotedOrRepliedRemoteUsers(note),
]); ]);
await this.relayService.deliverToRelays(user, content);
} }
} }

View file

@ -46,7 +46,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { isReply } from '@/misc/is-reply.js'; import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackTask } from '@/misc/promise-tracker.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { LatestNoteService } from '@/core/LatestNoteService.js'; import { LatestNoteService } from '@/core/LatestNoteService.js';
@ -669,7 +669,7 @@ export class NoteEditService implements OnApplicationShutdown {
//#region AP deliver //#region AP deliver
if (!data.localOnly && this.userEntityService.isLocalUser(user)) { if (!data.localOnly && this.userEntityService.isLocalUser(user)) {
(async () => { trackTask(async () => {
const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user); const noteActivity = await this.renderNoteOrRenoteActivity(data, note, user);
const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity);
@ -713,12 +713,12 @@ export class NoteEditService implements OnApplicationShutdown {
} }
} }
if (['public'].includes(note.visibility)) { await dm.execute();
this.relayService.deliverToRelays(user, noteActivity);
}
trackPromise(dm.execute()); if (['public'].includes(note.visibility)) {
})(); await this.relayService.deliverToRelays(user, noteActivity);
}
});
} }
//#endregion //#endregion
} }

View file

@ -49,7 +49,7 @@ export class NotePiningService {
* @param noteId * @param noteId
*/ */
@bindThis @bindThis
public async addPinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { public async addPinned(user: MiUser, noteId: MiNote['id']) {
// Fetch pinee // Fetch pinee
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOneBy({
id: noteId, id: noteId,
@ -78,7 +78,7 @@ export class NotePiningService {
// Deliver to remote followers // Deliver to remote followers
if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) {
this.deliverPinnedChange(user.id, note.id, true); this.deliverPinnedChange(user, note.id, true);
} }
} }
@ -88,7 +88,7 @@ export class NotePiningService {
* @param noteId * @param noteId
*/ */
@bindThis @bindThis
public async removePinned(user: { id: MiUser['id']; host: MiUser['host']; }, noteId: MiNote['id']) { public async removePinned(user: MiUser, noteId: MiNote['id']) {
// Fetch unpinee // Fetch unpinee
const note = await this.notesRepository.findOneBy({ const note = await this.notesRepository.findOneBy({
id: noteId, id: noteId,
@ -106,22 +106,19 @@ export class NotePiningService {
// Deliver to remote followers // Deliver to remote followers
if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) { if (this.userEntityService.isLocalUser(user) && !note.localOnly && ['public', 'home'].includes(note.visibility)) {
this.deliverPinnedChange(user.id, noteId, false); this.deliverPinnedChange(user, noteId, false);
} }
} }
@bindThis @bindThis
public async deliverPinnedChange(userId: MiUser['id'], noteId: MiNote['id'], isAddition: boolean) { public async deliverPinnedChange(user: MiUser, noteId: MiNote['id'], isAddition: boolean) {
const user = await this.usersRepository.findOneBy({ id: userId });
if (user == null) throw new Error('user not found');
if (!this.userEntityService.isLocalUser(user)) return; if (!this.userEntityService.isLocalUser(user)) return;
const target = `${this.config.url}/users/${user.id}/collections/featured`; const target = `${this.config.url}/users/${user.id}/collections/featured`;
const item = `${this.config.url}/notes/${noteId}`; const item = `${this.config.url}/notes/${noteId}`;
const content = this.apRendererService.addContext(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item)); const content = this.apRendererService.addContext(isAddition ? this.apRendererService.renderAdd(user, target, item) : this.apRendererService.renderRemove(user, target, item));
this.apDeliverManagerService.deliverToFollowers(user, content); await this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content); await this.relayService.deliverToRelays(user, content);
} }
} }

View file

@ -90,10 +90,7 @@ export class PollService {
} }
@bindThis @bindThis
public async deliverQuestionUpdate(noteId: MiNote['id']) { public async deliverQuestionUpdate(note: MiNote) {
const note = await this.notesRepository.findOneBy({ id: noteId });
if (note == null) throw new Error('note not found');
if (note.localOnly) return; if (note.localOnly) return;
const user = await this.usersRepository.findOneBy({ id: note.userId }); const user = await this.usersRepository.findOneBy({ id: note.userId });
@ -101,8 +98,8 @@ export class PollService {
if (this.userEntityService.isLocalUser(user)) { if (this.userEntityService.isLocalUser(user)) {
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, user, false), user)); const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, user, false), user));
this.apDeliverManagerService.deliverToFollowers(user, content); await this.apDeliverManagerService.deliverToFollowers(user, content);
this.relayService.deliverToRelays(user, content); await this.relayService.deliverToRelays(user, content);
} }
} }
} }

View file

@ -296,7 +296,7 @@ export class ApNoteService {
await this.pollService.vote(actor, reply, index); await this.pollService.vote(actor, reply, index);
// リモートフォロワーにUpdate配信 // リモートフォロワーにUpdate配信
this.pollService.deliverQuestionUpdate(reply.id); this.pollService.deliverQuestionUpdate(reply);
} }
return null; return null;
}; };
@ -493,7 +493,7 @@ export class ApNoteService {
await this.pollService.vote(actor, reply, index); await this.pollService.vote(actor, reply, index);
// リモートフォロワーにUpdate配信 // リモートフォロワーにUpdate配信
this.pollService.deliverQuestionUpdate(reply.id); this.pollService.deliverQuestionUpdate(reply);
} }
return null; return null;
}; };

View file

@ -5,6 +5,10 @@
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set(); const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
export function trackTask(task: () => Promise<unknown>): void {
trackPromise(task());
}
/** /**
* This tracks promises that other modules decided not to wait for, * This tracks promises that other modules decided not to wait for,
* and makes sure they are all settled before fully closing down the server. * and makes sure they are all settled before fully closing down the server.

View file

@ -614,7 +614,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// フォロワーにUpdateを配信 // フォロワーにUpdateを配信
if (this.userNeedsPublishing(user, updates) || this.profileNeedsPublishing(profile, updatedProfile)) { if (this.userNeedsPublishing(user, updates) || this.profileNeedsPublishing(profile, updatedProfile)) {
this.accountUpdateService.publishToFollowers(user.id); this.accountUpdateService.publishToFollowers(user);
} }
return iObj; return iObj;

View file

@ -174,7 +174,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
// リモートフォロワーにUpdate配信 // リモートフォロワーにUpdate配信
this.pollService.deliverQuestionUpdate(note.id); this.pollService.deliverQuestionUpdate(note);
}); });
} }
} }