ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Activepieces 座位数下限的数据库权威强制:active-user seat floor 架构决策深度解析

Activepieces 座位数下限的数据库权威强制:active-user seat floor 架构决策深度解析 Activepieces 座位数下限的数据库权威强制active-user seat floor 架构决策深度解析【免费下载链接】activepiecesAI Agents MCPs AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces平台的活动用户active user数量不得超过其套餐的座位数上限usersLimit这是 Activepieces 云平台Cloud版多租户计费体系的一条硬约束。本文以仓库决策记录 000013-active-user-seat-floor-is-enforced-db-authoritatively.md 为主体结合 platform-plan.service.ts 等源码实现完整还原座位下限由 AP 服务器自身数据库权威强制这一架构决策的来龙去脉为什么放弃 console 兜底方案、如何用checkUsersExceededLimit与assertSeatsNotBelowActiveUsers两个守卫覆盖全部触达点、以及这套机制在并发、计费投影与降级场景下的行为边界。读完本文你将能准确理解 Activepieces 座位配额机制的设计取舍并掌握如何在源码中定位与验证这两条守卫的全部调用链。一、问题背景两个必须强制的时刻决策记录开篇即给出了约束定义一个平台的活动用户数不得超过其套餐座位上限usersLimit。由于用户的加入与限额两个方向都可能越界强制存在两个关键时刻添加/邀请用户时平台已经用满座位usedSeats usersLimit此时新增任何活动用户都会超限降低限额时发生套餐降级、取消至 Free 套餐或减少座位数导致目标限额低于当前活动用户数。后者尤其隐蔽——限额下降是存量超限前者则是增量拦截。两者性质不同需要不同的强制点这正是后续两条守卫函数分工的依据。1.1 早期方案的失败console 独立兜底决策记录还原了被放弃的第一版设计让计费控制台Autumn通过console.activepieces.com持有主密钥作为独立的第二道防线backstop。其思路是AP 服务器把活动用户数推送给 Autumnbalances.update/ setUsage控制台在办理会越过座位下限的计划变更时读取该数字并拒绝。这个方案最终被废弃原因写得很直白两条都是架构级的密钥权限不足客户维度的密钥无权调用balances.update返回 403写操作必须经由 console 主密钥代理——为了把数字写回 Autumn这一件事要新增一整个 endpoint 和客户端方法成本与收益严重不成比例循环论证控制台读回的是AP 服务器自己写过去的数字它并不是一次独立的校验只是一个最终一致的副本拷贝中间还夹着 TOCTOUtime-of-check-to-time-of-use竞态窗口——检查与使用之间数字可能已经变化。这两个缺陷让console 兜底既笨重又不可信。值得强调的是这一失败案例本身就定义了一个重要原则校验必须基于权威数据源自身而不能基于权威数据源的自我拷贝。二、核心决策在 AP 服务器、对着自己的数据库强制最终的决策只有一句话把下限强制收敛到唯一一处——AP 服务器对着它自己的数据库执行对应源码 platform-plan.service.ts。两个方向的强制分别落成两条守卫强制方向守卫函数触发点失败行为增加邀请/激活checkUsersExceededLimit邀请创建、INACTIVE→ACTIVE 激活抛QUOTA_EXCEEDEDmetric USERS降低限额assertSeatsNotBelowActiveUsers计划降级/checkout、取消至 Free、减少座位抛QUOTA_EXCEEDEDmetric USERS2.1 增加方向的守卫checkUsersExceededLimit在 platform-plan.service.ts 中checkUsersExceededLimit的实现值得逐段拆解它包含三层提前返回和一次关键的一致性保障checkUsersExceededLimit: async ({ platformId, entityManager, additionalSeatsNeeded 1 }): Promisevoid { if (ApEdition.COMMUNITY edition) return // ① 社区版不执行 if (additionalSeatsNeeded 0) return // ② 座位中性操作跳过 if (!await billingProvider.get(log).isBillingEnforced(platformId)) return // ③ 计费未强制时放行fail-open const platformPlan await platformPlanRepo(entityManager) .createQueryBuilder(platform_plan) .setLock(pessimistic_write) // ④ 悲观行锁平台 plan 行 FOR UPDATE .where(platform_plan.platformId :platformId, { platformId }) .getOne() if (isNil(platformPlan)) return const usersLimit effectiveUsersLimit(platformPlan) // ⑤ min(usersLimit, scheduledUsersLimit) if (isNil(usersLimit)) return // unlimited无上限放行 const { usedSeats } await countUsedSeats({ platformId, log, entityManager }) if (usedSeats additionalSeatsNeeded usersLimit) { throw new ActivepiecesError({ code: ErrorCode.QUOTA_EXCEEDED, params: { metric: PlatformUsageMetric.USERS } }) } },几个细节值得注意① 社区版豁免社区版COMMUNITY没有计费实体直接返回这是版本边界② 座位中性操作additionalSeatsNeeded 0时跳过。这个特例的完整论证来自配套决策 000014-pending-invitations-reserve-seats.md当被邀请邮箱已存在平台用户只是加入第二个项目或该邮箱已有未过期邀请在占座即重发邀请时操作不新增任何人座位早已计入usedSeats此时硬比较会误伤合法操作——已达上限是合法稳态参见决策 000017 的降级场景③ fail-open 安全阀isBillingEnforced来自 Autumn 客户状态缓存中的BILLING_ENFORCED特性位见 autumn-utils.ts。计费未强制时放行而不是拒绝符合未知余额时 fail-open的既有设计哲学对应决策 000020-credit-gating-fails-open-on-an-unknown-balance.md 中同一原则的延伸④ 悲观行锁座位检查与写操作在同一个事务里对平台的platform_plan行执行SELECT … FOR UPDATE.setLock(pessimistic_write)使该平台所有消耗座位的写操作串行化。这是决策 000014 对并发问题的最终裁决相比接受时拒绝把错误抛给被邀请者而非管理员、RedLock引入 Redis 依赖和批量争用下的 200ms 轮询延迟等被否决方案Postgres 行锁阻塞即唤醒、无轮询、零新依赖且与仓库既有先例waitpoint-service.ts一致⑤ effectiveUsersLimit取min(usersLimit, scheduledUsersLimit)platform-plan.service.ts来自决策 000017-scheduled-downgrades-cap-seats-immediately.md——当降级已排期但尚未生效时座位消费方强制使用排期座位上限堵住降级到周期结束前不断重新激活/邀请用户的窗口期漏洞。2.2 计数口径usedSeats 活动用户 预留邀请两条守卫共用同一个计数函数countUsedSeatsplatform-plan.service.tsexport async function countUsedSeats({ platformId, log, entityManager }): PromiseSeatBreakdown { const [activeUsers, invitedSeats] await Promise.all([ userService(log).countActiveByPlatformId({ platformId, entityManager }), userInvitationsService(log).countReservedSeats({ platformId, entityManager }), ]) return { activeUsers, invitedSeats, usedSeats: activeUsers invitedSeats } }这里usedSeats activeUsers invitedSeats是决策 000014 确立的GitHub 式模型邀请一旦创建即占座。countReservedSeats的 SQL 口径user-invitation.service.ts是COUNT(DISTINCT LOWER(email))按小写邮箱去重状态限PENDING与ACCEPTED后者在用户尚未被置备前继续占座时间窗口updated cutoff以 7 天INVITATION_EXPIRY_SECONDSuser-invitation.service.ts为界且过滤用updated而非created——重发邀请会刷新 JWT 的updated避免链接仍有效但占座已失效的漂移排除邮箱已存在平台用户任意状态的记录谓词与wouldAddNewUser完全一致。2.3 降低方向的守卫assertSeatsNotBelowActiveUsers限额降低方向由 platform-plan.service.ts 的独立导出函数负责export async function assertSeatsNotBelowActiveUsers({ platformId, targetLimit, log }): Promisevoid { const { usedSeats } await countUsedSeats({ platformId, log }) if (targetLimit usedSeats) { throw new ActivepiecesError({ code: ErrorCode.QUOTA_EXCEEDED, params: { metric: PlatformUsageMetric.USERS } }) } }与增加方向不同它刻意不取行锁决策 000014 的解释这些流程会通过 Autumn 的attach发起网络往返若在锁内跨网络等待计费方响应会串行阻塞该平台的所有邀请。一个有界的瞬时超限读取与限额变更之间恰好落进一个邀请是可接受的——邀请路径是唯一硬上限降级后超限不在范围内。在 autumn-billing.ts 中可以看到它的三处接线目标限额分别来自不同来源计划降级createCheckoutSessionL42-L54取目标计划的includedSeats减少座位adjustUnconsumableFeatureQuantityL63-L73对USERS_LIMIT特性位取将要设置的quantity取消至 FreecancelSubscriptionL103-L114取 Free 计划的includedSeats。三、UI 主动预防 后端权威校验的双层结构决策记录特别强调UI 层是主动预防的——降级/减座前先弹出停用用户对话框让管理员先释放座位对应前端 packages/web/src/app/routes/platform/users/index.tsx 的用户管理页而后端守卫才是权威检查。这个分工的意义在于UI 只负责体验引导永远不能成为安全边界即便绕过 UI 直接调用 API守卫依然在服务端生效。配套决策 000014 还补充了一个细节因为只有owner 不可删除停用用户对话框会列出所有其他活动用户因此总有一个可操作的目标管理员必然能释放出至少一个座位下限永远是可达的。四、限额来源只读投影零回写决策的核心收益之一是usersLimit是从 Autumn 的balance.granted包含座位 已购预付费座位投影而来的一次只读操作。在 autumn-utils.ts 中mapAutumnFeaturesToPlatformPlan(entitlements) { const users entitlements.balances[UnconsumableFeatureId.USERS_LIMIT] return { ... usersLimit: toPlatformPlanLimit(users, null), scheduledUsersLimit: entitlements.scheduledUsersLimit, ... } }toPlatformPlanLimitL528-L536直接取balance.grantedbalance.unlimited为真时映射为null无上限。scheduledUsersLimit则从 Autumn 客户状态中的scheduled状态订阅里提取toScheduledUsersLimitL518-L526随既有的refreshEntitlements投影同步自动派生因此对 Stripe 门户、Autumn 控制台等带外降级也能自愈。没有任何活动用户使用量被写回 Autumn控制台也不做任何座位检查——这正是与早期 backstop 方案的本质区别计费方向的数据流是单向拉取pull-based projectionAP 数据库是强制的唯一事实来源。五、后果与权衡这套设计的代价与边界5.1 单一事实来源零同步负担强制只依赖 AP 数据库没有setUsage、没有 console 兜底、没有新鲜度契约、没有跨服务同步需要维护强制发生在请求时刻request time而非周期结算时问题当场暴露。5.2 座位减少立即生效是临时配置决策记录诚实标注了一个interim临时状态座位减少目前通过 Autumn 的on_decrease: prorate立即生效并退还剩余款项。预期的终态是延迟到周期结束且不退款但由于 Autumn 的客户响应尚未暴露 pending 的计划数量延迟生效的减少量在 UI 中不可见临时配置让减少量立即落入常规的balances.usersLimitAP 投影无需读取任何 scheduled 状态。一旦 Autumn 在getCustomer中提供排期数量就应翻回延迟至周期结束、无退款详见 Billing 特性文档。因为减少立即生效限额在请求时刻就随守卫一起下降决策 000017 中续费时超上限的风险在临时配置下不会出现。5.3 已知边界并非全局不变量值得诚实地说明决策 000014 明确记录SCIM 与托管认证managed-authn / embedding创建用户时不经过任何座位检查因此usedSeats ≤ usersLimit在这些路径上仍可能被突破——这是被刻意排除在范围外的另立工单跟踪。同样开放的问题是接受邀请后的置备本身不做座位检查若占座窗口过期后再注册仍可能越过上限。5.4 计费不因越界而多收即使发生超限Autumn 的计量基于独立的 ACTIVE-only 查询license-key-usage-report-service.ts与usedSeats口径无关——超限不会导致重复计费。六、测试验证如何确认这两条守卫的行为仓库的集成测试 seat-reservation.test.ts 直接验证了下限守卫describe(Active-user floor (assertSeatsNotBelowActiveUsers), () { it(rejects a target seat limit below used seats (active reserved), async () { const { mockPlatform } await setupPlatform({ usersLimit: 5 }) await seedInvitation({ platformId: mockPlatform.id, status: InvitationStatus.PENDING }) await expect( assertSeatsNotBelowActiveUsers({ platformId: mockPlatform.id, targetLimit: 1, log: mockLog }), ).rejects.toMatchObject({ error: { code: ErrorCode.QUOTA_EXCEEDED, params: { metric: PlatformUsageMetric.USERS } } }) }) it(accepts a target seat limit equal to used seats, async () { // ... targetLimit: 2与 usedSeats 相等 resolves }) })两个用例恰好钉住边界语义目标限额低于活动用户 预留邀请时拒绝等于已用座位时放行边界允许取而非。测试还揭示了一个对后续维护者的实际陷阱模拟老化邀请时必须同时回写updated而不仅是created原生UPDATE … SET created会顺带把updated刷到当前时间使记录在占座查询中看起来是新鲜的。七、总结一条决策串起的完整强制链路把这条 ADR 与其两个直接后继000014 预留占座、000017 排期上限放在一起看能拼出 Activepieces 座位体系的全貌方向一增加checkUsersExceededLimit在邀请创建与用户激活时基于usedSeats活动 预留对effectiveUsersLimit当前与排期下限取 min做硬校验配合platform_plan行锁串行化所有占座写操作方向二降低assertSeatsNotBelowActiveUsers在降级、取消、减座三处计费入口用无锁读把目标限额与已用座位对齐数据来源限额只读投影自 Autumnbalance.granted无任何使用量回写AP 数据库是唯一强制事实源诚实边界SCIM/托管认证不设防、接受后置备无独立检查、减少立即生效是临时配置——这些限制都被显式记录而非隐藏。对于要在自建环境中复现或扩展这套机制的开发者建议按以下路径深入源码先读 platform-plan.service.ts 掌握两条守卫与计数口径再看 user-invitation.module.ts 与 user-service.ts 的接线点最后用 autumn-billing.ts 确认三处降级入口并以 seat-reservation.test.ts 作为行为契约。【免费下载链接】activepiecesAI Agents MCPs AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表