Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(backend): チャンネルフォロー一覧のsinceId/untilIdによる絞り込みが上手く動いていないのを修正 #13698

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from

Conversation

samunohito
Copy link
Member

@samunohito samunohito commented Apr 13, 2024

What

チャンネルのフォロー一覧を返すAPIにて、sinceId/untilIdの比較条件として与えているIDに間違いがあるのを直します(与えられているIDはchannel.idだが、比較先のIDはchannel_following.id)
これにより、チャンネルフォロー一覧から結果が抜け落ちる現象が改善されます。

Why

fix #12175

Additional info (optional)

実際にチャンネルを大量に作成&お気に入り登録し、歯抜けにならないことを確認。

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Add story of storybook
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

@github-actions github-actions bot added the packages/backend Server side specific issue/PR label Apr 13, 2024
Copy link

codecov bot commented Apr 13, 2024

Codecov Report

Attention: Patch coverage is 3.12500% with 31 lines in your changes missing coverage. Please review.

Project coverage is 64.90%. Comparing base (e0cf5b2) to head (cd94b17).
Report is 78 commits behind head on develop.

Files Patch % Lines
packages/backend/src/core/QueryService.ts 4.34% 22 Missing ⚠️
...kend/src/server/api/endpoints/channels/followed.ts 0.00% 9 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #13698      +/-   ##
===========================================
- Coverage    64.91%   64.90%   -0.01%     
===========================================
  Files          989      989              
  Lines       113003   113018      +15     
  Branches      5777     5776       -1     
===========================================
  Hits         73352    73352              
- Misses       38209    38224      +15     
  Partials      1442     1442              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

github-actions bot commented Apr 13, 2024

このPRによるapi.jsonの差分

差分はこちら

Get diff files from Workflow Page

q.orderBy(`${q.alias}.id`, 'DESC');
q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SELECT
    "MiChannelFollowing"."id" AS "MiChannelFollowing_id",
    "MiChannelFollowing"."followeeId" AS "MiChannelFollowing_followeeId",
    "MiChannelFollowing"."followerId" AS "MiChannelFollowing_followerId"
FROM
    "channel_following" "MiChannelFollowing"
WHERE
    "MiChannelFollowing"."followeeId" < $ 1
    AND "MiChannelFollowing"."followerId" = $ 2
ORDER BY
    "MiChannelFollowing"."followeeId" DESC
LIMIT
    30

↑のような感じで、外から任意のIDカラムを条件として使用できるようにしました。
省略時は従来通りidを使います

@samunohito samunohito marked this pull request as ready for review April 13, 2024 06:08
@syuilo
Copy link
Member

syuilo commented Apr 14, 2024

channel.idをIDとして与えている箇所って具体的にどこかしら?

@syuilo
Copy link
Member

syuilo commented Apr 14, 2024

見た感じバックエンドは現行の実装で何も問題なさそうだった

@samunohito
Copy link
Member Author

(いまコメントに気が付いた)

@samunohito
Copy link
Member Author

①フロントエンド側のMkPaginationidというプロパティを期待しており、これをページネーションのsinceId/untilIdに使用している

function arrayToEntries(entities: MisskeyEntity[]): [string, MisskeyEntity][] {
return entities.map(en => [en.id, en]);
}

const fetchMore = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
offset: offset.value,
} : {
untilId: Array.from(items.value.keys()).at(-1),
}),
}).then(res => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (i === 10) item._shouldInsertAd_ = true;
}
const reverseConcat = _res => {
const oldHeight = scrollableElement.value ? scrollableElement.value.scrollHeight : getBodyScrollHeight();
const oldScroll = scrollableElement.value ? scrollableElement.value.scrollTop : window.scrollY;
items.value = concatMapWithArray(items.value, _res);
return nextTick(() => {
if (scrollableElement.value) {
scroll(scrollableElement.value, { top: oldScroll + (scrollableElement.value.scrollHeight - oldHeight), behavior: 'instant' });
} else {
window.scroll({ top: oldScroll + (getBodyScrollHeight() - oldHeight), behavior: 'instant' });
}
return nextTick();
});
};
if (res.length === 0) {
if (props.pagination.reversed) {
reverseConcat(res).then(() => {
more.value = false;
moreFetching.value = false;
});
} else {
items.value = concatMapWithArray(items.value, res);
more.value = false;
moreFetching.value = false;
}
} else {
if (props.pagination.reversed) {
reverseConcat(res).then(() => {
more.value = true;
moreFetching.value = false;
});
} else {
items.value = concatMapWithArray(items.value, res);
more.value = true;
moreFetching.value = false;
}
}
offset.value += res.length;
}, err => {
moreFetching.value = false;
});
};
const fetchMoreAhead = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
offset: offset.value,
} : {
sinceId: Array.from(items.value.keys()).at(-1),
}),
}).then(res => {
if (res.length === 0) {
items.value = concatMapWithArray(items.value, res);
more.value = false;
} else {
items.value = concatMapWithArray(items.value, res);
more.value = true;
}
offset.value += res.length;
moreFetching.value = false;
}, err => {
moreFetching.value = false;
});
};


②バックエンド側のQueryService.makePaginationQuery()idというプロパティを期待しており、これをページネーションのsinceId/untilIdに使用している

const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId)
.andWhere({ followerId: me.id });

public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.id`, 'DESC');
} else if (sinceId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.orderBy(`${q.alias}.id`, 'ASC');
} else if (untilId) {
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.id`, 'DESC');
} else if (sinceDate && untilDate) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.id`, 'DESC');
} else if (sinceDate) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.orderBy(`${q.alias}.id`, 'ASC');
} else if (untilDate) {
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.id`, 'DESC');
} else {
q.orderBy(`${q.alias}.id`, 'DESC');
}
return q;
}

ここで渡しているのはchannelFollowingsRepositoryのクエリビルダーなので、channel_followings.idがsinceId/untilIdとして渡されることが想定されている


③バックエンドから返される時にpackされているが、この時のIDはchannelのものになる

return await Promise.all(followings.map(x => this.channelEntityService.pack(x.followeeId, me)));


①+②+③

フロントエンドにはchannel.idをキーとした一覧が返却され、MkPaginationによるページネーションもこのIDが使用される。channels/followedのsinceId/untilIdにはchannel.idが渡ってくることになるが、channels/followedそのものはchannel_followings.idを使って絞り込みをしており、不正確な条件で絞り込みをしてしまう(IDの種類が違うので)

@samunohito
Copy link
Member Author

@syuilo
↑こんな感じかなと

@syuilo
Copy link
Member

syuilo commented Jun 15, 2024

ふぉろーIDを想定しているAPIにチャンネルIDを渡しているのを修正しないとダメそう

@samunohito
Copy link
Member Author

@syuilo
この流れ、1回やった気がするのでもう一度 #12175 を確認いただけますと… 🙏
そのうえでこのブランチの対応がよく無さそうであれば、別の方法を考えます

@syuilo
Copy link
Member

syuilo commented Jul 12, 2024

互換性を維持したまま直すとしたら新しいエンドポイント作るしかなさそうね

@syuilo
Copy link
Member

syuilo commented Jul 12, 2024

フォロー中ユーザー一覧ってどんなレスポンスだったっけ

@samunohito
Copy link
Member Author

return await awaitAll({
id: following.id,
createdAt: this.idService.parse(following.id).date.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
});

followingのIDでした

@syuilo
Copy link
Member

syuilo commented Jul 12, 2024

それと同じ形式で返すエンドポイントを作るしかないわね

@samunohito
Copy link
Member Author

うーむ。

@samunohito
Copy link
Member Author

方針がよくなさそうなので、このPRは白紙にしますか。

@samunohito samunohito closed this Jul 12, 2024
@samunohito samunohito deleted the fix/channel-following-list branch July 12, 2024 10:08
@Sayamame-beans
Copy link
Member

個人的な考えとして…

  • 本来返すべきもの(ChannelFollowing)を返せていない(Channelを返している)問題
    • v2なエンドポイントの作成が必要で、v2が生え次第Misskeyのフロントエンドもこっちを使うように変えるべき
    • v1(現行)とv2で互換性は無い
  • 現状返しているものを前提とした上での挙動が正しくない(現行Misskeyのフロントエンドと、v1を使っているサードパーティクライアントが正常に動いていない)問題
    • v1(現行)のエンドポイントを使っているサードパーティクライアント(更新未追従)が壊れないまま修正でき、互換性も失われない(このPR)

という2つの問題があると捉えていて、前者と後者は方向性が違う(ので両方やって良さそう)と思っているのですが、syuiloさんとしては両方やるのは冗長…という感覚でしょうか?

  • 前者のみの対応だと、サードパーティクライアントは追従するまで壊れた挙動のままになる
  • 後者の対応を行ったとしても、本来返すべきものを返すようにv2を作成する理由は依然ある(v2が生えればMisskeyのフロントエンドもv2に切り替える。v1は互換性のために残す)

@syuilo
Copy link
Member

syuilo commented Jul 12, 2024

v1(現行)のエンドポイントを使っているサードパーティクライアント(更新未追従)が壊れないまま修正でき、互換性も失われない(このPR)

自分の理解が間違ってるかもしれないけど、このPRで修正されるとは思えない(違う種類のデータ同士を比較しているから、期待したデータが返って来たとしても偶然と思われる)

@Sayamame-beans
Copy link
Member

v1(現行)のエンドポイントを使っているサードパーティクライアント(更新未追従)が壊れないまま修正でき、互換性も失われない(このPR)

自分の理解が間違ってるかもしれないけど、このPRで修正されるとは思えない(違う種類のデータ同士を比較しているから、期待したデータが返って来たとしても偶然と思われる)

sinceId/untilIdをフォローした時間ベースのものであると捉えると正にそうなのですが、しかし「"v1(現行)は返ってきているものがChannelであり、ChannelFollowingではない"ので、v1(現行)におけるsinceId/untilIdがチャンネルが作成された時間ベースのものであってもおかしくはない」と思うのですが、いかがでしょうか…?
(もちろん、v2が生えればそれが返すものはChannelFollowingになるので、v2のsinceId/untilIdはフォローした時間ベースになるべき)

@syuilo
Copy link
Member

syuilo commented Jul 12, 2024

あー

@syuilo
Copy link
Member

syuilo commented Jul 12, 2024

それならおかしくないわね

@syuilo
Copy link
Member

syuilo commented Jul 12, 2024

とはいえこのエンドポイントのためだけに makePaginationQuery を拡張したりするのは混乱が生まれそう

@samunohito
Copy link
Member Author

makePaginationQueryを少し弄ったコピペ版を作成し(ただし、安易に複製しないでほしい旨と経緯、このissue/prをコメントで添える)、それを使ってページング処理をするようにすれば、すべてクリアになりますかね…?

@poppingmoon
Copy link

ページネーションが壊れているのを直すという目的を考えると、offsetを追加してsinceId/untilIdは非推奨にするのがシンプルなのではないかと思います

変更に追従しないサードパーティは壊れたままになりますがレスポンスが異なる別のエンドポイントが増える場合よりも追従は容易になります

@Sayamame-beans
Copy link
Member

確かに、offset式に変えるとページネーションは直せるかもしれません…? しかし、正しいものが返ってきていないということが発覚した時点で、"このエンドポイントに手を付けるならv2を作る"ということ自体はほぼ確定しているのでは、と思っています。
そのため、v1に対して"変更に追従しないと壊れたまま"な変更を加えるぐらいなら、直さずにv2作るだけで良いじゃんとなりそうな気がします?
(このPRは、とりあえず"変更に追従されなくても挙動が直る"ようにすることを目的にしていると思うので)

@poppingmoon
Copy link

Channelの代わりにChannelFollowingを返すようにすることにどのような意味があるのでしょうか(フロントエンドでフォローした日時を表示する?)
返すものを変えるためにほとんど同じ機能の別のエンドポイントを作るぐらいなら元のエンドポイントに(互換性を保ったまま)変更を入れるだけでいいのではと思いました

@syuilo
Copy link
Member

syuilo commented Jul 13, 2024

Channelの代わりにChannelFollowingを返すようにすることにどのような意味があるのでしょうか(フロントエンドでフォローした日時を表示する?)

一般的にフォロー中の何かを一覧で返す場合は、フォローした順に一覧したいことが多そう

@poppingmoon
Copy link

現在の実装でもフォローした順に返されているのではないかと思います

@Sayamame-beans
Copy link
Member

現在の実装でもフォローした順に返されているのではないかと思います

そうですが、代わりにページネーションで間が抜けます

@poppingmoon
Copy link

offsetにすれば抜けないのでは?

@Sayamame-beans
Copy link
Member

offsetにすれば抜けないのでは?

サードパーティクライアントが(追従するまで)壊れたままになります

@poppingmoon
Copy link

サードパーティクライアントで壊れないようにするにはv1も修正する必要があると思います
v1を変更してv2も追加するということですか?

@Sayamame-beans
Copy link
Member

offsetを使うか、v2を生やすかのどちらが良いかは諸説あるかもしれませんが、sinceId/untilIdをした時に間が抜けなくなる実装はその話とは別な気もします。
(#13698 (comment) の前者と後者の関係に近いので)

サードパーティクライアントで壊れないようにするにはv1も修正する必要があると思います v1を変更してv2も追加するということですか?

私はそれを意図していますね。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
packages/backend Server side specific issue/PR
Projects
4 participants