sharing_reconcile_recovery_worker.php 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. <?php
  2. // Isolated SQLite crash worker. Never boots application providers or loads .env.
  3. require dirname(__DIR__) . '/bootstrap-agent.php';
  4. use App\Services\Sharing\ShareSettlementService;
  5. use Carbon\Carbon;
  6. use Illuminate\Config\Repository;
  7. use Illuminate\Database\Capsule\Manager;
  8. use Illuminate\Foundation\Application;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Facade;
  11. $database=$argv[1] ?? '';
  12. $date=$argv[2] ?? '';
  13. $point=$argv[3] ?? '';
  14. $directory=realpath(dirname($database));
  15. if (!$directory || dirname($directory)!==realpath(sys_get_temp_dir())
  16. || !preg_match('/^bot28-share-reconcile-[a-f0-9]{12}$/D',basename($directory))
  17. || realpath($database)!==$directory.'/db.sqlite'
  18. || !in_array($point,['before','after'],true) || !preg_match('/^\d{4}-\d{2}-\d{2}$/D',$date)
  19. || !function_exists('posix_kill') || !defined('SIGKILL')) {
  20. throw new RuntimeException('A dedicated temporary recovery database and POSIX signals are required');
  21. }
  22. Carbon::setTestNow(Carbon::parse('2026-10-27 12:00:00','Asia/Shanghai'));
  23. $app=new Application(dirname(__DIR__,2));
  24. $app->instance('config',new Repository(['app'=>['timezone'=>'Asia/Shanghai'],'third_game'=>['currency'=>'CNY']]));
  25. Facade::setFacadeApplication($app);
  26. $db=new Manager($app); $db->addConnection(['driver'=>'sqlite','database'=>$database,'prefix'=>'bot_']);
  27. $db->setAsGlobal(); $db->bootEloquent(); $app->instance('db',$db->getDatabaseManager());
  28. $service=new class($date,$point,$directory.'/interrupted.json') extends ShareSettlementService {
  29. public function __construct(private string $date,private string $point,private string $marker) {}
  30. public function settle(string $date,bool $write=true,bool $allowToday=false): array
  31. {
  32. if ($date===$this->date && $this->point==='before') $this->interrupt($date);
  33. $result=parent::settle($date,$write,$allowToday);
  34. if ($date===$this->date && $this->point==='after') $this->interrupt($date);
  35. return $result;
  36. }
  37. private function interrupt(string $date): void
  38. {
  39. file_put_contents($this->marker,json_encode(['date'=>$date,'point'=>$this->point,
  40. 'pending'=>json_decode(DB::table('share_system_state')->where('key','facts_dirty_dates')->value('value'),true)],JSON_THROW_ON_ERROR));
  41. if (!posix_kill(getmypid(),SIGKILL)) throw new RuntimeException('Could not terminate the recovery worker');
  42. // Let the signal terminate us; a normal exit would not exercise crash recovery.
  43. while (true) usleep(1000);
  44. }
  45. };
  46. $service->reconcile();
  47. throw new RuntimeException('The expected interruption point was not reached');