Flostra(gback)面试追问树
核对日期:2026-09-07。范围是 Go 控制面 gback,以及 Python backend、React frontend 中必要的消息和事件契约。gback HEAD 为 9751cd1ce0438ab4574db6b172bd96d18b4c1090;分析包含当前未提交修改,不能把 HEAD 内容替代工作树。本章只读代码和现有测试,没有运行 Compose、全量测试或浏览器。
下文“参考回答”是可口述的技术解释;“建议”是尚待实现的设计;“源码推断”表示发现了可具体推导的失败路径,但本次没有执行复现。个人职责、设计决策和验收经历,仍只能讲自己实际完成并能解释的部分。
先校正简历中容易被追问的主张
| 优先级 | 简历表述 | 当前源码核对 | 面试措辞 |
|---|---|---|---|
| 高 | Redis Stream 回放 + SSE 断线续传(sequenceId) | 全局 Stream 用 XADD 追加;Replay 实际从每执行的 Redis List 做 LRANGE。SSE 只写 data,没有 id,也不读取 Last-Event-ID | 改为“Redis 短期事件回放 + SSE 重连重放,前端利用事件序号处理重复”;不能说标准 SSE 游标续传 |
| 高 | Attempt Fencing 防旧实例回写 | Go 有 current_attempt_id、worker_id、lease 和终态校验;Worker 发布 started 后直接运行,没有等待 Go 返回 claim 授权 | 可以讲“控制面状态围栏”;不能讲“执行前排他锁,因此不会重复副作用” |
| 高 | 可靠异步执行 | Outbox、Confirm、重投已实现;当前 Go publish 的 mandatory 为 false;仍有确认未知和不可路由窗口 | 至少一次投递机制,需说明拓扑假设、重发和业务幂等 |
| 中 | Python DAG 能力 | 有 Kahn 拓扑校验、批次并发、可重试异常策略;ready 条件与异常汇总仍有需复现的源码风险 | 不再说“仅串行”,也不扩写为完整可靠的通用 DAG 引擎 |
| 中 | 结果可靠性历史风险 | 当前 Python 已改为 PERSISTENT、主执行路径终态发布失败 NACK;Go 未知完成状态会报协议错误 | 旧版本“非持久事件、失败仍无条件 ACK、未知状态默认成功”不适用于当前主路径 |
| 中 | 安全隔离 | Go 新建执行拒绝 code;Worker 默认注册表也不再导入 code。已有出站检查与部分字段脱敏;Secret 仍明文落库 | 区分平台访问控制、可信服务、传输、静态存储和节点能力,不声称生产级沙箱 |
核心证据:回放与事件持久化、SSE 输出、任务发布、Worker 执行。
追问树索引
这是一组可自适应展开的项目面试分支。面试官通常选择其中 3—5 根深挖,不需要一轮机械问完。共 9 根问题、29 个追问节点;每个节点在后文都有参考回答。
F1 架构与真实 DAG 能力
├─ F1.1 若解释了双语言职责 → 为什么这样划分?
└─ F1.2 若提到并行 DAG → 拓扑排序等于正确调度吗?
└─ F1.2.1 若认为全部前驱都等待了 → 给出不等深汇合反例
F2 Outbox、事务、SKIP LOCKED
├─ F2.1 若说“同一事务” → 哪些操作在事务内?
└─ F2.2 若说“多副本认领” → 锁与租约各解决什么?
└─ F2.2.1 若说租约大于单次超时就够 → 批次串行发送呢?
F3 Publisher Confirm
├─ F3.1 若把 Confirm 等同送达队列 → mandatory=false 呢?
│ └─ F3.1.1 若建议打开 mandatory → return 与 confirm 如何协调?
└─ F3.2 若认为 messageId 可防重复 → 确认后崩溃窗口
F4 Attempt 与 Watchdog
├─ F4.1 若说 Worker 抢到 lease 才运行 → claim 是否有响应?
│ └─ F4.1.1 若理解异步认领 → 如何加强执行授权?
└─ F4.2 若说租约过期必然拒绝完成 → 检查真实终态竞争
F5 三种重试与 Redrive
├─ F5.1 若提到幂等键 → 并发双击如何处理?
│ └─ F5.1.1 若说返回完全相同响应 → 核对响应语义
└─ F5.2 若提到节点 retryPolicy → 已接入哪些真实异常?
F6 协作式取消
├─ F6.1 若解释 Redis marker → PG/Redis 双写失败怎么办?
│ └─ F6.1.1 若认为补偿覆盖所有情况 → 进程中途崩溃呢?
└─ F6.2 若说取消立即终止 → prefetch、阻塞节点与副作用
F7 结果事件与 SSE
├─ F7.1 若说终态可靠 → ACK/NACK、协议错误如何闭环?
│ └─ F7.1.1 若说事件失败都会被捕获 → gather 异常汇总呢?
└─ F7.2 若说 Stream 续传 → List、SSE id 与保留期
└─ F7.2.1 若说 sequence 解决全部问题 → 缺口与跨 Attempt
F8 租户和 RBAC
├─ F8.1 若说 UUID 隔离 → 权限从哪里推导?
│ └─ F8.1.1 若说撤权立即生效 → 已建立 SSE 怎么办?
└─ F8.2 若说 Redis key 已隔离 → Worker 和配额边界
F9 Secret 与 Worker 能力
├─ F9.1 若把明文等同跨租户 → 分清四种威胁
│ └─ F9.1.1 若说事件已全部脱敏 → 终态 results/errorMessage 呢?
└─ F9.2 若说已经禁用 code → 这是否等于沙箱?
└─ F9.2.1 若说有出站检查就安全 → DNS/连接/其他 connector
F1:请从用户点击运行,讲清一次任务的完整路径
参考回答: 前端请求 Go API,控制面完成身份和资源授权,优先从分支当前 Revision 取图快照(旧数据无 Revision 时回退到分支图),在 PostgreSQL 同一事务写 execution 与 outbox。Dispatcher 后台认领并通过 RabbitMQ 投递,Python 解析图、执行节点、发布结果事件;Go 按 Attempt 校验和更新状态,保存短期事件,SSE 推给前端。用户不需要等待工作流执行完才拿到 executionId。Go 负责控制规则和持久状态,Python 负责节点运行。
证据:创建执行、跨进程字段、Worker 主流程。
F1.1|为什么不统一成 Go,或者全部写 Python?
参考回答:语言分界服务于职责。控制面集中事务、RBAC、版本、调度和状态机;执行面承接已有 Python 节点生态。代价是协议兼容、跨服务调试和部署复杂度。因此我优先明确 workflow.run/run_result 字段语义和错误分类,不把增加语言或服务数量当成架构成果。改成 Go 也不会自动获得副作用幂等或安全隔离。
F1.2|有拓扑排序,就能说是正确并行 DAG 吗?
参考回答:不能。拓扑排序证明无环和存在合法顺序;调度还必须等所有必需前驱进入满足条件的状态,区分“尚未产出”与“该分支不会产出”。当前实现确实有 Kahn 排序、control/data 边和 asyncio.gather,但 ready 使用 should_skip_node。不能仅凭代码里出现 gather 宣称完整 DAG 能力。
F1.2.1|A→B、A→D、B→D,D 是否必然等 B?
参考回答:按当前源码推导,A 完成后,B 和 D 都可能因已有一个数据输入被视为 ready,在同一批启动;静态常量还会让带前驱的节点更早满足条件。现有对称菱形测试不能证明这种不等深汇合正确。我会用屏障控制 B 完成时机,断言 D 之前不能读到部分输入;设计上维护未完成依赖计数,再单独计算分支跳过语义。这是本次静态推断,未执行复现。
证据:workflow_engine.py:255-265、314-317;现有测试 test_engine_scheduling.py。
F2:为什么是 Outbox,事务边界到底在哪里?
参考回答: 直接“写 PG,再发 Rabbit”有进程崩溃窗口;顺序反过来又会出现消息已发但数据库回滚。Outbox 把待发任务和 execution 一起落库,让后台能继续处理已提交事实。它不是 PG 与 Rabbit 的分布式原子事务,处理的是可恢复投递意图。
F2.1|哪些操作在事务内,为什么网络调用放在事务外?
参考回答:创建 execution/outbox 在一笔事务;认领 outbox 并写 owner/lease 在另一笔短事务;真正 publish 在这些事务提交之后。长网络调用不应一直持有认领行锁。Confirm 后另开事务标 PUBLISHED。任务图快照已经落库;Secret 则在发布前按引用重新解析,避免在 outbox 再存一份凭据。
证据:service.go:221-229;outbox.go:194-232、260-282、373-390。
F2.2|行锁和租约是不是重复设计?
参考回答:行锁保护“这一刻谁来认领”的并发原子性,事务提交后释放;租约把“这个实例正在处理”的所有权持久化,支持实例宕机后接管。SKIP LOCKED 跳过别人占用的候选行,适合队列式工作分配,不保证严格 FIFO 或全局公平。发布后写状态还需 owner 条件,不能只凭手里的旧对象覆盖新 owner。
PostgreSQL 对这类队列用途和不一致视图的说明见 SELECT 文档。项目证据:outbox.go:198-229、397-409。
F2.2.1|默认一次认领 16 条、依次发,每条 10 秒,30 秒租约够吗?
参考回答:仅比较一次 publish timeout 和 lease 不够。当前批次统一租约、串行 dispatch;后面的记录可能在真正发送前过期。prepareAttempt 检查 owner,可在被别人接管后停止,但不检查尚未被接管的过期时间,也没有逐条续租。因此慢 broker 下存在额外重复窗口的源码依据。可以按可用并发逐条认领,或在发布前刷新租约,并持续保留 fencing;不要把“租约 30 秒”当正确性证明。
证据:outbox.go:86-95、169-182、207-228、297-305。此处未做延迟注入复现。
F3:Publisher Confirm 是不是消息执行成功?
参考回答: 不是。Confirm 与 consumer ACK 分别覆盖 publisher→broker、consumer→broker。持久消息路由到 durable queue 后的确认,与 Worker 是否完成业务是两个事实。当前实现持久队列、Persistent 消息、串行等待 Confirm,超时关闭旧 channel,避免晚来的确认被误配给下一条消息。
证据:Rabbit 客户端,:187-241;协议语义见 RabbitMQ 官方文档。
F3.1|如果队列在声明后被删除,仍可能拿到 Confirm 吗?
参考回答:会有这个风险。当前 mandatory=false,没有处理 basic.return;不可路由消息也可能被 broker 确认。因此现在应说“在预期队列拓扑存在的条件下等待发布确认”,不能承诺任意拓扑故障下不丢。需要把不可路由作为发布失败纳入 outbox,并监控拓扑变化。
证据:rabbitmq.go:210-215;RabbitMQ 确认时机说明。
F3.1.1|打开 mandatory,再 select 一下 return 和 confirm 就行了吗?
参考回答:还要协调两类通知。协议上不可路由消息的 return 在 confirm 前发送,但客户端若拆到不同 channel,业务 select 不能把先选中的 ACK 直接当成功。应在发布前注册 return/confirm 接收器,按发布标识关联,以统一状态机完成判定;超时状态保持未知,并丢弃旧 channel。不能靠“ACK 后睡几十毫秒没 return”证明成功。这是建议,当前项目未实现 return 处理。
F3.2|Confirm 后、更新 PUBLISHED 前崩溃,messageId 会自动去重吗?
参考回答:不会。恢复后重发是合法情况,Rabbit 普通队列不会因为 messageId 相同就去重。executionId 用于关联,Attempt 用于控制面当前执行权;邮件、SQL、第三方 API 等副作用还需要自己的幂等契约。接收端可用唯一键记录操作结果,与业务写入同事务;外部 API 则使用其幂等键或可查询的业务编号。
证据:rabbitmq.go:210-225;outbox.go:293-355;messageId 来源。
F4:Attempt fencing 是否真的让一个任务只被一个 Worker 执行?
参考回答: 当前围栏主要保证控制面只接受当前 Attempt 和合法 Worker 的状态。Dispatcher 创建新 attempt,旧活动 attempt 置为 SUPERSEDED;Go 收到 started 认领 worker_id,续租、完成、节点事件都校验身份。它不等于执行前拿到一个双方确认的排他许可。
证据:Attempt 创建、认领和续租、事件围栏。
F4.1|Worker 等 Go 返回 claim 成功才开始执行吗?
参考回答:没有。Worker 将 started 发到结果队列后直接运行,等待的是消息发布而非 Go 的业务授权响应。另一个 Worker 即使最终被控制面拒绝,也可能已经执行节点。故不能说“worker_id + lease 保证只有一个 Worker 产生副作用”。
证据:backend/workflow_engine.py:307-358;backend/mq_worker.py:374-398、426-431。
F4.1.1|怎样加强?
参考回答:可增加受认证的 claim RPC 或请求响应协议,只有 CAS 认领成功者进入执行,并让授权有期限。续租失败后停止调度新节点。但进程暂停、网络分区和外部调用已提交仍会产生重复风险,最终还要让外部资源识别 fencing token 或幂等键。不能把 RPC claim 单独升级为 exactly-once。
F4.2|租约过期的一瞬间,completed 一定被拒绝吗?
参考回答:按当前代码不是。Heartbeat 拒绝过期续租,但 CompleteAttempt 没有再次检查 lease/deadline;它与 Watchdog 通过 execution→attempt 同顺序加锁竞争。Watchdog 先把状态置 TIMED_OUT 后,迟到完成会被挡住;若完成先拿锁且状态仍 active,可能成功。因此准确契约是“Watchdog 裁决后的终态不被旧完成复活”,不是严格截止时间内完成证明。
证据:attempt.go:151-176、193-245;Watchdog。Watchdog 只扫描 RUNNING,未启动的 DISPATCHED 任务没有相同运行期租约保障,需另定排队超时策略。
F5:重试、死信和 Redrive 各解决什么?
参考回答: Outbox 重试解决准备/投递失败,采用上限退避,最终写数据库 DEAD_LETTERED;Worker 节点重试属于业务执行过程;manual redrive 是 FAILED/TIMED_OUT 后的显式恢复操作。Watchdog 不自动重跑未知副作用。当前“死信”是数据库状态,未完成 Rabbit 原生 DLX 长期归档。
F5.1|同一幂等键并发两次怎么防双重重置?
参考回答:先查 execution/action/key hash 的审计事实,再按 outbox→execution 顺序加锁,拿到锁后重查。首个请求在同事务写审计、重置 execution 为 QUEUED 和 outbox 为 PENDING;第二个返回已有 auditId。换新 key 不应绕过运行态检查,只有符合终态条件才允许 redrive。
证据:redrive.go:159-213、229-258。
F5.1.1|重复请求的响应逐字相同吗?
参考回答:当前不是响应快照语义。它复用同一 auditId,设置 Idempotent=true,但重新读取当前 execution;任务可能已经运行或完成。所以保证“同一幂等键不再次执行重置动作”,不保证返回原来的 JSON。Attempt 是 Dispatcher 下一次投递准备时创建,redrive API 本身先清空 current_attempt_id。
证据:redrive.go:168-175、194-199、240-257;outbox.go:343-354。
F5.2|设置 retryPolicy 就能让所有 HTTP 错误指数退避吗?
参考回答:不能这样讲。通用策略只捕获 RetryableNodeError;全仓搜索目前主要由引擎和测试使用。HTTP 节点另有自己的固定间隔重试。应先统一 connector 错误分类和幂等约束,避免内外两层重试相乘。配置里 backoffMs 限制的是基数,指数增长后的总等待并不等于“最多 30 秒”。
证据:backend/workflow_engine.py:479-503、539-550;HTTP 重试。
F6:取消请求如何跨数据库与正在运行的 Worker?
参考回答: PG 记录 CANCEL_REQUESTED,Redis 保存取消 marker;Worker 在消费前及事件边界检查它。这样即使任务进入 Rabbit prefetch 也能看到取消意图。API 成功表达“取消已请求”,实际停止受节点合作程度影响,不能当作外部操作已经撤销。
F6.1|PG 成功、Redis 失败怎么办?
参考回答:代码先提交 PG,再写 Redis;写失败时,只有当前仍为 CANCEL_REQUESTED 才补偿回先前状态,避免覆盖并发完成。重复取消仍会重写 marker。接口返回 503,而不是假装请求已可靠通知 Worker。补偿错误当前被忽略,这也需要如实说明。
证据:service.go:441-476。
F6.1.1|PG 提交后进程立刻崩溃,补偿还会执行吗?
参考回答:不会。跨存储双写仍有 crash window。建议把取消意图及待发送控制事件持久化,后台重复下发,或者让 Worker 定期从受认证接口读取持久意图;Redis 只做加速。不能用失败返回路径存在补偿来证明任意故障下原子。
F6.2|长节点与心跳如何处理取消?
参考回答:当前 emit 边界的检查不能强杀正在等待的外部动作;Redis 读取故障还会继续执行。应给 connector 合理超时和取消传播,并区分“停止调度后续节点”“中止本地等待”“外部服务是否已提交”三件事。共享 Worker 的强杀会影响其他任务,硬停止需要隔离进程与资源回收边界。
证据:mq_worker.py:377-391、433-471、593-603。
F7:把结果事件可靠性从头到尾讲一遍
参考回答: 当前 Worker 以持久消息发布事件,主执行路径记录 terminal_delivered,终态成功发布才 ACK 输入,否则 NACK。Go 校验 schema、允许的事件、大小与 Attempt,先更新 PG,再 XADD、RPUSH、EXPIRE、PUBLISH;普通依赖失败促使结果消息重投,永久无效/过期事件 ACK 丢弃。短期回放与 PG 状态之间没有共同事务,所以应允许重复并能说明缺口。
证据:Worker 发布、ACK/NACK、Go 消费、错误分类。
F7.1|Go 已写终态,Redis 失败,重投会不会被状态机挡掉?
参考回答:同 attempt、同 worker、同完成状态的重复完成会被 CompleteAttempt 接受,因此可以继续补 Redis;但每次 XADD/RPUSH 不带幂等写,部分成功再重试会重复。未知完成状态当前返回协议错误,不会默认成功。需要 durable inbox/eventId 或数据库事件表与投影重建,才能进一步缩小跨存储缺口。这些属于改进方向。
证据:attempt.go:205-208;event/service.go:198-226、329-338;rabbitmq.go:302-328。
F7.1.1|Worker 某个 node_started 发布失败,也会让任务正确失败吗?
参考回答:不能据主函数 try/except 直接保证。当前 gather(return_exceptions=True) 结果没有逐项检查,run_one 只捕获 NodeExecutionError;emit 或 Secret 解析抛其他异常时,按源码推导可能被收集后忽略,节点还被移出 remaining,最后发布 success。应注入 emitter 单次失败来复现,再统一传播基础设施错误,区分“业务执行失败”和“结果发布失败”,不要直接重跑全部外部动作。本次只确认这条源码路径,未运行故障复现。
证据:workflow_engine.py:331-360、446-489、407-415。
F7.2|简历写 Stream 回放和 sequenceId 续传,实际如何实现?
参考回答:实际回放是每执行 Redis List 的 LRANGE;Stream 是另一个全局追加流。SSE 先订阅本地 Hub 后回放,允许重叠重复;输出只有 data,不是 SSE id/Last-Event-ID 游标协议。List 活动期 TTL 30 分钟,完成事件缩短到 5 分钟;过期后只能查 PG 终态/失败信息,不能恢复完整历史结果。所以我会修改简历措辞。
证据:event/service.go:197-226、308-327;SSE。
F7.2.1|有 sequenceId 为什么仍可能卡界面?
参考回答:序号每个 emitter 从 0 开始,且发布前自增;失败、同 attempt 重投、多个消费者处理和慢浏览器都会影响连续性。Hub 缓冲 64 满后静默丢事件;编辑器只按连续序号 drain,未见缺口超时修复。执行详情虽然把 attempt/timestamp/data 纳入去重 key,但排序仍先按 sequence,跨 attempt 也不是统一时间线。应采用稳定 eventId 和执行/attempt 范围的 cursor,检测缺口后补拉或重建快照。
证据:mq_worker.py:192-220;Hub,:46-55;编辑器;详情。这里是源码风险,未做浏览器复现。
F8:多租户权限如何落到执行与事件接口?
参考回答: 以数据库关系解析 execution→workflow→workspace 或 branch→workflow→workspace,查 subject 在这些 scope 的 role/action,而不是信任前端传 workspaceId。执行详情、事件、取消、redrive 各自有相应权限;Secret 查询也限定 workspace。审查要覆盖 REST、SSE、审计、Worker 消息和资源消耗。
F8.1|UUID 不可猜,为什么还要查权限?
参考回答:UUID 是标识,可能从日志、分享、关联记录中获得,不能作为授权凭据。当前代码根据真实 ownership 查授权链。高价值测试是两个租户互换 executionId,验证 detail/events/stream/cancel/redrive 都拒绝,同时保留授权用户正常访问;不需要穷举所有 UI 文本。
F8.1.1|撤销角色后,已连接 SSE 立即断开吗?
参考回答:当前授权发生在连接入口,stream 循环未见定期重新鉴权或收到撤权通知后断开。新请求会重新查权限,现有连接不能宣称即时撤权。可以选择短连接授权租期、订阅权限变化事件,或定期复验;要明确产品对撤权延迟的契约。
证据:server.go:123;event_handlers.go:47-65;access/service.go:157-187。
F8.2|Redis key 有 execution UUID、多了配额,隔离是否就完整?
参考回答:Redis key 只是命名,浏览器权限来自 API;共享 Worker 对消息中 scope 的信任还依赖 broker ACL 和可信生产者。当前 workspace 并发上限是事务内 COUNT 后 INSERT,没有 workspace 锁或原子计数,默认隔离下并发可同时看到余量,因此不能当硬配额。CPU、内存、节点并发和外部访问也需要独立限制。
证据:event/service.go:325-327;execution/service.go:162-175、224;backend/mq_worker.py:473-519。本次没有压测配额并发。
F9:Secret 安全和 Worker 能力要如何客观回答?
参考回答: Go 从图引用中只解析本 workspace 的 Secret,缺失拒绝派发;Outbox 不保存已解析 secret map,发布时才注入。Worker 使用这些值运行节点。这个设计减少重复持久化和错误引用,但不等于完整 Secret 托管:Value 仍明文落库,消息中仍有可用凭据,节点上下文也比单节点最小授权更宽。
证据:Secret hydration、按租户查询、Value 字段。
F9.1|多租户是否必然意味着 Go→Rabbit→Python 不能传明文?
参考回答:不能这样推导。租户隔离讨论 A 能读写 B 的什么;传输安全讨论窃听、TLS、网络和 ACL;静态存储讨论数据库/备份泄露;Worker 信任讨论它是否被允许读取凭据。可信服务必须在某处获得可用凭据,明文的合理性取决于明确的信任部署模型。本次没有检查真实部署 TLS 或 broker ACL,不能把它们当已具备。
F9.1.1|既然做了事件脱敏,凭据不会到前端了吧?
参考回答:不能保证。当前 sanitizer 只处理 inputValues/outputValues;workflow_completed 的 results 和 errorMessage 未覆盖。终态结果可能复用节点原始输出,因此部分字段已脱敏不等于全链路脱敏。建议在唯一事件出口按 schema 覆盖所有可能携带值的字段、限制日志和错误信息,并减少节点获得的凭据范围;字面量替换也无法抵抗编码外泄。
证据:脱敏字段,:58-71;workflow_engine.py:398-414;mq_worker.py:442-450。未使用任何真实密钥做验证。
F9.2|code 已禁用,可以称安全沙箱吗?
参考回答:不能。当前新建执行明确拒绝 code,Worker 默认节点注册表未导入 code;code_node.py 本身仍保留 exec 和线程池能力。这是缩小可执行能力,不是建立沙箱。还应核对历史 outbox/redrive 与 Worker 注册策略一致,评估 SSH、SQL、文件、模板、HTTP 等能力以及资源配额,不把所有 connector 视为低风险。
证据:execution/service.go:192-196;默认节点注册;保留的 code 实现。
F9.2.1|已经有 egress.py,还缺什么?
参考回答:它已有主机解析和私网/回环/metadata 检查,部分 connector 已接入,不能说完全没有防护。但预解析与实际连接若再次解析,仍需处理 DNS 变化;若允许重定向,应在连接前逐跳验证,响应后检查不能撤销访问。还要检查代理、不同 connector 的目标解析和凭据能力。网络出口策略、可信 connector 配置和进程隔离应共同限制能力。
证据:出站校验、HTTP 校验与连接,:172-184。这里是需要核对的安全边界,不代表本次已做 SSRF 利用验证。
最容易失分的回答
错误一:“Outbox + Confirm + messageId 实现 exactly-once。”
纠正:Outbox 保存投递意图;Confirm 确认 broker;messageId 只是标识。确认未知、Worker 崩溃、结果发布失败都可能重做。当前围栏约束控制面状态,外部副作用必须另做幂等或对账。
错误二:“Redis Stream 保证 SSE 从最后一条精确续传。”
纠正:本项目实际 List 全量短期回放,SSE 无 id/Last-Event-ID 协议。先订阅后回放减少窗口,客户端还要处理重复、缺口和 attempt 切换。这句应在投递简历前修正。
错误三:“禁用 code、数据有 workspaceId,所以安全支持不可信租户。”
纠正:需分别证明资源授权、Secret 使用权、消息来源、静态存储、网络与 Worker 资源能力。当前已有防护仍不等于沙箱或完整租户隔离验收。
90 秒口述
“Flostra 的重点是把工作流的业务运行和可靠控制分开。Go 负责权限、版本快照、调度和执行状态,Python 负责节点运行。运行请求把 execution 和 outbox 放进同一 PostgreSQL 事务,后台通过 SKIP LOCKED、租约和退避处理投递,用 RabbitMQ 持久消息和 Confirm 观察 broker 接收。
每次投递有独立 Attempt,结果携带 attemptId 和 workerId。控制面校验当前 Attempt,接收心跳并续租;Watchdog 将失联运行判为超时,避免旧 Worker 的迟到事件复活状态。恢复采用显式 redrive、幂等键和审计,因为不能确定一个失联 Worker 的外部动作有没有完成。
事件通过 Rabbit 回到 Go,保存短期 Redis 回放,并用 SSE 重连重放给前端。这里实际是 List 回放,不是完整的 Stream 游标续传。我能说明 Confirm、状态围栏与副作用幂等各自的边界。当前项目也有并行 DAG、Secret 引用解析和出站检查,但 DAG 汇合语义、事件缺口、节点最小能力和长期结果持久化还要继续完善。”
简历建议措辞
保留三个重点,避免把当前工程扩展全部挤入简历:
- 负责 Go 控制面与 Python Worker 异步执行链路:以 PostgreSQL 事务保存执行与任务快照,通过 RabbitMQ 持久队列投递,结果事件回传更新执行状态。
- 针对数据库提交与消息发布的双写问题,实现 Transactional Outbox、SKIP LOCKED 多实例认领、租约与指数退避,并等待 Publisher Confirm 后记录发布状态;明确至少一次语义。
- 设计 Attempt/Worker 身份校验与 Watchdog,限制旧执行的迟到回写;支持带幂等键和审计的人工 Redrive、协作式取消,以及 Redis 短期回放与 SSE 重连恢复。
“可靠”需要在面试中解释故障假设与剩余窗口。未修复/复现 DAG 风险前,不建议新增“完善的并行 DAG 调度”卖点;没有个人验收记录时,不添加压测吞吐、线上可用率或“所有故障均验证”的描述。
证据与复习清单
| 主题 | 关键位置(相对 E:/Proj/Flostra) | 建议自证场景 |
|---|---|---|
| 快照与事务 | gback/internal/execution/service.go:152-229 | 分支修改后原任务图不变;事务第二次写失败整笔回滚 |
| 认领与重发 | gback/internal/execution/outbox.go:169-243、293-362 | 两个 Dispatcher;Confirm 后数据库提交前崩溃 |
| Confirm/返回 | gback/internal/platform/rabbitmq/rabbitmq.go:187-241 | 队列删除、不可路由、超时晚确认 |
| 状态围栏 | gback/internal/execution/attempt.go:83-245;watchdog.go:95-178 | 旧 attempt 完成;续租与超时竞争;未 started 排队超时 |
| Redrive | gback/internal/execution/redrive.go:151-263 | 相同 key 并发;新 key 遇到活动 execution;审计历史 |
| 结果可靠性 | backend/mq_worker.py:195-220、334-471;gback/internal/event/service.go:156-230 | 节点事件发布失败、终态发布失败、PG 成功 Redis 失败 |
| 回放 | gback/internal/httpapi/event_handlers.go:28-70;event/service.go:308-327;frontend/src/workflow/hooks/useWorkflowController.ts:165-191 | 慢订阅者、序号缺口、重投从 0、TTL 到期 |
| DAG | backend/workflow_engine.py:238-267、310-360;tests/test_engine_scheduling.py:39-119 | 不等深汇合、常量加依赖、emit 抛异常 |
| 安全 | gback/internal/access/service.go:118-187;backend/security/event_safety.py:58-71;security/egress.py:98-155 | 跨租户 ID、撤权连接、终态脱敏、解析与连接一致性 |
本次验证层级是当前源码与既有测试审阅。没有执行 go test、pytest、race、Compose、故障注入、浏览器和真实跨租户请求;没有检查真实环境密钥、TLS、broker ACL 或线上容量。因此,文中的源码风险不能包装成已复现线上故障,已有测试文件也不能包装成本次测试通过。
复习优先级:先讲清 Outbox/Confirm/Attempt 的承诺边界,再修正 SSE 简历措辞;随后自行复现 DAG 汇合、事件异常吞掉与序号缺口,最后核对 Secret 出口和权限撤销。README 中关于真实 code 节点验收的旧说明,与当前禁用 code 的路径需要重新对齐,不能直接当当前版本验收证据。