/polls/voters.json 在分页请求中返回重复用户

调用 /polls/voters.json 端点时,使用分页,我们发现第一次调用按预期返回 25 个用户,但第二页返回 26 个用户,其中一个用户在第一次调用中已返回。当用户超过 25 个时,这始终是这样。

问题出在 discourse 的 poll 插件中:
https://github.com/discourse/poll/blob/main/plugins/poll/lib/poll.rb#L223

offset 的计算假设 postgresql SQL 在使用 BETWEEN 时是排他的,但它是包含的。

第一次查询实际返回 25 个,因为计算出的行实际上从 1 开始,而不是 0。

我提出的修复很简单:

   params = {
      offset: offset + 1,
      offset_plus_limit: offset + limit,
      option_digest: opts[:option_id].presence,
    }

或者,一个更优雅的解决方案可能是使用 postgresql 的 LIMIT 和 OFFSET

    params = {
      limit: limit,
      offset: offset,
      option_digest: opts[:option_id].presence,
    }
          WHERE pv.poll_id IN (:poll_ids)
                /* where */
        ) v
        ORDER BY digest, CASE WHEN rank = 'Abstain' THEN 1 ELSE CAST(rank AS integer) END, username
        LIMIT :limit OFFSET :offset
      SQL
2 个赞

干得漂亮,给这个加上一个 pr-welcome,以防有人想尝试修复它。

我们需要确认我们不会破坏 polls 中现有的内容,以防前端错误地依赖于此处的错误假设。

1 个赞

感谢您的快速回复!
我将尝试设置一个环境,我不想在没有测试的情况下进行 PR。任何人都可以随时抢先我。我根本不是 Ruby 开发者。我也不知道如何更改 spec 或测试。

可笑的是,在 UI 中有一个代码可以将结果转储到一个集合中以绕过此错误。

2 个赞

对于其他查看此内容的人,我们在 raffle 软件中进行了类似的 UI 修复,地址如下:

感谢 VHS 的 @lukecyca 识别出问题所在。

还有一个关于从一月开始在投票中加载更多投票者的相关报告。

1 个赞

抓得好。

这比那还有趣。我很久以前提交了几个非常大的 PR 来现代化前端并添加排序选择。

那些 PR 的巨大规模肯定导致我错过了这一点,而后端修复实际上并不在范围内(尽管在处理那些 PR 的过程中,我确实解决了很多严格来说不属于那些 PR 修复的问题)。

我承认我没有怎么考虑就遵循了 2018 年设定的排序选择的前端先例 :sweat_smile:。(你们也只是知道地遵循了它,尽管如此 :laughing:

奇怪的是,在没有识别出根本问题的情况下,变通方法竟然能持续存在。

3 个赞

我想这是我第一次尝试贡献,但它似乎比我预期的要棘手一些。:wink:

我有一个草稿 PR:FIX: do not return duplicates from /polls/voters.json by clechasseur · Pull Request #1 · clechasseur/discourse · GitHub

在这个 PR 中,我首先添加了重现问题的测试,然后应用了 Rob 的简单修复,测试就通过了。

我也尝试了更优雅的解决方案(我更倾向于这个),但虽然它确实防止了重复投票者,但它也改变了返回的投票者在哪个页面上,包括第一个,这可能被认为是一个破坏性更改(取决于前端如何处理它——我还没有查看)。

然而,退一步看,我开始怀疑调用此端点的 limit 参数的真正含义——它并没有真正限制返回的投票者数量,仅仅是每个投票选项返回的投票者数量。您可以在我为多选投票添加的测试中看到这种效果此处——第一页确实限制为每个选项 2 名投票者,但总共返回了三名不同的投票者(分布在各个选项中)。切换到优雅的解决方案(即使用 LIMIT :limit OFFSET :offset)会导致 limit 应用于投票总数而不是投票者。我并不完全确定这是否更好或更直观。

总之,我是新手,可能想多了。简单的解决方案确实消除了重复投票者,并且没有造成太多破坏,所以这可能是可行的方法。在向父仓库提交 PR 之前,我会等待反馈。

另外,我认为代码的这部分还有另一个错误。加载投票者的查询按 digest、rank 和 username 排序——但在按 rank 排序时,它使用了这个条件:

CASE WHEN rank = 'Abstain' THEN 1 ELSE CAST(rank AS integer) END

然而,'Abstain' 实际上对应于 rank 0,而不是 1——rank 1 也可以返回为'1'。这可能会导致跨查询的排序不确定,这意味着根据投票者的数量和使用的 limit 值,在进行分页查询时可能会遗漏投票者。在我的新测试中,我不得不对返回的投票者进行排序以解决不确定的性质。(因为它不确定,所以我假设在测试中很难重现,但我可以尝试一下……)

1 个赞

我点击了上面链接到的一个先前报告,其中包含一个指向更早的更早报告的链接,其中似乎也指出了这种可能性:

不过那已经是很久以前的事了,所以代码可能在那时完全不同。(我尝试用 Git 回溯时间,但历史记录在 2021 年就停止了,所以我假设代码在某个时候被迁移了。)

经过一些试验,我未能找到一个测试用例来重现非确定性排序导致返回的投票者出现问题。我认为这是因为查询的构建方式以及投票的创建方式,但我不能100%确定。

我创建了一个包含我的测试和建议修复的拉取请求:修复:避免从 /polls/voters.json 端点返回重复的投票者 by clechasseur · Pull Request #34433 · discourse/discourse

3 个赞

非常感谢您的修复和对我们审查的耐心。

我已经批准并合并了。 :partying_face:

3 个赞

此主题已在 3 天后自动关闭。不再允许回复。