Skip to content

fix: resolve server startup hang on macOS ARM64 (Apple Silicon)#3902

Open
beats-dh wants to merge 3 commits intomainfrom
claude/debug-macos-server-hang-iWFbB
Open

fix: resolve server startup hang on macOS ARM64 (Apple Silicon)#3902
beats-dh wants to merge 3 commits intomainfrom
claude/debug-macos-server-hang-iWFbB

Conversation

@beats-dh
Copy link
Copy Markdown
Contributor

@beats-dh beats-dh commented Mar 31, 2026

LuaJIT runs in interpreter-only mode on macOS ARM64 (JIT disabled).
The require() function's package.searchpath iterates through every
entry in package.path doing string.sub operations for each call.
With 50+ nested require calls during quest catalog loading, the
cumulative cost causes the server to hang for 10+ minutes at startup.

Three changes to fix this:

  • Set minimal package.path in C++ after luaL_openlibs to strip
    system/env LUA_PATH entries; extend it in core.lua with only the
    needed libs directory
  • Replace require() with dofile()/loadfile() for the quest loading
    chain, eliminating package.searchpath overhead entirely
  • Remove the custom package.searchers hook that was needed for the
    require-based approach

Summary by CodeRabbit

  • Refactor

    • Simplified and streamlined quest loading and catalog initialization to improve startup reliability and path resolution.
    • Adjusted runtime module search behavior to use explicit lookup paths, reducing ambiguous module resolution and redundant loader logic.
  • Chores

    • Updated build baseline reference for dependency tooling.

claude and others added 2 commits March 31, 2026 12:10
LuaJIT runs in interpreter-only mode on macOS ARM64 (JIT disabled).
The require() function's package.searchpath iterates through every
entry in package.path doing string.sub operations for each call.
With 50+ nested require calls during quest catalog loading, the
cumulative cost causes the server to hang for 10+ minutes at startup.

Three changes to fix this:
- Set minimal package.path in C++ after luaL_openlibs to strip
  system/env LUA_PATH entries; extend it in core.lua with only the
  needed libs directory
- Replace require() with dofile()/loadfile() for the quest loading
  chain, eliminating package.searchpath overhead entirely
- Remove the custom package.searchers hook that was needed for the
  require-based approach

https://claude.ai/code/session_01HUV9qCDaWCyvugmnzL9Qk1
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8194e3f-5b81-49fd-836a-b61802b204b7

📥 Commits

Reviewing files that changed from the base of the PR and between 23989dd and c217854.

📒 Files selected for processing (1)
  • vcpkg.json
✅ Files skipped from review due to trivial changes (1)
  • vcpkg.json

📝 Walkthrough

Walkthrough

Refactors quest loading to use direct file execution (dofile/loadfile) instead of Lua's require(), adds explicit catalogDirectory paths, mutates package.path/package.cpath handling, and removes a custom package.searchers-based catalog loader, with no public API surface changes.

Changes

Cohort / File(s) Summary
Top-level loaders
data-canary/lib/core/load.lua, data-otservbr-global/lib/core/load.lua
Replace require("...quests") with dofile(DATA_DIRECTORY .. "/lib/core/quests.lua") to load quests via direct script execution.
Quest entry modules
data-canary/lib/core/quests.lua, data-otservbr-global/lib/core/quests.lua, data/libs/functions/quests.lua
Switch loader invocation from require(...loader).load(DATA_DIRECTORY) to dofile(CORE_DIRECTORY .. "/lib/core/quests/loader.lua").load(DATA_DIRECTORY) (or equivalent dofile of quests.lua).
Catalog init changes
data-canary/lib/core/quests/catalog/init.lua, data-otservbr-global/lib/core/quests/catalog/init.lua
Compute path separator and pass an explicit catalogDirectory (DATA_DIRECTORY + path) as a third argument to catalog.build(...).
Catalog build logic
data/lib/core/quests/catalog.lua
Extend build(namespace, questModules)build(namespace, questModules, catalogDirectory); when catalogDirectory is provided, load quest modules with loadfile (and execute) instead of require.
Quest loader simplification
data/lib/core/quests/loader.lua
Remove custom package.searchers injector and package.loaded trick; use global _G._questCatalogNamespace to guard loads and dofile(initPath) to load catalog init.lua.
Package path / Lua host changes
data/core.lua, src/lua/functions/lua_functions_loader.cpp
Mutate package.path to include CORE_DIRECTORY libs patterns; host side sets package.path to ./?.lua;./?/init.lua and clears package.cpath.
Tooling metadata
vcpkg.json
Update builtin-baseline commit hash for vcpkg baseline.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • dudantas
  • majestyotbr

Poem

🐰
I hopped along the package trail,
swapped tangled requires for a simpler tale.
Loadfiles hum and dofiles sing,
catalogs open — hop, spring!
My warren runs light on every trail. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: resolve server startup hang on macOS ARM64 (Apple Silicon)' directly describes the primary issue being fixed and the target platform, aligning with the changeset's main purpose of eliminating package.searchpath overhead during quest loading on macOS ARM64.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/debug-macos-server-hang-iWFbB

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
data/core.lua (1)

9-11: Make package.path extension idempotent.

Line 10 always appends, so reloading core.lua duplicates entries and grows package.path.

Diff suggestion
 local sep = package.config:sub(1, 1)
-package.path = package.path .. ";" .. CORE_DIRECTORY .. sep .. "libs" .. sep .. "?.lua" .. ";" .. CORE_DIRECTORY .. sep .. "libs" .. sep .. "?" .. sep .. "init.lua"
+local libsLua = CORE_DIRECTORY .. sep .. "libs" .. sep .. "?.lua"
+local libsInit = CORE_DIRECTORY .. sep .. "libs" .. sep .. "?" .. sep .. "init.lua"
+if not package.path:find(libsLua, 1, true) then
+	package.path = table.concat({ package.path, libsLua, libsInit }, ";")
+end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@data/core.lua` around lines 9 - 11, The code unconditionally appends entries
to package.path causing duplicates on reload; update core.lua to make the
extension idempotent by computing the two candidate entries using CORE_DIRECTORY
and sep (the same values used now) and only append each entry to package.path if
package.path:find(entry, 1, true) is nil; adjust the logic around
sep/CORE_DIRECTORY/package.path so package.path is modified only when those
exact entries are missing.
data/lib/core/quests/catalog.lua (1)

36-45: loadfile approach with fallback is well-implemented.

Good error handling for loadfile failures. The fallback to require when catalogDirectory is falsy maintains backward compatibility.

One optional consideration: if loader() throws a runtime error during chunk execution (line 42), the error message won't include the module name context. This is minor since the stack trace will point to the file, but you could wrap it for consistency with the loadfile error format:

♻️ Optional: Add context to runtime errors
 			if not loader then
 				error(string.format("Quest module %s failed to load: %s", moduleName, errMsg))
 			end
-			quest = loader()
+			local ok, result = pcall(loader)
+			if not ok then
+				error(string.format("Quest module %s failed to execute: %s", moduleName, result))
+			end
+			quest = result
 		else
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@data/lib/core/quests/catalog.lua` around lines 36 - 45, The runtime
invocation of loader() can raise an error without module context; wrap the chunk
execution (the call to loader() that assigns quest) in a protected call (e.g.,
pcall or xpcall) to catch runtime errors and rethrow or error with a formatted
message that includes moduleName (same style as the existing loadfile error) and
optionally the traceback; update the branch where loader is used so any error
from loader() is reported as "Quest module <moduleName> failed to execute:
<err>" while still returning/assigning quest on success.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@data/core.lua`:
- Around line 9-11: The code unconditionally appends entries to package.path
causing duplicates on reload; update core.lua to make the extension idempotent
by computing the two candidate entries using CORE_DIRECTORY and sep (the same
values used now) and only append each entry to package.path if
package.path:find(entry, 1, true) is nil; adjust the logic around
sep/CORE_DIRECTORY/package.path so package.path is modified only when those
exact entries are missing.

In `@data/lib/core/quests/catalog.lua`:
- Around line 36-45: The runtime invocation of loader() can raise an error
without module context; wrap the chunk execution (the call to loader() that
assigns quest) in a protected call (e.g., pcall or xpcall) to catch runtime
errors and rethrow or error with a formatted message that includes moduleName
(same style as the existing loadfile error) and optionally the traceback; update
the branch where loader is used so any error from loader() is reported as "Quest
module <moduleName> failed to execute: <err>" while still returning/assigning
quest on success.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1723fe80-b6f3-4bee-87d5-cc77b772cc26

📥 Commits

Reviewing files that changed from the base of the PR and between 0bdc790 and 23989dd.

📒 Files selected for processing (11)
  • data-canary/lib/core/load.lua
  • data-canary/lib/core/quests.lua
  • data-canary/lib/core/quests/catalog/init.lua
  • data-otservbr-global/lib/core/load.lua
  • data-otservbr-global/lib/core/quests.lua
  • data-otservbr-global/lib/core/quests/catalog/init.lua
  • data/core.lua
  • data/lib/core/quests/catalog.lua
  • data/lib/core/quests/loader.lua
  • data/libs/functions/quests.lua
  • src/lua/functions/lua_functions_loader.cpp

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

✅ Updated vcpkg baseline to 2026.03.18 (c3867e714dd3a51c272826eea77267876517ed99)

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 1, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants