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
9 changes: 8 additions & 1 deletion src/clice.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ struct WorkerOptions {
required = false)
<std::uint64_t> memory_limit;

DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--max-documents", "--max-documents="},
help = "Max compiled documents kept before LRU eviction (stateful worker only)",
required = false)
<std::uint64_t> max_documents;

DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--worker-name", "--worker-name="},
required = false)
Expand Down Expand Up @@ -129,7 +135,8 @@ int main(int argc, const char** argv) {
auto log_dir = opts.log_dir.value_or("");
if(opts.stateful) {
auto limit = opts.memory_limit.value_or(4ULL * 1024 * 1024 * 1024);
exit_code = clice::run_stateful_worker_mode(limit, name, log_dir);
auto max_docs = opts.max_documents.value_or(clice::default_max_documents);
exit_code = clice::run_stateful_worker_mode(limit, name, log_dir, max_docs);
} else {
exit_code = clice::run_stateless_worker_mode(name, log_dir);
}
Expand Down
99 changes: 86 additions & 13 deletions src/server/compiler/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,31 @@ std::string uri_to_path(const std::string& uri) {
return uri;
}

/// The pch_ref write license: a round may (re)write the session's PCH
/// reference only while BOTH staleness tokens still hold their takeoff
/// values. A supersede bumps generation; a Lost-type invalidation (disk or
/// CDB change behind an in-flight round) bumps only dirty_epoch — either
/// way the round's resolved directory/arguments may describe a command
/// that no longer exists, and writing its PCH key back would hand later
/// incomplete-preamble edits a stale-flag PCH.
static bool may_write_pch_ref(const Session& session,
std::uint64_t launch_generation,
std::uint64_t launch_epoch) {
return session.generation == launch_generation && session.dirty_epoch == launch_epoch;
}

kota::task<bool> Compiler::ensure_pch(Session& session,
std::uint64_t launch_generation,
std::uint64_t launch_epoch,
const std::string& directory,
const std::vector<std::string>& arguments) {
// A round invalidated during the caller's earlier awaits (module
// dependencies) must not touch pch_ref at all: the reset and cache-hit
// branches below write it before the first suspension point.
if(!may_write_pch_ref(session, launch_generation, launch_epoch)) {
co_return false;
}

auto path_id = session.path_id;
auto path = workspace.path_pool.resolve(path_id);
auto& text = session.text;
Expand Down Expand Up @@ -364,6 +386,12 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
if(auto it = workspace.pch_cache.find(pch_key);
it != workspace.pch_cache.end() && it->second.building) {
co_await it->second.building->wait();
// Guard the pch_ref write below against an invalidated round's
// continuation: a newer round (or a context switch) may have
// established the session's PCH identity while we waited.
if(!may_write_pch_ref(session, launch_generation, launch_epoch)) {
co_return false;
}
if(auto it2 = workspace.pch_cache.find(pch_key);
it2 != workspace.pch_cache.end() && !it2->second.path.empty()) {
session.pch_ref = Session::PCHRef{pch_key, it2->second.bound};
Expand Down Expand Up @@ -430,20 +458,27 @@ kota::task<bool> Compiler::ensure_pch(Session& session,
st.inactive_regions = std::move(result.value().inactive_regions);
st.open_conditionals = std::move(result.value().open_conditionals);

session.pch_ref = Session::PCHRef{pch_key, bound};

LOG_INFO("PCH built for {}: {}", path, st.path);

// Persist cache metadata after successful build.
workspace.save_cache(contexts);

// The cache entry above is content-keyed and correct regardless; only
// the session pointer must not be written by an invalidated round.
if(!may_write_pch_ref(session, launch_generation, launch_epoch)) {
co_return false;
}
session.pch_ref = Session::PCHRef{pch_key, bound};

co_return true;
}

/// Compile module dependencies, build/reuse PCH, and fill PCM paths.
/// Shared preparation step used by both ensure_compiled() (stateful path)
/// and forward_stateless() (completion/signatureHelp path).
kota::task<bool> Compiler::ensure_deps(Session& session,
std::uint64_t launch_generation,
std::uint64_t launch_epoch,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
Expand Down Expand Up @@ -540,7 +575,8 @@ kota::task<bool> Compiler::ensure_deps(Session& session,
}

// Build or reuse PCH.
auto pch_ok = co_await ensure_pch(session, directory, arguments);
auto pch_ok =
co_await ensure_pch(session, launch_generation, launch_epoch, directory, arguments);
if(pch_ok && session.pch_ref.has_value()) {
if(auto pch_it = workspace.pch_cache.find(session.pch_ref->key);
pch_it != workspace.pch_cache.end()) {
Expand Down Expand Up @@ -589,6 +625,11 @@ kota::task<> Compiler::run_compile(std::shared_ptr<Session> session) {
auto pc = session->compiling;
auto pid = session->path_id;
auto gen = session->generation;
// Takeoff snapshot for the conditional dirty-flag clear on landing
// (see Session::settle_compile). The generation checks below answer
// "is the buffer still the same buffer"; this answers "did the world
// get dirty again while we were flying".
auto epoch = session->dirty_epoch;

auto finish_compile = [&]() {
if(session->compiling == pc) {
Expand Down Expand Up @@ -628,7 +669,17 @@ kota::task<> Compiler::run_compile(std::shared_ptr<Session> session) {
}
contexts.append_suffix_include(*session, params.text);

// Whether this round is the self-containment probe: a header
// deliberately compiled without its includer prefix to see if it
// stands alone. Decided here, where resolve_command chose to omit
// the prefix; the landing gates what the probe may write.
bool trial_round = attempt == 0 && !session->trial_done && header_context &&
header_context->preamble_path.empty() &&
contexts.header_mode(file_path, pid) == HeaderMode::Unknown;

bool deps_ok = co_await ensure_deps(*session,
gen,
epoch,
params.directory,
params.arguments,
params.pch,
Expand Down Expand Up @@ -700,17 +751,26 @@ kota::task<> Compiler::run_compile(std::shared_ptr<Session> session) {
co_return;
}

// A probe invalidated mid-flight is discarded whole: its verdict is
// a conditional write like the dirty flag (dispatch reset trial_done
// and the header mode for the recompile to re-earn), and its
// diagnostics come from a compile deliberately run without includer
// context — they are never published, including on this path.
// ast_dirty is still set, so the next request re-runs the trial.
if(trial_round && session->dirty_epoch != epoch) {
LOG_INFO("Discarding invalidated self-containment probe for {}", uri_str);
finish_compile();
co_return;
}

// Self-containment trial verdict. Scored once per settled input
// state: trial_done is reset whenever compile inputs change for
// reasons other than buffer edits, so a dependency change re-runs
// the trial while ordinary typing never does. Only NeedsContext is
// persisted — SelfContained is recorded in memory alone (dependency
// changes erase it) so queryContext can dedup identical-flag hosts
// once the verdict is actually earned, never on a guess.
auto* trial_context = contexts.header_context(pid);
if(attempt == 0 && !session->trial_done && trial_context &&
trial_context->preamble_path.empty() &&
contexts.header_mode(file_path, pid) == HeaderMode::Unknown) {
if(trial_round) {
std::vector<protocol::Diagnostic> diagnostics;
if(!result.value().diagnostics.empty()) {
[[maybe_unused]] auto status =
Expand All @@ -729,7 +789,11 @@ kota::task<> Compiler::run_compile(std::shared_ptr<Session> session) {
}
}

session->ast_dirty = false;
// Conditional write: if an invalidation landed mid-flight (a header
// was saved, the document was evicted, ...) this product describes
// a stale world — record it, publish it (bounded staleness), but do
// not declare it fresh; the next request recompiles.
session->settle_compile(epoch);
Comment thread
16bit-ykiko marked this conversation as resolved.
pc->succeeded = true;
record_deps(*session, result.value().deps);

Expand Down Expand Up @@ -802,10 +866,14 @@ kota::task<bool> Compiler::ensure_compiled(std::shared_ptr<Session> session) {
if(!is_stale(*session)) {
co_return true;
}
// Dependency change, not a buffer edit: re-run the trial too.
session->ast_dirty = true;
session->trial_done = false;
contexts.forget_self_contained(path_id);
// A dependency changed on disk behind this session's back — the
// lazy twin of the file tracker's DiskChanged. Route it through
// the event pipeline (synchronous) so both share one cascade; for
// an open file that dispatch marks the AST dirty, resets the trial
// and bumps dirty_epoch. The dispatch re-resolves the session by
// path_id; no suspension separates it from this frame, so it finds
// the same open session this coroutine holds.
on_stale(path_id);
}

// If an up-to-date compile is already in flight, wait for it.
Expand Down Expand Up @@ -947,6 +1015,11 @@ Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
auto path_id = session->path_id;
auto path = std::string(workspace.path_pool.resolve(path_id));
auto gen = session->generation;
// Takeoff snapshot for the pch_ref write license (see
// may_write_pch_ref): this request runs concurrently with compiles and
// holds no compiling token, so it is the easiest continuation to come
// back stale after a disk/CDB change.
auto epoch = session->dirty_epoch;

worker::BuildParams wp;
wp.priority = worker::Priority::High;
Expand All @@ -958,7 +1031,7 @@ Compiler::RawResult Compiler::forward_build(worker::BuildKind kind,
contexts.append_suffix_include(*session, wp.text);

ScopedTimer timer;
if(!co_await ensure_deps(*session, wp.directory, wp.arguments, wp.pch, wp.pcms)) {
if(!co_await ensure_deps(*session, gen, epoch, wp.directory, wp.arguments, wp.pch, wp.pcms)) {
LOG_WARN("forward_build: dependency preparation failed for {}", path);
co_return kota::outcome_error(kota::ipc::Error{"Dependency preparation failed"});
}
Expand Down
29 changes: 29 additions & 0 deletions src/server/compiler/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@

namespace clice {

namespace testing {

struct CompilerFixture;

}

namespace protocol = kota::ipc::protocol;

class ContextResolver;
Expand Down Expand Up @@ -98,22 +104,43 @@ class Compiler {
/// Callback invoked when indexing should be scheduled.
std::function<void()> on_indexing_needed;

/// Invoked from ensure_compiled's fast path when the pull-side
/// staleness check finds a dependency changed on disk. The owner routes
/// it into the event pipeline as a DiskChanged (synchronously), so lazy
/// detection and the file tracker's polling share one invalidation
/// cascade instead of maintaining two.
std::function<void(std::uint32_t path_id)> on_stale;

/// Cancel in-flight compile tasks and wait for them to finish.
kota::task<> stop();

private:
kota::task<> run_compile(std::shared_ptr<Session> session);

/// @param launch_generation, launch_epoch The caller's staleness-token
/// snapshots from the moment its round took off, NOT ones
/// taken on entry: a round invalidated during the
/// dependency phase would otherwise re-snapshot the new
/// values here and slip a stale pch_ref past the write
/// guards. Both tokens are needed — a supersede bumps
/// generation, but a Lost-type invalidation (disk or CDB
/// change behind an in-flight round) bumps only
/// dirty_epoch, and a round that resolved its command
/// before the event must not write pch_ref back either.
/// @param scope When set, cancels the module-dependency wait if this
/// compile round is superseded by a newer one.
kota::task<bool> ensure_deps(Session& session,
std::uint64_t launch_generation,
std::uint64_t launch_epoch,
const std::string& directory,
const std::vector<std::string>& arguments,
std::pair<std::string, uint32_t>& pch,
std::unordered_map<std::string, std::string>& pcms,
std::optional<kota::cancellation_token> scope = {});

kota::task<bool> ensure_pch(Session& session,
std::uint64_t launch_generation,
std::uint64_t launch_epoch,
const std::string& directory,
const std::vector<std::string>& arguments);

Expand All @@ -125,6 +152,8 @@ class Compiler {
ContextResolver& contexts;
WorkerPool& pool;
kota::task_group<> compile_tasks{loop};

friend struct testing::CompilerFixture;
};

} // namespace clice
22 changes: 6 additions & 16 deletions src/server/compiler/context_resolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -751,12 +751,7 @@ bool ContextResolver::drop_orphaned_choices(SessionStore& sessions) {
LOG_INFO("Dropping orphaned context choice for {}: its basis no longer exists",
workspace.path_pool.resolve(session_id));
drop_header_context(session_id);
session->pch_ref.reset();
session->ast_dirty = true;
session->trial_done = false;
// Invalidate in-flight compiles so they cannot clobber the
// reset state when they finish (same as switchContext).
session->generation += 1;
SessionStore::reset_compile_state(*session, ResetDepth::Superseded);
saved_contexts.erase(it);
dropped_saved = true;
}
Expand Down Expand Up @@ -984,17 +979,12 @@ ext::SwitchContextResult ContextResolver::switch_context(llvm::StringRef path,
}

drop_header_context(path_id);
session->pch_ref.reset();
session->ast_deps.reset();
session->ast_dirty = true;
// The new context needs its own self-containment trial — a
// different host can change the macro environment.
session->trial_done = false;
// The new context is a different compilation identity: supersede any
// in-flight compile and drop the state earned under the old one. It
// also needs its own self-containment trial — a different host can
// change the macro environment.
SessionStore::reset_compile_state(*session, ResetDepth::Superseded);
forget_self_contained(path_id);
// Invalidate any in-flight compile: without the bump it would
// clobber ast_dirty on completion and publish results for the
// old context, with nothing left for is_stale() to detect.
session->generation++;

// The table entry is the active choice; persist it across sessions.
saved_contexts[path_id] = std::move(saved);
Expand Down
14 changes: 7 additions & 7 deletions src/server/state/invalidator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,6 @@ DirtySet Invalidator::apply(llvm::ArrayRef<FileEvent> events) {
dirty.reschedule_indexing = true;
break;
}
case FileEvent::Kind::ContextChanged: {
// Context validation, persistence and session reset happen in
// ContextResolver::switch_context, which already lives in the
// right module; this case is a hook for future cross-file
// policy.
break;
}
case FileEvent::Kind::WorkerCrashed: {
// The worker's ASTs are gone; every document it owned must
// recompile. Compile inputs did not change, so trial state
Expand All @@ -351,6 +344,13 @@ DirtySet Invalidator::apply(llvm::ArrayRef<FileEvent> events) {
}
break;
}
case FileEvent::Kind::DocumentEvicted: {
// Same loss as a crash, scoped to one document: without the
// recompile, feature requests re-route to a worker that no
// longer holds the AST and silently return null.
dirty.mark_lost.push_back(event.path_id);
break;
}
}
}

Expand Down
Loading