Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6cfc18b
Implement service start and stop
mjansson Jul 20, 2026
2f61888
Size client threading for relaying to the service
mjansson Jul 20, 2026
85e5b2d
Pin the service tests to a single test worker
mjansson Jul 20, 2026
0f392ea
Test the client runtime sizing for relaying vs local work
mjansson Jul 22, 2026
77e0c61
Isolate the service's global config in tests and cover config routing
mjansson Jul 22, 2026
f86aacb
Recognise more off values for LORE_USE_SERVICE
mjansson Jul 22, 2026
edd7929
Treat a stopped service as success when stopping
mjansson Jul 22, 2026
f6b0fca
Document the use-service cache staleness limits
mjansson Jul 22, 2026
1720e84
Document that relay sizing is one-shot
mjansson Jul 22, 2026
1b0340f
Document that shutdown does not drain in-flight requests
mjansson Jul 22, 2026
244362e
Point the closed-connection error at a stopped service
mjansson Jul 22, 2026
9fecd8a
Note a service status command as a follow-up
mjansson Jul 22, 2026
7aae473
Remove the unreachable running-as-service guard from start
mjansson Jul 22, 2026
35bfe8c
Document the shutdown wake-up dependency
mjansson Jul 22, 2026
a0d26cf
Document the single-service-per-process constraint
mjansson Jul 22, 2026
4f8531a
Avoid a probe connection on each service call
mjansson Jul 22, 2026
0851b9b
Test graceful shutdown and concurrent stop
mjansson Jul 23, 2026
0ec50e7
Size the runtime lean on the first relayed FFI call
mjansson Jul 23, 2026
311599c
Fix pedantic clippy lints and doc trailing newline
mjansson Jul 23, 2026
332e973
Log a real command name for the service commands
mjansson Jul 23, 2026
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
2 changes: 1 addition & 1 deletion docs/reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Reference is the technical description of the machinery and how to operate it. A
## Reference pages

- [Lore CLI command reference](lore-cli-commands.md) — every `lore` command, subcommand, argument, and flag, generated from `lore --markdown-help`.
- [Lore CLI configuration reference](lore-cli-config.md) — every field in the per-repository `config.toml` and user-level `cli.toml`, with each field's type, default, and on-disk location.
- [Lore CLI configuration reference](lore-cli-config.md) — every field in the per-repository `config.toml`, the user-level global `config.toml`, and the user-level `cli.toml`, with each field's type, default, and on-disk location.
- [Lore Server configuration reference](lore-server-config.md) — every `loreserver` CLI flag, config-file layer, and settings field, including the AWS, DynamoDB, Consul, and hook plugin backends.

## Suggested starting points
Expand Down
25 changes: 18 additions & 7 deletions docs/reference/lore-cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ This page is generated from `lore --markdown-help` (CLI `0.8.2-nightly+31`). Eve
* [`lore service run`↴](#lore-service-run)
* [`lore service start`↴](#lore-service-start)
* [`lore service stop`↴](#lore-service-stop)
* [`lore service set-use-automatically`↴](#lore-service-set-use-automatically)
* [`lore notification`↴](#lore-notification)
* [`lore notification subscribe`↴](#lore-notification-subscribe)
* [`lore completions`↴](#lore-completions)
Expand Down Expand Up @@ -219,8 +220,9 @@ This page is generated from `lore --markdown-help` (CLI `0.8.2-nightly+31`). Eve
* `--compress-limit <count>` — Set maximum number of parallel compress operations
* `--search-limit <SEARCH_LIMIT>` — Set maximum number of revisions to search when matching or finding revisions
* `--search-nearest` — Set to search for nearest match when matching revisions
* `--gc` — Set to run automatic garbage collection on local store in background
* `--no-gc` — Prevent automatic incremental garbage collection for this command; it otherwise runs in the background on writes. `lore repository gc` always runs a full pass regardless

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are we making gc on by default for all clients?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That is already the case. This was just auxiliary cleanup of stale docs while adding the docs on service process. I can move this line to a separate PR if need be.

* `--sync-data` — Force sync data to storage media during flush
* `--cache` — Cache fragment payloads fetched from remote in the local store
* `--non-interactive` — Disable interactive prompts (e.g., per-link commit messages)


Expand Down Expand Up @@ -2598,8 +2600,9 @@ Manage the repository in a service process
###### **Subcommands:**

* `run` — Run this process as the service
* `start` — Start service for a repository
* `stop` — Stop service for a repository
* `start` — Start the service process
* `stop` — Stop the service process
* `set-use-automatically` — Set whether to automatically use the service process



Expand All @@ -2613,21 +2616,29 @@ Run this process as the service

## `lore service start`

Start service for a repository
Start the service process

**Usage:** `lore service start`



## `lore service stop`

Stop service for a repository
Stop the service process

**Usage:** `lore service stop [all]`
**Usage:** `lore service stop`



## `lore service set-use-automatically`

Set whether to automatically use the service process

**Usage:** `lore service set-use-automatically <enabled>`

###### **Arguments:**

* `<all>` — Flag to stop servicing all repositories
* `<enabled>` — Automatically run Lore commands through the service process

Possible values: `true`, `false`

Expand Down
33 changes: 32 additions & 1 deletion docs/reference/lore-cli-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

```text
<repo>/.lore/config.toml # per-repository client settings (created on init/clone)
~/.config/lore/config.toml # user-level global settings (OS user config dir; Linux shown)
~/.config/lore/cli.toml # user-level CLI settings (OS user config dir; Linux shown)
```

This page documents the **Lore CLI** client configuration: the per-repository `config.toml` and the user-level `cli.toml` that the `lore` binary reads. These are distinct from the Lore Server daemon's configuration — for server stores, endpoints, topology, and plugin backends, see the [Lore Server configuration reference](lore-server-config.md). The fields below are written by `lore repository create` and `lore clone`, or you edit them by hand; you don't need to read the source to look one up.
This page documents the **Lore CLI** client configuration: the per-repository `config.toml`, the user-level global `config.toml`, and the user-level `cli.toml` that the `lore` binary reads. These are distinct from the Lore Server daemon's configuration — for server stores, endpoints, topology, and plugin backends, see the [Lore Server configuration reference](lore-server-config.md). The fields below are written by `lore repository create` and `lore clone`, or you edit them by hand; you don't need to read the source to look one up.

## Per-repository `config.toml`

Expand Down Expand Up @@ -87,6 +88,36 @@ A config that uses the legacy names still loads. New configs use the current nam

Lore normally writes this table for you when you clone with `--use-shared-store`. For how shared stores work and how to set one up, see [Step 6 of the Quickstart](../tutorials/quickstart.md#step-6-set-up-a-shared-store-and-clone-a-second-working-tree); for the `lore clone` and `lore shared-store` flags, see the [Lore CLI command reference](lore-cli-commands.md).

## User-level global `config.toml`

### Location

The global `config.toml` holds settings that apply to every repository, rather than to one. It shares the OS user config directory with `cli.toml`, so on a typical Linux setup it is `~/.config/lore/config.toml`; see the table under `cli.toml` below for the other platforms. Setting `LORE_GLOBAL_PATH` moves the whole directory, which is how the test suite isolates it.

The file is optional. When it is absent, every setting below takes its default.

### Fields

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `use_shared_store_automatically` | bool | `false` | Whether `lore repository create` and `lore clone` configure a shared store without being asked. Read only when a repository is created or cloned; the result is written into that repository's own config. |
| `use_service_automatically` | bool | `false` | Whether Lore runs every command in the background service process. See below. |
| `default_shared_stores` | table | empty | Per-remote default shared store paths, keyed by remote URL. |

### Running commands through the service

With `use_service_automatically` enabled, Lore sends each command to a background service process over a local socket instead of executing it in the CLI process, and starts that service automatically if it is not already running. Set it with:

```bash
lore service set-use-automatically true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we have configs interspersed in each subcommand or would be be better to have something like lore config set/get?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, but we don't have that yet and this matches the shared store way of configuring.

```

`lore service start` and `lore service stop` control the process explicitly. Starting when a service is already running, and stopping when none is, are both no-ops rather than errors.

The `LORE_USE_SERVICE` environment variable overrides the setting for a single invocation: any value other than `0` or `false` forces the service on, and `0`, `false`, or an empty value forces it off.

Automatic start-up only applies to the `lore` CLI. When Lore is embedded as a library the running executable is the host application, which Lore will not relaunch as a service; start the service separately in that case.

## User-level `cli.toml`

### Location
Expand Down
141 changes: 115 additions & 26 deletions lore-base/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,15 @@ pub struct TokioSettings {
#[serde(default = "default_thread_keep_alive")]
pub thread_keep_alive_seconds: u64,
pub worker_threads: Option<usize>,
/// Whether to build the rayon compute pool up front. Turning this off does
/// not make the pool unavailable: [`compute_pool`] still builds it on first
/// use, so this only decides whether its threads are paid for eagerly.
#[serde(default = "default_eager_compute_pool")]
pub eager_compute_pool: bool,
}

fn default_eager_compute_pool() -> bool {
true
}

impl Default for TokioSettings {
Expand All @@ -549,6 +558,23 @@ impl Default for TokioSettings {
max_blocking_threads: default_blocking_threads(),
thread_keep_alive_seconds: default_thread_keep_alive(),
worker_threads: None,
eager_compute_pool: true,
}
}
}

impl TokioSettings {
/// Settings for a process that only relays work elsewhere, such as a client
/// whose calls all execute in the Lore service. Sized for IPC rather than
/// for doing the work, and with no compute pool, which such a process never
/// touches. Each pool keeps [`MIN_THREADS_PER_POOL`] so that neither can
/// starve the other.
pub fn relay_only() -> Self {
TokioSettings {
max_blocking_threads: MIN_THREADS_PER_POOL,
thread_keep_alive_seconds: default_thread_keep_alive(),
worker_threads: Some(MIN_THREADS_PER_POOL),
eager_compute_pool: false,
}
}
}
Expand All @@ -563,6 +589,38 @@ pub fn runtime() -> Handle {
/// If no runtime exists yet, creates one with the provided settings (or defaults if `None`).
/// If a tokio runtime is already active on the current thread, returns its handle instead.
/// Respects the `LORE_WORKER_THREADS` environment variable for overriding worker thread count.
/// The number of tokio worker threads a runtime with these settings is built
/// with. Precedence: the `LORE_WORKER_THREADS` env override, then an explicit
/// positive count in the settings, then the budget-derived default. Always a
/// concrete count, because leaving it unset makes tokio use the raw core count
/// and ignore the thread limit.
fn resolve_worker_threads(settings: &TokioSettings) -> usize {
match (
env_thread_override("LORE_WORKER_THREADS"),
settings.worker_threads,
) {
(Some(val), _) => val,
(None, Some(val)) if val > 0 => val,
_ => default_worker_threads(),
}
}

/// Builds a fresh multi-thread tokio runtime from the settings. Does not touch
/// the shared runtime or the compute pool, so it is safe to call in isolation.
fn build_tokio_runtime(settings: &TokioSettings) -> tokio::runtime::Runtime {
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder
.enable_all()
.max_blocking_threads(settings.max_blocking_threads)
.thread_keep_alive(Duration::from_secs(settings.thread_keep_alive_seconds))
.thread_name_fn(|| {
static ID: AtomicUsize = AtomicUsize::new(0);
format!("lore-tokio-{}", ID.fetch_add(1, Ordering::Relaxed))
})
.worker_threads(resolve_worker_threads(settings));
builder.build().expect("Failed to create runtime")
}

pub fn runtime_with_settings(settings: Option<TokioSettings>) -> Handle {
if let Ok(handle) = tokio::runtime::Handle::try_current() {
handle
Expand All @@ -572,38 +630,19 @@ pub fn runtime_with_settings(settings: Option<TokioSettings>) -> Handle {
runtime.handle().clone()
} else {
let settings = settings.unwrap_or_default();
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder
.enable_all()
.max_blocking_threads(settings.max_blocking_threads)
.thread_keep_alive(Duration::from_secs(settings.thread_keep_alive_seconds))
.thread_name_fn(|| {
static ID: AtomicUsize = AtomicUsize::new(0);
format!("lore-tokio-{}", ID.fetch_add(1, Ordering::Relaxed))
});
// Always set an explicit count, else tokio would default to the raw
// core count and ignore the thread limit. Precedence: env override,
// explicit setting, budget-derived default.
let worker_threads = match (
env_thread_override("LORE_WORKER_THREADS"),
settings.worker_threads,
) {
(Some(val), _) => val,
(None, Some(val)) if val > 0 => val,
_ => default_worker_threads(),
};
builder.worker_threads(worker_threads);
let runtime = builder.build().expect("Failed to create runtime");
let runtime = build_tokio_runtime(&settings);
let handle = runtime.handle().clone();
*default_runtime = Some(runtime);

// Build the compute pool off-thread so runtime creation isn't
// blocked on spawning N rayon workers. No LORE_CONTEXT is active
// yet, so Handle::spawn directly rather than lore_spawn!.
#[allow(clippy::disallowed_methods)]
handle.spawn(async {
let _ = COMPUTE_POOL.get_or_init(build_compute_pool);
});
if settings.eager_compute_pool {
#[allow(clippy::disallowed_methods)]
handle.spawn(async {
let _ = COMPUTE_POOL.get_or_init(build_compute_pool);
});
}

handle
}
Expand Down Expand Up @@ -693,6 +732,7 @@ mod tests {
max_blocking_threads: 4,
thread_keep_alive_seconds: 5,
worker_threads: Some(2),
eager_compute_pool: true,
};
let handle = runtime_with_settings(Some(settings));
handle.block_on(async {
Expand All @@ -708,6 +748,55 @@ mod tests {
assert_eq!(counts.compute, 7);
}

#[test]
fn relay_only_settings_are_minimal() {
let relay = TokioSettings::relay_only();
assert_eq!(relay.worker_threads, Some(MIN_THREADS_PER_POOL));
assert_eq!(relay.max_blocking_threads, MIN_THREADS_PER_POOL);
assert!(
!relay.eager_compute_pool,
"a relay process never touches the compute pool, so it must not build it eagerly"
);
assert!(
TokioSettings::default().eager_compute_pool,
"a full runtime builds the compute pool eagerly"
);
}

#[test]
fn relay_runtime_is_smaller_than_full() {
// The env override, if set in the test environment, would defeat the
// per-settings worker count both branches resolve, so skip then.
if env_thread_override("LORE_WORKER_THREADS").is_some() {
return;
}

let relay = build_tokio_runtime(&TokioSettings::relay_only());
let full = build_tokio_runtime(&TokioSettings::default());

assert_eq!(
relay.metrics().num_workers(),
MIN_THREADS_PER_POOL,
"a relay runtime is sized for IPC, not for doing the work"
);
assert!(
full.metrics().num_workers() >= relay.metrics().num_workers(),
"the full runtime is never smaller than the relay one"
);
}

#[test]
fn built_runtime_honors_resolved_worker_count() {
// Independent of any env override: whatever count is resolved is the
// count the runtime is actually built with.
let settings = TokioSettings::relay_only();
let runtime = build_tokio_runtime(&settings);
assert_eq!(
runtime.metrics().num_workers(),
resolve_worker_threads(&settings)
);
}

#[test]
fn apportion_returns_defaults_when_within_limit() {
let defaults = default_thread_counts(8);
Expand Down
56 changes: 52 additions & 4 deletions lore-capi/lore.h
Original file line number Diff line number Diff line change
Expand Up @@ -4940,17 +4940,22 @@ typedef struct lore_storage_upload_args_t {
struct lore_storage_upload_item_array_t items;
} lore_storage_upload_args_t;

// Arguments for starting the Lore service process for the current repository (no parameters).
// Arguments for starting the Lore service process (no parameters).
typedef struct lore_service_start_args_t {
int _unused;
} lore_service_start_args_t;

// Arguments for stopping the Lore service process for the current or all repositories.
// Arguments for stopping the Lore service process (no parameters).
typedef struct lore_service_stop_args_t {
// Stop all repositories rather than just the current one
uint8_t all;
int _unused;
} lore_service_stop_args_t;

// Arguments for setting whether Lore automatically routes calls through the service process.
typedef struct lore_service_set_use_automatically_args_t {
// Automatically use the service process
uint8_t enabled;
} lore_service_set_use_automatically_args_t;

// Arguments for subscribing to repository notifications (no parameters).
typedef struct lore_notification_subscribe_args_t {
int _unused;
Expand Down Expand Up @@ -10472,6 +10477,49 @@ void lore_service_stop_async(const struct lore_global_args_t *globals,
const struct lore_service_stop_args_t *args,
struct lore_event_callback_config_t callback);

// Set whether Lore automatically routes calls through the background service.
//
// When enabled, every Lore call is executed by the service process, which is
// started automatically if it is not already running.
//
// # Events
//
// Events are delivered via the callback as `lore_event_t`. Use the `tag` field to identify the event type.
//
// ## Standard Events
//
// These events are emitted by all interface functions:
//
// | Tag | Data Type | Description |
// |-----|-----------|-------------|
// | `LORE_EVENT_LOG` | `lore_log_event_data_t` | Diagnostic messages throughout execution |
// | `LORE_EVENT_ERROR` | `lore_error_event_data_t` | Emitted for a non-fatal error during the operation |
// | `LORE_EVENT_COMPLETE` | `lore_complete_event_data_t` | Always emitted at the end; `status` is `0` on success or the error code on failure |
// | `LORE_EVENT_END` | `lore_end_event_data_t` | Always emitted after `COMPLETE` to signal callback termination |
int32_t lore_service_set_use_automatically(const struct lore_global_args_t *globals,
const struct lore_service_set_use_automatically_args_t *args,
struct lore_event_callback_config_t callback);

// Asynchronous version of `lore_service_set_use_automatically`.
//
// # Events
//
// Events are delivered via the callback as `lore_event_t`. Use the `tag` field to identify the event type.
//
// ## Standard Events
//
// These events are emitted by all interface functions:
//
// | Tag | Data Type | Description |
// |-----|-----------|-------------|
// | `LORE_EVENT_LOG` | `lore_log_event_data_t` | Diagnostic messages throughout execution |
// | `LORE_EVENT_ERROR` | `lore_error_event_data_t` | Emitted for a non-fatal error during the operation |
// | `LORE_EVENT_COMPLETE` | `lore_complete_event_data_t` | Always emitted at the end; `status` is `0` on success or the error code on failure |
// | `LORE_EVENT_END` | `lore_end_event_data_t` | Always emitted after `COMPLETE` to signal callback termination |
void lore_service_set_use_automatically_async(const struct lore_global_args_t *globals,
const struct lore_service_set_use_automatically_args_t *args,
struct lore_event_callback_config_t callback);

// Subscribe to repository notifications.
//
// # Events
Expand Down
Loading