@feltdb/core 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-state.d.ts +489 -0
- package/dist/agent-state.d.ts.map +1 -0
- package/dist/agent-state.js +993 -0
- package/dist/build-identity.json +28 -0
- package/dist/cli/commands.js +88 -43
- package/dist/collection.d.ts +41 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +116 -47
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +11 -1
- package/dist/create/build-identity.json +28 -0
- package/dist/create/cli.js +3 -3
- package/dist/create/create.js +17 -19
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +18 -3
- package/dist/create/server-source/Cargo.toml +4 -2
- package/dist/create/server-source/crates/feltdb/Cargo.toml +20 -0
- package/dist/create/server-source/crates/feltdb/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb/README.md +75 -0
- package/dist/create/server-source/crates/feltdb/src/agent_state.rs +320 -0
- package/dist/create/server-source/crates/feltdb/src/agent_transition_contract.rs +1545 -0
- package/dist/create/server-source/crates/feltdb/src/authority.rs +145 -15
- package/dist/create/server-source/crates/feltdb/src/lib.rs +238 -23
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +4 -1
- package/dist/create/server-source/crates/feltdb/src/state_model.rs +850 -155
- package/dist/create/server-source/crates/feltdb/tests/agent_state_native.rs +1188 -0
- package/dist/create/server-source/crates/feltdb/tests/agent_transition_contract_conformance.rs +2884 -0
- package/dist/create/server-source/crates/feltdb/tests/authority_transaction_identity.rs +534 -0
- package/dist/create/server-source/crates/feltdb/tests/fixtures/agent_transition_corpus.json +23340 -0
- package/dist/create/server-source/crates/feltdb/tests/generalized_version_control_substrate_completion.rs +31 -13
- package/dist/create/server-source/crates/feltdb/tests/state_ref_authority.rs +918 -0
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +15 -2
- package/dist/create/server-source/crates/feltdb-client/Cargo.toml +29 -0
- package/dist/create/server-source/crates/feltdb-client/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb-client/README.md +70 -0
- package/dist/create/server-source/crates/feltdb-client/src/application.rs +527 -0
- package/dist/create/server-source/crates/feltdb-client/src/authority.rs +679 -0
- package/dist/create/server-source/crates/feltdb-client/src/control.rs +292 -0
- package/dist/create/server-source/crates/feltdb-client/src/error.rs +227 -0
- package/dist/create/server-source/crates/feltdb-client/src/events.rs +146 -0
- package/dist/create/server-source/crates/feltdb-client/src/lib.rs +466 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/README.md +4 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/private-ca.pem +20 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server-key.pem +28 -0
- package/dist/create/server-source/crates/feltdb-client/tests/fixtures/server.pem +20 -0
- package/dist/create/server-source/crates/feltdb-client/tests/private_ca.rs +95 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +24 -1
- package/dist/create/server-source/crates/feltdb-server/LICENSE +21 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +12 -1
- package/dist/create/server-source/crates/feltdb-server/benches/query_shape_observer.rs +136 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +3 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +6 -2
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +21 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +75 -8
- package/dist/create/server-source/crates/feltdb-server/src/query_shape.rs +1137 -0
- package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.expected.json +300 -0
- package/dist/create/server-source/crates/feltdb-server/tests/fixtures/query-shape-trace.json +25 -0
- package/dist/create/server-source/crates/feltdb-server/tests/query_shape_trace.rs +128 -0
- package/dist/create/server-source/crates/feltdb-server/tests/rust_client.rs +683 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +3 -1
- package/dist/create/template/default-project/README.md +12 -7
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +1 -1
- package/dist/create/template/dot-feltdb-README.md +3 -3
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +43 -17
- package/dist/development-runtime-bridge.d.ts.map +1 -1
- package/dist/development-runtime-bridge.js +22 -1
- package/dist/embedded-transaction.d.ts +75 -8
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +124 -18
- package/dist/error-codes.d.ts +2 -0
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +2 -0
- package/dist/feltdb.d.ts +1 -11
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +6 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +43 -11
- package/dist/index-core.d.ts +1 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +1 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +65 -41
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +29 -8
- package/dist/studio/{KeyManagementPanel-DZuSeWBK.js → KeyManagementPanel-D5FMvvU9.js} +1 -0
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/GlobalSearch.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.js +1 -1
- package/dist/studio/components/RouteErrorBoundary.d.ts +5 -0
- package/dist/studio/components/RouteErrorBoundary.d.ts.map +1 -0
- package/dist/studio/components/index.js +2 -2
- package/dist/studio/{components-BHTKlUgT.js → components-B4eKIUN5.js} +3 -2
- package/dist/studio/control-plane/Agents.d.ts.map +1 -1
- package/dist/studio/control-plane/Data.d.ts.map +1 -1
- package/dist/studio/control-plane/Instance.d.ts.map +1 -1
- package/dist/studio/control-plane/Operations.d.ts.map +1 -1
- package/dist/studio/control-plane/Overview.d.ts.map +1 -1
- package/dist/studio/control-plane/Schema.d.ts.map +1 -1
- package/dist/studio/control-plane/State.d.ts +11 -0
- package/dist/studio/control-plane/State.d.ts.map +1 -1
- package/dist/studio/control-plane/context.d.ts +4 -1
- package/dist/studio/control-plane/context.d.ts.map +1 -1
- package/dist/studio/control-plane/index.d.ts +1 -1
- package/dist/studio/control-plane/index.d.ts.map +1 -1
- package/dist/studio/control-plane/index.js +2 -2
- package/dist/studio/control-plane/primitives.d.ts +22 -2
- package/dist/studio/control-plane/primitives.d.ts.map +1 -1
- package/dist/studio/control-plane-fxMcJn74.js +4173 -0
- package/dist/studio/index.js +307 -235
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
- package/dist/studio/utils/control-plane-api.js +4 -3
- package/dist/studio/utils/service-api.d.ts.map +1 -1
- package/dist/studio/utils/service-api.js +5 -4
- package/dist/studio-app/assets/{feltdb_wasm-DYfWAV0E.js → feltdb_wasm-BatwwogY.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BX0newOr.wasm +0 -0
- package/dist/studio-app/assets/index-CiC8VY-a.css +1 -0
- package/dist/studio-app/assets/index-DoVvbIay.js +30 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/telemetry.d.ts +1 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +4 -3
- package/dist/transaction-identity.d.ts +62 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +138 -0
- package/dist/transaction.d.ts +50 -5
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +28 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/wasm/package.json +1 -1
- package/package.json +2 -2
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/studio/control-plane-DP8ljTDO.js +0 -3207
- package/dist/studio-app/assets/feltdb_wasm_bg-DHiOUhoQ.wasm +0 -0
- package/dist/studio-app/assets/index-6f6KVx5R.js +0 -30
- package/dist/studio-app/assets/index-BrsF-kWL.css +0 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FeltDB Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -5,7 +5,18 @@ second state store: every HTTP mutation calls `FeltDb::insert`, `FeltDb::update`
|
|
|
5
5
|
`FeltDb::delete`, producing the same operations, vector-clock updates, durable log entries, and
|
|
6
6
|
reactive events as an embedded mutation.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Install it from crates.io (any Rust 1.96+ toolchain):
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
cargo install feltdb-server --locked
|
|
12
|
+
feltdb-server --host 127.0.0.1 --port 7700 --namespace my-app --data ./data/feltdb.log
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The command line and the HTTP API are the supported contract. The library half of this crate exposes
|
|
16
|
+
the server's building blocks without a compatibility promise; for embedded state in your own process
|
|
17
|
+
depend on [`feltdb`](https://crates.io/crates/feltdb) instead.
|
|
18
|
+
|
|
19
|
+
From a checkout of the repository, run it with:
|
|
9
20
|
|
|
10
21
|
```sh
|
|
11
22
|
cargo run -p feltdb-server -- \
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
//! Direct per-operation cost of the query-shape observer.
|
|
2
|
+
//!
|
|
3
|
+
//! This isolates what the observer adds to one `POST /query`: building the
|
|
4
|
+
//! structural input, normalizing, and recording one outcome. It is deliberately
|
|
5
|
+
//! not an end-to-end number — an HTTP round trip costs orders of magnitude more
|
|
6
|
+
//! and would hide it — and it says nothing about contention with the database's
|
|
7
|
+
//! state lock, which the observer never takes. The end-to-end comparison lives
|
|
8
|
+
//! in `tools/benchmarks/query-shape-observer.mjs`.
|
|
9
|
+
//!
|
|
10
|
+
//! cargo bench --locked -p feltdb-server --bench query_shape_observer
|
|
11
|
+
//!
|
|
12
|
+
//! Output is one JSON object. Each figure is the median of `ROUNDS` timed
|
|
13
|
+
//! rounds, in nanoseconds per operation, on this machine and build profile
|
|
14
|
+
//! only.
|
|
15
|
+
|
|
16
|
+
use feltdb_server::query_shape::{Access, ObserverConfig, QueryShapeObserver, ShapeInput};
|
|
17
|
+
use std::hint::black_box;
|
|
18
|
+
use std::sync::Arc;
|
|
19
|
+
use std::time::Instant;
|
|
20
|
+
|
|
21
|
+
const ROUNDS: usize = 7;
|
|
22
|
+
const OPERATIONS: usize = 200_000;
|
|
23
|
+
|
|
24
|
+
fn median(mut values: Vec<f64>) -> f64 {
|
|
25
|
+
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
26
|
+
values[values.len() / 2]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// One simulated query: the same `Vec` construction the handler performs.
|
|
30
|
+
fn observe(observer: &QueryShapeObserver, collection: &str, field: &str) {
|
|
31
|
+
let predicates = vec![(field, "eq"), ("kind", "eq")];
|
|
32
|
+
let order_by = vec![("n", "asc")];
|
|
33
|
+
let observation = observer.begin(&ShapeInput {
|
|
34
|
+
collection,
|
|
35
|
+
predicates: &predicates,
|
|
36
|
+
order_by: &order_by,
|
|
37
|
+
limit: 50,
|
|
38
|
+
has_semantic_clause: false,
|
|
39
|
+
});
|
|
40
|
+
observation.finish_executed(Access::Indexed, black_box(40), black_box(40));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn per_op(mut run: impl FnMut(usize)) -> f64 {
|
|
44
|
+
median(
|
|
45
|
+
(0..ROUNDS)
|
|
46
|
+
.map(|_| {
|
|
47
|
+
let started = Instant::now();
|
|
48
|
+
for index in 0..OPERATIONS {
|
|
49
|
+
run(index);
|
|
50
|
+
}
|
|
51
|
+
started.elapsed().as_nanos() as f64 / OPERATIONS as f64
|
|
52
|
+
})
|
|
53
|
+
.collect(),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn main() {
|
|
58
|
+
let observer = QueryShapeObserver::new(ObserverConfig::with_capacity(128));
|
|
59
|
+
|
|
60
|
+
// The cost of building the input with no observer at all: the floor.
|
|
61
|
+
let floor = per_op(|_| {
|
|
62
|
+
let predicates = vec![("tenant", "eq"), ("kind", "eq")];
|
|
63
|
+
let order_by = vec![("n", "asc")];
|
|
64
|
+
black_box((&predicates, &order_by));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let one_shape = per_op(|_| observe(&observer, "orders", "tenant"));
|
|
68
|
+
|
|
69
|
+
let fixed: Vec<String> = (0..64).map(|i| format!("c{i}")).collect();
|
|
70
|
+
let sixty_four_shapes = per_op(|index| observe(&observer, &fixed[index % 64], "tenant"));
|
|
71
|
+
|
|
72
|
+
// Every operation admits a new shape into a full table: the eviction scan.
|
|
73
|
+
let churn_names: Vec<String> = (0..4096).map(|i| format!("churn{i}")).collect();
|
|
74
|
+
let churn = QueryShapeObserver::new(ObserverConfig::with_capacity(128));
|
|
75
|
+
let eviction_churn = per_op(|index| observe(&churn, &churn_names[index % 4096], "tenant"));
|
|
76
|
+
let churn_max = QueryShapeObserver::new(ObserverConfig::with_capacity(1024));
|
|
77
|
+
let eviction_churn_max_capacity =
|
|
78
|
+
per_op(|index| observe(&churn_max, &churn_names[index % 4096], "tenant"));
|
|
79
|
+
|
|
80
|
+
let reserved = per_op(|_| {
|
|
81
|
+
let order_by = vec![("n", "asc")];
|
|
82
|
+
black_box(observer.begin(&ShapeInput {
|
|
83
|
+
collection: "_feltdb.Proposal",
|
|
84
|
+
predicates: &[],
|
|
85
|
+
order_by: &order_by,
|
|
86
|
+
limit: 50,
|
|
87
|
+
has_semantic_clause: false,
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Contention: four threads recording into one observer.
|
|
92
|
+
let shared = Arc::new(QueryShapeObserver::new(ObserverConfig::with_capacity(128)));
|
|
93
|
+
let contended = median(
|
|
94
|
+
(0..ROUNDS)
|
|
95
|
+
.map(|_| {
|
|
96
|
+
let started = Instant::now();
|
|
97
|
+
let handles: Vec<_> = (0..4)
|
|
98
|
+
.map(|thread| {
|
|
99
|
+
let shared = shared.clone();
|
|
100
|
+
std::thread::spawn(move || {
|
|
101
|
+
let name = format!("t{thread}");
|
|
102
|
+
for _ in 0..OPERATIONS / 4 {
|
|
103
|
+
observe(&shared, &name, "tenant");
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
.collect();
|
|
108
|
+
for handle in handles {
|
|
109
|
+
handle.join().unwrap();
|
|
110
|
+
}
|
|
111
|
+
started.elapsed().as_nanos() as f64 / OPERATIONS as f64
|
|
112
|
+
})
|
|
113
|
+
.collect(),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
let snapshot = churn_max.snapshot();
|
|
117
|
+
println!(
|
|
118
|
+
"{}",
|
|
119
|
+
serde_json::json!({
|
|
120
|
+
"benchmark": "query_shape_observer",
|
|
121
|
+
"profile": if cfg!(debug_assertions) { "debug" } else { "release" },
|
|
122
|
+
"operations_per_round": OPERATIONS,
|
|
123
|
+
"rounds": ROUNDS,
|
|
124
|
+
"unit": "ns/op (median of rounds)",
|
|
125
|
+
"input_construction_floor_no_observer": floor,
|
|
126
|
+
"begin_finish_one_shape": one_shape,
|
|
127
|
+
"begin_finish_64_shapes": sixty_four_shapes,
|
|
128
|
+
"begin_finish_every_op_evicts_capacity_128": eviction_churn,
|
|
129
|
+
"begin_finish_every_op_evicts_capacity_1024": eviction_churn_max_capacity,
|
|
130
|
+
"reserved_collection_skipped": reserved,
|
|
131
|
+
"four_threads_one_observer_wall_per_op": contended,
|
|
132
|
+
"capacity_1024_memory_bound_bytes": snapshot.memory_bound_bytes,
|
|
133
|
+
"capacity_1024_memory_estimate_bytes_at_full": snapshot.memory_estimate_bytes,
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -86,6 +86,9 @@ pub struct AppState {
|
|
|
86
86
|
pub state_triggers: Arc<std::sync::Mutex<StateTriggerStore>>,
|
|
87
87
|
pub mesh: Arc<std::sync::Mutex<WorkerMeshStore>>,
|
|
88
88
|
pub readiness_probe: Arc<PathBuf>,
|
|
89
|
+
/// Opt-in, value-free query-shape observer for `POST /query`, or `None`
|
|
90
|
+
/// (the default). Diagnostic only: nothing reads it to answer a request.
|
|
91
|
+
pub query_shapes: Option<Arc<crate::query_shape::QueryShapeObserver>>,
|
|
89
92
|
pub bounded_query_cursors: Arc<std::sync::Mutex<HashMap<String, BoundedQueryCursor>>>,
|
|
90
93
|
/// The deployment's semantic runtime binding for bounded semantic query
|
|
91
94
|
/// composition, or `None` when the deployment configures no runtime. A
|
|
@@ -411,11 +411,15 @@ fn copy_source(
|
|
|
411
411
|
if let Some(parent) = destination.parent() {
|
|
412
412
|
fs::create_dir_all(parent)?;
|
|
413
413
|
}
|
|
414
|
-
|
|
414
|
+
// Record what was copied, not what the source measured before the
|
|
415
|
+
// copy. An online backup copies files the server is still appending
|
|
416
|
+
// to (the audit log): a length taken from the source before copying
|
|
417
|
+
// would disagree with the copy's hash and fail verification.
|
|
418
|
+
let copied = fs::copy(source, destination)?;
|
|
415
419
|
sync_file(destination)?;
|
|
416
420
|
entries.push(BackupEntry {
|
|
417
421
|
path: relative.into(),
|
|
418
|
-
bytes:
|
|
422
|
+
bytes: copied,
|
|
419
423
|
sha256: hash_file(destination)?,
|
|
420
424
|
});
|
|
421
425
|
}
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
//! # feltdb-server
|
|
2
|
+
//!
|
|
3
|
+
//! The FeltDB server: the managed FeltDB authority over HTTP. Install and run it with
|
|
4
|
+
//!
|
|
5
|
+
//! ```sh
|
|
6
|
+
//! cargo install feltdb-server --locked
|
|
7
|
+
//! feltdb-server --host 127.0.0.1 --port 7700 --namespace my-app --data ./data/feltdb.log
|
|
8
|
+
//! ```
|
|
9
|
+
//!
|
|
10
|
+
//! Its supported contract is the binary's command line and its HTTP API (`feltdb.public.substrate@1.0`
|
|
11
|
+
//! and the `/v1` Service API, specified in the repository's
|
|
12
|
+
//! [`docs/reference/public-api.md`](https://github.com/rkendel1/flow_db/blob/main/docs/reference/public-api.md)).
|
|
13
|
+
//!
|
|
14
|
+
//! The modules below are the server's building blocks. They are public so the binary and the
|
|
15
|
+
//! repository's tests can use them, and **no compatibility promise covers them**: any release may
|
|
16
|
+
//! change them. For embedded state in your own process, depend on [`feltdb`] directly; it is
|
|
17
|
+
//! re-exported here at the exact version this server is built against.
|
|
18
|
+
|
|
19
|
+
pub use feltdb;
|
|
20
|
+
|
|
1
21
|
pub mod app_state;
|
|
2
22
|
pub mod application_contract;
|
|
3
23
|
pub mod artifacts;
|
|
@@ -27,6 +47,7 @@ pub mod metrics;
|
|
|
27
47
|
pub mod portable_bundle;
|
|
28
48
|
pub mod principals;
|
|
29
49
|
pub mod providers;
|
|
50
|
+
pub mod query_shape;
|
|
30
51
|
pub mod releases;
|
|
31
52
|
pub mod request_telemetry;
|
|
32
53
|
pub mod sessions;
|
|
@@ -96,6 +96,7 @@ use feltdb_server::{
|
|
|
96
96
|
portable_bundle::{BundleArtifact, BundleStore, ExportOptions, ImportMode, PortableBundle},
|
|
97
97
|
principals::{PrincipalKind, PrincipalStatus},
|
|
98
98
|
providers::{ProviderManifest, ProviderStatus, ProviderStore},
|
|
99
|
+
query_shape::{Access, ObserverConfig, QueryShapeObserver, ShapeInput},
|
|
99
100
|
releases::{
|
|
100
101
|
provider_for, AuthorizedDeploymentContext, DeploymentPlan, DeploymentProfile,
|
|
101
102
|
DeploymentProvider, DeploymentProviderKind, DomainBinding, ReleaseManifest, ReleaseStore,
|
|
@@ -8766,6 +8767,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
8766
8767
|
// Constructed before the state so the same handle can be stopped after
|
|
8767
8768
|
// serving: the pipeline outlives the router by exactly one shutdown.
|
|
8768
8769
|
let audit_handle = AuditLog::new(config.audit.clone());
|
|
8770
|
+
// Opt-in and off by default. A malformed setting is a startup error rather
|
|
8771
|
+
// than a silent default, so a mistyped exclusion list cannot yield an
|
|
8772
|
+
// observer that records what the operator meant to exclude.
|
|
8773
|
+
let query_shapes = ObserverConfig::from_environment()?.map(|config| {
|
|
8774
|
+
println!(
|
|
8775
|
+
"feltdb: query-shape observer enabled (capacity {}, value-free, in-memory)",
|
|
8776
|
+
config.capacity
|
|
8777
|
+
);
|
|
8778
|
+
Arc::new(QueryShapeObserver::new(config))
|
|
8779
|
+
});
|
|
8769
8780
|
let state = AppState {
|
|
8770
8781
|
started_at: Instant::now(),
|
|
8771
8782
|
admission: Arc::new(tokio::sync::Semaphore::new(config.max_in_flight)),
|
|
@@ -8829,6 +8840,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
8829
8840
|
state_triggers: Arc::new(std::sync::Mutex::new(state_trigger_store)),
|
|
8830
8841
|
mesh: Arc::new(std::sync::Mutex::new(mesh_store)),
|
|
8831
8842
|
readiness_probe: Arc::new(readiness_probe),
|
|
8843
|
+
query_shapes,
|
|
8832
8844
|
bounded_query_cursors: Arc::new(std::sync::Mutex::new(HashMap::new())),
|
|
8833
8845
|
semantic_runtime: configured_semantic_runtime()?,
|
|
8834
8846
|
};
|
|
@@ -13520,7 +13532,7 @@ fn bounded_query_records(
|
|
|
13520
13532
|
state: &AppState,
|
|
13521
13533
|
request: &BoundedQueryRequest,
|
|
13522
13534
|
execution: BoundedQueryExecution,
|
|
13523
|
-
) -> Result<Vec<feltdb::StoredRow>, ApiError> {
|
|
13535
|
+
) -> Result<(Vec<feltdb::StoredRow>, Access), ApiError> {
|
|
13524
13536
|
if execution != BoundedQueryExecution::Scan {
|
|
13525
13537
|
let equalities = bounded_query_equalities(&request.conditions);
|
|
13526
13538
|
let indexed =
|
|
@@ -13530,7 +13542,7 @@ fn bounded_query_records(
|
|
|
13530
13542
|
bounded_query_matches(row, &request.conditions)
|
|
13531
13543
|
})?;
|
|
13532
13544
|
if let Some(records) = indexed {
|
|
13533
|
-
return Ok(records);
|
|
13545
|
+
return Ok((records, Access::Indexed));
|
|
13534
13546
|
}
|
|
13535
13547
|
if execution == BoundedQueryExecution::Index {
|
|
13536
13548
|
// Only reachable through the test-only header. A forced-index run
|
|
@@ -13541,11 +13553,12 @@ fn bounded_query_records(
|
|
|
13541
13553
|
));
|
|
13542
13554
|
}
|
|
13543
13555
|
}
|
|
13544
|
-
|
|
13556
|
+
let scanned = state
|
|
13545
13557
|
.db
|
|
13546
13558
|
.query_collection(&request.collection, None, |row| {
|
|
13547
13559
|
bounded_query_matches(row, &request.conditions)
|
|
13548
|
-
})
|
|
13560
|
+
})?;
|
|
13561
|
+
Ok((scanned, Access::Scan))
|
|
13549
13562
|
}
|
|
13550
13563
|
|
|
13551
13564
|
fn issue_bounded_cursor(
|
|
@@ -13628,6 +13641,37 @@ async fn execute_bounded_query(
|
|
|
13628
13641
|
feltdb::validate_semantic_clause(clause).map_err(semantic_query_error)?;
|
|
13629
13642
|
}
|
|
13630
13643
|
let hash = bounded_query_hash(&request)?;
|
|
13644
|
+
// Diagnostic only, and only when opted in. The observation sees structure
|
|
13645
|
+
// (names, operators, directions), never a value, and any failure inside it
|
|
13646
|
+
// is its own: nothing below reads it to decide a result. Dropped without
|
|
13647
|
+
// being finished — an early `?` return, a cancelled request — it records an
|
|
13648
|
+
// error outcome for the shape.
|
|
13649
|
+
let observation = state.query_shapes.as_deref().map(|observer| {
|
|
13650
|
+
let predicates: Vec<(&str, &str)> = request
|
|
13651
|
+
.conditions
|
|
13652
|
+
.iter()
|
|
13653
|
+
.filter_map(|condition| {
|
|
13654
|
+
condition
|
|
13655
|
+
.operators
|
|
13656
|
+
.keys()
|
|
13657
|
+
.next()
|
|
13658
|
+
.map(|operator| (condition.field.as_str(), operator.as_str()))
|
|
13659
|
+
})
|
|
13660
|
+
.collect();
|
|
13661
|
+
let order_by: Vec<(&str, &str)> = request
|
|
13662
|
+
.order_by
|
|
13663
|
+
.iter()
|
|
13664
|
+
.map(|order| (order.field.as_str(), order.direction.as_str()))
|
|
13665
|
+
.collect();
|
|
13666
|
+
observer.begin(&ShapeInput {
|
|
13667
|
+
collection: &request.collection,
|
|
13668
|
+
predicates: &predicates,
|
|
13669
|
+
order_by: &order_by,
|
|
13670
|
+
limit: request.limit,
|
|
13671
|
+
has_semantic_clause: request.semantic.is_some(),
|
|
13672
|
+
})
|
|
13673
|
+
});
|
|
13674
|
+
let mut executed: Option<(Access, usize)> = None;
|
|
13631
13675
|
let (records, position) = if let Some(token) = &request.cursor {
|
|
13632
13676
|
let cursor = state
|
|
13633
13677
|
.bounded_query_cursors
|
|
@@ -13665,7 +13709,8 @@ async fn execute_bounded_query(
|
|
|
13665
13709
|
// this surface's contract, so the matching set — not the collection, and
|
|
13666
13710
|
// not an index bucket — is what gets sorted, and the limit is applied to
|
|
13667
13711
|
// that ordered set. Nothing below this line knows which execution ran.
|
|
13668
|
-
let matches = bounded_query_records(&state, &request, execution)?;
|
|
13712
|
+
let (matches, access) = bounded_query_records(&state, &request, execution)?;
|
|
13713
|
+
executed = Some((access, matches.len()));
|
|
13669
13714
|
// Result materialization happens outside the state lock, so it is a cost
|
|
13670
13715
|
// this request pays alone rather than one it imposes on every other
|
|
13671
13716
|
// operation. PR36 measures it separately for exactly that reason.
|
|
@@ -13724,6 +13769,12 @@ async fn execute_bounded_query(
|
|
|
13724
13769
|
} else {
|
|
13725
13770
|
None
|
|
13726
13771
|
};
|
|
13772
|
+
if let Some(observation) = observation {
|
|
13773
|
+
match executed {
|
|
13774
|
+
Some((access, matched)) => observation.finish_executed(access, matched, page.len()),
|
|
13775
|
+
None => observation.finish_continuation(page.len()),
|
|
13776
|
+
}
|
|
13777
|
+
}
|
|
13727
13778
|
// The semantic stage transforms exactly this page. It runs after the
|
|
13728
13779
|
// deterministic cursor is issued because continuation is by deterministic
|
|
13729
13780
|
// position: the page may return fewer records than `limit` and still not
|
|
@@ -13814,6 +13865,15 @@ fn query_execution_diagnostics_router() -> Router<AppState> {
|
|
|
13814
13865
|
get(query_execution_diagnostics),
|
|
13815
13866
|
);
|
|
13816
13867
|
}
|
|
13868
|
+
// Query-shape observations name collections and fields, so they are a
|
|
13869
|
+
// separate switch from the counters and exist only when the observer was
|
|
13870
|
+
// opted into. Counts and structure only; never a value.
|
|
13871
|
+
if ObserverConfig::from_environment().ok().flatten().is_some() {
|
|
13872
|
+
router = router.route(
|
|
13873
|
+
"/internal/query-shape-observations",
|
|
13874
|
+
get(query_shape_observations),
|
|
13875
|
+
);
|
|
13876
|
+
}
|
|
13817
13877
|
// Phase attribution is a separate switch from the execution counters,
|
|
13818
13878
|
// because it is a separate cost: the counters are a handful of atomic adds
|
|
13819
13879
|
// per query, while attribution reads the clock at every instrumented
|
|
@@ -13833,6 +13893,15 @@ fn query_execution_diagnostics_router() -> Router<AppState> {
|
|
|
13833
13893
|
router
|
|
13834
13894
|
}
|
|
13835
13895
|
|
|
13896
|
+
/// The observer's retained shapes and counters. Diagnostic evidence about
|
|
13897
|
+
/// workload structure, not proof that any change would help.
|
|
13898
|
+
async fn query_shape_observations(State(state): State<AppState>) -> Json<Value> {
|
|
13899
|
+
match state.query_shapes.as_deref() {
|
|
13900
|
+
Some(observer) => Json(json!({ "enabled": true, "observations": observer.snapshot() })),
|
|
13901
|
+
None => Json(json!({ "enabled": false })),
|
|
13902
|
+
}
|
|
13903
|
+
}
|
|
13904
|
+
|
|
13836
13905
|
/// Phase attribution: where elapsed time went, as counts and nanoseconds.
|
|
13837
13906
|
///
|
|
13838
13907
|
/// Test instrumentation, mounted only under `FELTDB_WORKLOAD_DIAGNOSTICS=1`.
|
|
@@ -14224,9 +14293,7 @@ async fn commit_transaction(
|
|
|
14224
14293
|
FlowError::CapabilityError(message) if message.starts_with("INVALID_READ_SET:") => {
|
|
14225
14294
|
ApiError(StatusCode::BAD_REQUEST, message)
|
|
14226
14295
|
}
|
|
14227
|
-
FlowError::
|
|
14228
|
-
if message.starts_with("TRANSACTION_ID_PAYLOAD_MISMATCH:") =>
|
|
14229
|
-
{
|
|
14296
|
+
FlowError::TransactionIdPayloadMismatch { .. } => {
|
|
14230
14297
|
ApiError::structured(
|
|
14231
14298
|
StatusCode::CONFLICT,
|
|
14232
14299
|
json!({
|