Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions lua/opencode/config_file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ local M = {
M.get_opencode_config = Promise.async(function()
if not M.config_promise then
local state = require('opencode.state')
M.config_promise = state.api_client:get_config()
M.config_promise = Promise.retry(function()
return state.api_client:get_config()
end, 3, 500)
end
local ok, result = pcall(function()
return M.config_promise:await()
end)

if not ok then
M.config_promise = nil
vim.notify('Error fetching Opencode config: ' .. vim.inspect(result), vim.log.levels.ERROR)
return nil
end
Expand All @@ -28,12 +31,15 @@ end)
M.get_opencode_project = Promise.async(function()
if not M.project_promise then
local state = require('opencode.state')
M.project_promise = state.api_client:get_current_project()
M.project_promise = Promise.retry(function()
return state.api_client:get_current_project()
end, 3, 500)
end
local ok, result = pcall(function()
return M.project_promise:await()
end)
if not ok then
M.project_promise = nil
vim.notify('Error fetching Opencode project: ' .. vim.inspect(result), vim.log.levels.ERROR)
return nil
end
Expand Down Expand Up @@ -77,6 +83,7 @@ function M.get_opencode_providers()
end
local wrapped = M.providers_promise:catch(function(err)
vim.notify('Error fetching Opencode providers: ' .. vim.inspect(err), vim.log.levels.ERROR)
M.providers_promise = nil
return nil
end)
if not _providers_render_callback then
Expand Down
2 changes: 0 additions & 2 deletions lua/opencode/git_review.lua
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,7 @@ M.revert_selected_file = require_git_project(function(ref)
end)

M.revert_all = require_git_project(function(ref)
vim.print('⭕ ❱ git_review.lua:288 ❱ ƒ(anonymous) ❱ ref =', ref)
M.__current_ref = ref or M.get_first_snapshot()
vim.print('⭕ ❱ git_review.lua:289 ❱ ƒ(M.__current_ref) ❱ M.__current_ref =', M.__current_ref)

local files = get_changed_files()

Expand Down
31 changes: 31 additions & 0 deletions lua/opencode/opencode_server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,37 @@ function OpencodeServer:is_running()
return self.job.pid ~= nil
end

---Perform a health check on a server URL.
---@param url string The full health endpoint URL
---@param timeout_ms number Timeout in milliseconds
---@return Promise<boolean>
function OpencodeServer.health_check(url, timeout_ms)
local health_promise = Promise.new()
curl.request({
url = url,
method = 'GET',
timeout = timeout_ms or 2000,
proxy = '',
callback = function(response)
health_promise:resolve(response ~= nil and response.status >= 200 and response.status < 300)
end,
on_error = function(_err)
health_promise:resolve(false)
end,
})
return health_promise
end

---Check if the server is reachable via its health endpoint.
---@return Promise<boolean>
function OpencodeServer:check_health()
if not self.url then
return Promise.new():resolve(false)
end
local health_url = self.url:gsub('/$', '') .. '/global/health'
return OpencodeServer.health_check(health_url, 2000)
end

local function kill_process(pid, signal, desc)
local log = require('opencode.log')
local ok, err = pcall(vim.uv.kill, pid, signal)
Expand Down
36 changes: 36 additions & 0 deletions lua/opencode/promise.lua
Original file line number Diff line number Diff line change
Expand Up @@ -418,4 +418,40 @@ function Promise.all(promises)
end)
end

---Create a promise that resolves after a delay.
---@param ms number Delay in milliseconds
---@return Promise<boolean>
function Promise.delay(ms)
local delay = Promise.new()
vim.defer_fn(function()
pcall(delay.resolve, delay, true)
end, ms)
return delay
end

---Retry a promise-returning operation on failure.
---@generic T
---@param factory fun(): Promise<T> Creates a fresh promise per attempt
---@param max_retries number Total attempts (1 = no retry)
---@param delay_ms number Delay between retries in milliseconds
---@return Promise<T>
function Promise.retry(factory, max_retries, delay_ms)
return Promise.spawn(function()
local last_err
for i = 1, max_retries do
local ok, result = pcall(function()
return factory():await()
end)
if ok then
return result
end
last_err = result
if i < max_retries then
Promise.delay(delay_ms):await()
end
end
return Promise.new():reject(last_err)
end)
end

return Promise
86 changes: 39 additions & 47 deletions lua/opencode/server_job.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,20 @@ end
--- @param timeout number
--- @return Promise<string|nil>
local function try_custom_server(base_url, timeout)
local promise = Promise.new()
local health_url = base_url .. '/global/health'

log.debug('try_custom_server: checking health at %s', health_url)

curl.request({
url = health_url,
method = 'GET',
timeout = timeout * 1000,
proxy = '', -- Disable proxy for health check
callback = function(response)
if response and response.status >= 200 and response.status < 300 then
local success, health_data = pcall(vim.json.decode, response.body)
if success and health_data then
log.debug('try_custom_server: health check passed')
promise:resolve(base_url)
return
end
end

local err_msg =
string.format('Health check failed at %s (status: %d)', health_url, response and response.status or 0)
log.debug('try_custom_server: %s', err_msg)
promise:reject(err_msg)
end,
on_error = function(err)
log.debug('try_custom_server: error connecting to %s: %s', health_url, vim.inspect(err))
promise:reject(err)
end,
})
return opencode_server.health_check(health_url, timeout * 1000):and_then(function(healthy)
if healthy then
log.debug('try_custom_server: health check passed')
return base_url
end

return promise
local err_msg = string.format('Health check failed at %s', health_url)
log.debug('try_custom_server: %s', err_msg)
return Promise.new():reject(err_msg)
end)
end

--- @param response {status: integer, body: string}
Expand Down Expand Up @@ -185,15 +167,9 @@ local function resolve_port()
return existing or math.random(1024, 65535)
end

--- Ensure the opencode server is running, starting it if necessary.
--- @return Promise<OpencodeServer>
function M.ensure_server()
local function _start_server()
local promise = Promise.new()

if state.opencode_server and state.opencode_server:is_running() then
return promise:resolve(state.opencode_server)
end

local custom_url = config.server.url
if not custom_url then
log.debug('ensure_server: server.url not configured, spawning local server')
Expand All @@ -218,20 +194,36 @@ function M.ensure_server()
return promise
end

local function retry_connect(base_url, timeout, max_retries, on_success, on_failure)
local function attempt(retry_count)
vim.defer_fn(function()
try_custom_server(base_url, timeout):and_then(on_success):catch(function(err)
if retry_count < max_retries then
attempt(retry_count + 1)
else
log.error('try_connect_to_custom_server: exhausted %d retries: %s', max_retries, vim.inspect(err))
on_failure(err)
end
end)
end, retry_count * (config.server.retry_delay or 2000))
--- Ensure the opencode server is running, starting it if necessary.
--- @return Promise<OpencodeServer>
function M.ensure_server()
if state.opencode_server and state.opencode_server:is_running() then
return state.opencode_server:check_health():and_then(function(healthy)
if healthy then
return state.opencode_server
end
log.warn('ensure_server: cached server unhealthy, reconnecting')
state.jobs.clear_server()
return _start_server()
end)
end
attempt(1)

return _start_server()
end

local function retry_connect(base_url, timeout, max_retries, on_success, on_failure)
local delay = config.server.retry_delay or 2000
Promise.delay(delay)
:and_then(function()
return Promise.retry(function()
return try_custom_server(base_url, timeout)
end, max_retries, delay)
end)
:and_then(on_success)
:catch(function(err)
log.error('try_connect_to_custom_server: exhausted %d retries: %s', max_retries, vim.inspect(err))
on_failure(err)
end)
end

local function spawn_and_retry(base_url, custom_port, custom_url, promise, timeout)
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/persist_state_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ describe('persist_state', function()
is_running = function()
return true
end,
check_health = function()
return Promise.new():resolve(true)
end,
spawn = function() end,
shutdown = function()
return Promise.new():resolve(true)
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/server_job_spec.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local server_job = require('opencode.server_job')
local Promise = require('opencode.promise')

local curl = require('opencode.curl')
local assert = require('luassert')
Expand Down Expand Up @@ -93,6 +94,9 @@ describe('server_job', function()
end)
end,
shutdown = function() end,
check_health = function()
return Promise.new():resolve(true)
end,
}
opencode_server.new = function()
return fake
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/services_session_runtime_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ describe('opencode.services.session_runtime', function()
end,
shutdown = function() end,
url = 'http://127.0.0.1:4000',
check_health = function()
return Promise.new():resolve(true)
end,
})
end)

Expand Down
Loading