oracle.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. package dualrun
  2. import (
  3. "bytes"
  4. "context"
  5. "embed"
  6. "encoding/json"
  7. "fmt"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "strconv"
  12. "time"
  13. )
  14. //go:embed oracle_probe.php
  15. var oracleAssets embed.FS
  16. // OracleRunner 负责"起一个 PHP 进程、真跑一次结课、把观测值 JSON 带回来"。
  17. //
  18. // 为什么用子进程而不是同进程调用:
  19. // - legacy 是 PHP 应用,无法在 Go 进程内调用;
  20. // - **每次进程 = 每次新连接**,天然满足"每个 case 新连接"这条硬约束
  21. // (Q5:MEMORY 引擎临时表的行不参与回滚,靠 ROLLBACK 清不掉);
  22. // - 进程隔离后,PHP 侧的 notice/异常不会污染 Go 侧状态。
  23. type OracleRunner struct {
  24. cfg Config
  25. // probePath 是内嵌探针释放到磁盘后的路径(每轮 runner 一个,避免并发写)。
  26. probePath string
  27. }
  28. // NewOracleRunner 把内嵌的 PHP 探针释放到临时目录。
  29. func NewOracleRunner(cfg Config) (*OracleRunner, error) {
  30. dir, err := os.MkdirTemp("", "ac15-oracle-*")
  31. if err != nil {
  32. return nil, err
  33. }
  34. src, err := oracleAssets.ReadFile("oracle_probe.php")
  35. if err != nil {
  36. return nil, err
  37. }
  38. path := filepath.Join(dir, "oracle_probe.php")
  39. if err := os.WriteFile(path, src, 0o600); err != nil {
  40. return nil, err
  41. }
  42. return &OracleRunner{cfg: cfg, probePath: path}, nil
  43. }
  44. // Close 清理释放的探针。
  45. func (o *OracleRunner) Close() {
  46. if o.probePath != "" {
  47. _ = os.RemoveAll(filepath.Dir(o.probePath))
  48. }
  49. }
  50. // Run 执行一条 case 的 oracle 侧观测。
  51. //
  52. // 入参:ctx;c 待跑用例;frozenAt 冻结点(会被截断到整秒)。
  53. // 出参:OracleResult(含路径证据与"异常前写入值");error 仅表示"进程/解析层面失败"。
  54. func (o *OracleRunner) Run(ctx context.Context, c Case, frozenAt time.Time) (*OracleResult, error) {
  55. mut, err := json.Marshal(c.Mutations)
  56. if err != nil {
  57. return nil, err
  58. }
  59. cmd := exec.CommandContext(ctx, o.cfg.PHPBinary,
  60. "-d", "date.timezone=Asia/Shanghai",
  61. "-d", "opcache.enable_cli=0",
  62. "-d", "display_errors=stderr",
  63. "-d", "memory_limit=1024M",
  64. o.probePath,
  65. )
  66. cmd.Env = append(os.Environ(),
  67. "AC15_LEGACY_REPO="+o.cfg.LegacyRepo,
  68. "AC15_DB_HOST="+o.cfg.Host,
  69. "AC15_DB_PORT="+strconv.Itoa(o.cfg.Port),
  70. "AC15_DB_NAME="+o.cfg.LegacyDBName,
  71. "AC15_DB_USER="+o.cfg.User,
  72. "AC15_DB_PASS="+o.cfg.Pass,
  73. "AC15_RECORD_ID="+c.RecordID,
  74. "AC15_IS_SUPPLEMENT="+boolEnv(c.IsSupplement),
  75. "AC15_FROZEN_EPOCH="+strconv.FormatInt(TruncateToSecond(frozenAt).Unix(), 10),
  76. "AC15_MUTATIONS="+string(mut),
  77. )
  78. var stdout, stderr bytes.Buffer
  79. cmd.Stdout = &stdout
  80. cmd.Stderr = &stderr
  81. if err := cmd.Run(); err != nil {
  82. return nil, fmt.Errorf("php 进程失败:%v;stderr=%s", err, truncate(stderr.String(), 800))
  83. }
  84. raw := bytes.TrimSpace(stdout.Bytes())
  85. // 探针以 ##AC15_JSON_BEGIN## 标记 JSON 起点;老输出没有标记时回退到首个 '{'。
  86. // (不能用"最后一个 '{'":JSON 内部有嵌套对象,会切在中间)
  87. const marker = "##AC15_JSON_BEGIN##"
  88. if i := bytes.Index(raw, []byte(marker)); i >= 0 {
  89. raw = bytes.TrimSpace(raw[i+len(marker):])
  90. } else if i := bytes.IndexByte(raw, '{'); i > 0 {
  91. raw = raw[i:]
  92. }
  93. var res OracleResult
  94. if err := json.Unmarshal(raw, &res); err != nil {
  95. return nil, fmt.Errorf("解析探针输出失败:%v;原始输出=%s", err, truncate(string(raw), 800))
  96. }
  97. if !res.OK {
  98. return &res, fmt.Errorf("探针报错 %s: %s", res.Error, res.ErrorDetail)
  99. }
  100. return &res, nil
  101. }
  102. func boolEnv(b bool) string {
  103. if b {
  104. return "1"
  105. }
  106. return "0"
  107. }
  108. func truncate(s string, n int) string {
  109. if len(s) <= n {
  110. return s
  111. }
  112. return s[:n] + "…"
  113. }