~bigbes/tarantool-etcd · issues

c6rl6b6lve498os9seqlmc4b2bcjtvfn · 136 rows

idcontent_hashtitledescriptiondesignacceptance_criterianotesstatuspriorityissue_typeassigneeestimated_minutescreated_atcreated_byownerupdated_atclosed_atclosed_by_sessionexternal_refspec_idcompaction_levelcompacted_atcompacted_at_commitoriginal_sizesenderephemeralwisp_typepinnedis_templatemol_typework_typesource_systemmetadatasource_repoclose_reasonevent_kindactortargetpayloadawait_typeawait_idtimeout_nswaitershook_beadrole_beadagent_statelast_activityrole_typerigdue_atdefer_untilno_historystarted_atis_blocked
tarantool-etcd-ovob02eee9877621d78af7f66286e7ff63fc6564aac6beeca0cb795fe667217cb17Learner: durable applied_index (WAL-equivalent for restart)Persist enough raft state (currentTerm/HardState, applied_index, ConfState) so a restart resumes from the last applied entry instead of replaying from zero or forcing a full snapshot. Store in a dedicated space (e.g. etcd_raft_meta) written in the same transaction as each applied entry. On boot in learner mode: read last applied_index, reconnect transport, and request entries after it (or a snapshot if the leader has compacted past it). Tarantool's own WAL gives us durability of the *space writes*; this issue is specifically the raft bookkeeping that lets us re-attach to the cluster correctly.etcd stores this as raft WAL + snapshot metadata; we collapse it to a meta space since Tarantool's WAL already durably persists the applied data.open2taskNULLNULL2026-05-20T06:45:17ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1
tarantool-etcd-ovtdb6cf24009cca490a9e5a89f705f27d73388ddfcc3513448452e373522faeeb5iproto-verify: etcd_member_remove vs Cluster.MemberRemoveAdd Lua testkit verification for etcd_member_remove (mirrors gRPC etcdserverpb.Cluster.MemberRemove). Same test pattern as verify-only tasks: positive path plus at least one error path, response shape matches gRPC, error codes from app/etcd/errors.lua. For UNIMPLEMENTED-by-design endpoints (Cluster.MemberAdd/Remove/Update/Promote), assert the wrapper returns the canonical UNIMPLEMENTED error consistent with gRPC rather than skipping. Goes in test/iproto_parity_test.lua under group iproto/parity/cluster.Epic tarantool-etcd-w8q. Blocked on the matching impl issue.open3taskNULLNULL2026-05-19T14:54:21ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1
tarantool-etcd-ozm88152e03bc9011e822aa42ffc9212e11e04a9788bd5d40d5312f3e47da59657ciproto-verify: etcd_user_list vs Auth.UserListAdd Lua testkit verification for etcd_user_list (mirrors gRPC Auth.UserList). Successful path: assert response shape matches the gRPC response message after MsgPack round-trip. Error path: at least one error case asserting {code, message} matches app/etcd/errors.lua catalog. Exercise at least one non-default option if applicable. Goes in test/iproto_parity_test.lua under group iproto/parity/auth. Call shape: local resp = etcd_user_list(req).Verification-only. Wrapper exists at app/etcd/api.lua. Epic tarantool-etcd-w8q.open3taskNULLNULL2026-05-19T14:54:09ZEugene Blikhbigbes@gmail.com2026-05-19T14:54:09ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-p3sd7ab326ea50217052ff7528067d9bda449d1d177009210c5b6dc67df52f33a52iproto-verify: etcd_election_leader vs Election.LeaderAdd Lua testkit verification for etcd_election_leader (mirrors gRPC v3electionpb.Election.Leader). Same test pattern as verify-only tasks: positive path plus at least one error path, response shape matches gRPC, error codes from app/etcd/errors.lua. For UNIMPLEMENTED-by-design endpoints (Cluster.MemberAdd/Remove/Update/Promote), assert the wrapper returns the canonical UNIMPLEMENTED error consistent with gRPC rather than skipping. Goes in test/iproto_parity_test.lua under group iproto/parity/election.Epic tarantool-etcd-w8q. Blocked on the matching impl issue.open3taskNULLNULL2026-05-19T14:54:48ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1
tarantool-etcd-q5f722c317df5aebe05f32f99bf226d4b916604f1518089bea768b2ed206866ecd3Learner: snapshot receive (bbolt parse + bucket translation)Handle /raft/snapshot. When we fall too far behind (or on first join), the leader ships a bbolt DB file dump, NOT raft entries. Two options: (a) parse the bbolt file and translate etcd's buckets — key (the mvcc keyspace), meta (consistent_index, scheduledCompactRev, finishedCompactRev), lease, auth/authUsers/authRoles, members/members_removed, cluster, alarm — into our etcd_* spaces; or (b) refuse snapshots and bootstrap via clientv3 Range/Watch instead, accepting that the cluster MAY force a snapshot mid-stream if we lag past its compaction window (it eventually will). Recommend (a) for a true learner. Needs a minimal bbolt page/leaf reader in Lua or via FFI. This is the phase most likely to surprise — it bypasses the raft proto entirely.bbolt format: page-based B+tree, freelist, meta pages with txid. Bucket values for mvcc keys are mvccpb.KeyValue protobufs keyed by revision{main,sub}. This is the single biggest sub-effort in the epic.open2taskNULLNULL2026-05-20T06:45:16ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1
tarantool-etcd-qp91821d4049d7399c5a860e917014844d4b8ec8343d0ebed8371971c93410ce818MemberListRequest.linearizable decoded and droppedclientv3 sets linearizable=true by default since 3.5; the call must fail when the member is partitioned from quorum. That is the entire point of the flag — it is a quorum probe, not a freshness hint. app/etcd/cluster.lua:81 M.member_list() takes NO request argument and always serves local box.info.replication. The Range-only linearizable bounce (grpc.lua:180,221) does not cover MemberList. Nuance: our payload is never actually stale (membership is config-static, not raft-driven), so the RESPONSE is right — but the guarantee is absent, so a partitioned node answers confidently where etcd would fail. False-positive health signal.linearizable=true fails or bounces to leader when quorum is unavailable; linearizable=false keeps serving locally; test covers the partitioned case.open3bugNULLNULL2026-07-17T13:21:27ZEugene Blikhbigbes@gmail.com2026-07-17T13:21:27ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-rzh26beb784e16ef7c22030f27a08fe4e55b5e0453b6cd19af9df1ffd97a8bb3235iproto-impl: etcd_maintenance_status vs Maintenance.StatusWrap the existing handler maintenance.status (already invoked by gRPC/JSON via defs.lua route etcdserverpb.Maintenance.Status) as a Lua global etcd_maintenance_status callable via iproto CALL. Mechanical change in app/etcd/api.lua: add rawset(_G, 'etcd_maintenance_status', wrap('maintenance', 'status')) under the appropriate service block; append 'etcd_maintenance_status' to the funcs table so box.schema.func.create plus guest grant runs at role apply; document the wrapper in the module-level LDoc comment. If Maintenance.Status returns UNIMPLEMENTED today, the wrapper still gets added so the iproto caller observes the same UNIMPLEMENTED error gRPC would return. Election.Observe is server-streaming over gRPC; iproto has no stream primitive, so propose a polling shape (observe_create/_pull/_cancel) mirroring the Watch precedent at api.lua:81-83.etcd_maintenance_status is callable from net.box:call('etcd_maintenance_status', {req}) with no Lua error before handler dispatch. Successful request shape produces same successful response shape as gRPC. box.schema.func.create plus guest grant idempotent on role re-apply.Epic tarantool-etcd-w8q. Handler at app/etcd/maintenance.lua.open3taskNULLNULL2026-05-19T14:54:32ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-rzr3973dcbaf27d8ffaf25bdad2afcfcbfeb9e6deb07cd8ecf62c5a6446247c3468nghttp2 unary send-path rewrite — close parallel KVGet gap, drop jit.offParallel KVGet on tarantool is ~70% slower than etcd at GOMAXPROCS=6 (12 688 ops/s vs 21 900 ops/s; p95 970 µs vs 405 µs — docs/BENCH.md "Linux x86_64 — KVGet matrix, 2026-05-15"). Root cause: per-Connection IO mutex in lib/nghttp2/grpc.lua (Connection:_locked) — nghttp2 sessions are not reentrant, so every fiber's submit_response + send_all + sock:write serializes. Etcd has no such constraint. Fix: pre-build HEADERS + DATA + trailers in pure Lua, skip nghttp2's C→Lua data-source callback on the unary send path, recover ~10–20% throughput lost to jit.off + drop the JIT pin entirely.See memory project_nghttp2_send_callback_rewrite for the plan. Pre-frame DATA in Lua, avoid re-entering nghttp2 mid-send. Touch points: - lib/nghttp2/grpc.lua (Connection, _locked, send_response) - lib/nghttp2/ffi.lua (data-source callback signature) - M.recv/M.send/M.send_all/M.send_data currently pinned jit.off — un-pin once the callback re-entry is gone (memory feedback_nghttp2_session_not_reentrant explains the original panic).1. BenchmarkKVGetParallel/point closes the gap to within ~10% of etcd at GOMAXPROCS=6. 2. p95 latency under parallel load drops below 600 µs. 3. No bad-callback panics across 10 verbose runs (the regression test from commit d0fa396). 4. jit.off removed from M.recv/M.send/M.send_all/M.send_data.Sources: docs/BENCH.md "Linux x86_64" + memory notes project_nghttp2_send_callback_rewrite, feedback_nghttp2_session_not_reentrant, feedback_nghttp2_dual_connection.open2taskNULLNULL2026-05-19T13:34:42ZEugene Blikhbigbes@gmail.com2026-05-19T13:34:42ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-rzs63c2e0e3f3c883b2db5734e851457a1f568e3a1ebb871de515f13411bd7ecbd9iproto-impl: etcd_member_update vs Cluster.MemberUpdateWrap the existing handler cluster.member_update (already invoked by gRPC/JSON via defs.lua route etcdserverpb.Cluster.MemberUpdate) as a Lua global etcd_member_update callable via iproto CALL. Mechanical change in app/etcd/api.lua: add rawset(_G, 'etcd_member_update', wrap('cluster', 'member_update')) under the appropriate service block; append 'etcd_member_update' to the funcs table so box.schema.func.create plus guest grant runs at role apply; document the wrapper in the module-level LDoc comment. If Cluster.MemberUpdate returns UNIMPLEMENTED today, the wrapper still gets added so the iproto caller observes the same UNIMPLEMENTED error gRPC would return. Election.Observe is server-streaming over gRPC; iproto has no stream primitive, so propose a polling shape (observe_create/_pull/_cancel) mirroring the Watch precedent at api.lua:81-83.etcd_member_update is callable from net.box:call('etcd_member_update', {req}) with no Lua error before handler dispatch. Successful request shape produces same successful response shape as gRPC. box.schema.func.create plus guest grant idempotent on role re-apply.Epic tarantool-etcd-w8q. Handler at app/etcd/cluster.lua.open3taskNULLNULL2026-05-19T14:54:23ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-sdtce9fb24507b45a493bd9ffc4a4ab22edafe46b9e41a95b2813bb6b2016b0ccc8iproto-verify: etcd_auth_status vs Auth.AuthStatusAdd Lua testkit verification for etcd_auth_status (mirrors gRPC Auth.AuthStatus). Successful path: assert response shape matches the gRPC response message after MsgPack round-trip. Error path: at least one error case asserting {code, message} matches app/etcd/errors.lua catalog. Exercise at least one non-default option if applicable. Goes in test/iproto_parity_test.lua under group iproto/parity/auth. Call shape: local resp = etcd_auth_status(req).Verification-only. Wrapper exists at app/etcd/api.lua. Epic tarantool-etcd-w8q.open3taskNULLNULL2026-05-19T14:54:04ZEugene Blikhbigbes@gmail.com2026-05-19T14:54:04ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-sir5cc0da191ba98b3f835cc3fcdc3e3f5ed44926cb471cb6df204b16f644957871JWT auth-token mode (--auth-token=jwt) — we only have simple tokensetcd supports two auth-token modes: simple (an opaque token in a server-side table) and jwt (a signed, self-contained token the server verifies with a key, no server-side session state). etcd 3.7 leans further on JWT — 'clients may now set the JWT directly, offering more flexibility in authentication options.' We implement simple-token only: Auth.Authenticate mints an opaque token stored in the etcd_tokens space (truncated on auth disable, app/etcd/auth.lua). There is no JWT handling anywhere in app/etcd/. A client configured against a JWT-mode etcd, or one presenting a pre-issued JWT, gets rejected by us. This also matters for our own topology: simple tokens are server-local session state, so under a replicaset a token minted on one node is not honored by another unless etcd_tokens replicates and stays consistent. JWT is stateless and sidesteps that entirely. Scope: an auth.token config block selecting simple|jwt, JWT sign/verify (RS256 + HS256 at minimum) over the etcd claim set (username, revision), key material from config, and acceptance of a client-supplied JWT on both wires.Needs a JWT implementation in Lua — check what Tarantool ships (digest for HMAC/SHA, crypto for RSA) before pulling a dependency. Note digest.base64_encode MIME-wraps at 76 chars by default; JWT needs {nowrap = true} and URL-safe alphabet.Server boots in jwt mode; a token minted by Authenticate verifies on a different replicaset node without shared session state; a conformance cell runs the auth suite in jwt mode against both backends; simple mode stays the default and unchanged.open2featureNULLNULL2026-07-17T11:30:45ZEugene Blikhbigbes@gmail.com2026-07-17T11:30:45ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-sqve03dd24141b120acf0cc989ed396918ed61284c9962f1c036f30a262a51d39e6iproto-impl: etcd_election_proclaim vs Election.ProclaimWrap the existing handler election.proclaim (already invoked by gRPC/JSON via defs.lua route v3electionpb.Election.Proclaim) as a Lua global etcd_election_proclaim callable via iproto CALL. Mechanical change in app/etcd/api.lua: add rawset(_G, 'etcd_election_proclaim', wrap('election', 'proclaim')) under the appropriate service block; append 'etcd_election_proclaim' to the funcs table so box.schema.func.create plus guest grant runs at role apply; document the wrapper in the module-level LDoc comment. If Election.Proclaim returns UNIMPLEMENTED today, the wrapper still gets added so the iproto caller observes the same UNIMPLEMENTED error gRPC would return. Election.Observe is server-streaming over gRPC; iproto has no stream primitive, so propose a polling shape (observe_create/_pull/_cancel) mirroring the Watch precedent at api.lua:81-83.etcd_election_proclaim is callable from net.box:call('etcd_election_proclaim', {req}) with no Lua error before handler dispatch. Successful request shape produces same successful response shape as gRPC. box.schema.func.create plus guest grant idempotent on role re-apply.Epic tarantool-etcd-w8q. Handler at app/etcd/election.lua.open3taskNULLNULL2026-05-19T14:54:46ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-sy6ff088cd8c36175c942b8e80aa7a1bfa3bdaa64bf92a9ff46f8c736505ea0b302iproto-verify: etcd_watch_pull vs Watch.Watch-pullAdd Lua testkit verification for etcd_watch_pull (mirrors gRPC Watch.Watch-pull). Successful path: assert response shape matches the gRPC response message after MsgPack round-trip. Error path: at least one error case asserting {code, message} matches app/etcd/errors.lua catalog. Exercise at least one non-default option if applicable. Goes in test/iproto_parity_test.lua under group iproto/parity/watch. Call shape: local resp = etcd_watch_pull(req).Verification-only. Wrapper exists at app/etcd/api.lua. Epic tarantool-etcd-w8q.open3taskNULLNULL2026-05-19T14:53:57ZEugene Blikhbigbes@gmail.com2026-05-19T14:53:57ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-tf04410402a2773378d989b3453c6d566e9ab0eb85ec6a8ba9eb21801b478e96d73iproto-impl: etcd_maintenance_hash_kv vs Maintenance.HashKVWrap the existing handler maintenance.hash_kv (already invoked by gRPC/JSON via defs.lua route etcdserverpb.Maintenance.HashKV) as a Lua global etcd_maintenance_hash_kv callable via iproto CALL. Mechanical change in app/etcd/api.lua: add rawset(_G, 'etcd_maintenance_hash_kv', wrap('maintenance', 'hash_kv')) under the appropriate service block; append 'etcd_maintenance_hash_kv' to the funcs table so box.schema.func.create plus guest grant runs at role apply; document the wrapper in the module-level LDoc comment. If Maintenance.HashKV returns UNIMPLEMENTED today, the wrapper still gets added so the iproto caller observes the same UNIMPLEMENTED error gRPC would return. Election.Observe is server-streaming over gRPC; iproto has no stream primitive, so propose a polling shape (observe_create/_pull/_cancel) mirroring the Watch precedent at api.lua:81-83.etcd_maintenance_hash_kv is callable from net.box:call('etcd_maintenance_hash_kv', {req}) with no Lua error before handler dispatch. Successful request shape produces same successful response shape as gRPC. box.schema.func.create plus guest grant idempotent on role re-apply.Epic tarantool-etcd-w8q. Handler at app/etcd/maintenance.lua.open3taskNULLNULL2026-05-19T14:54:39ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-tt663510fbc9abb4568f1a181ec6515e1edededff7dbb3a0cf8e1f67e62b24c780dReplica route: lease / auth state mirroring (scope v1 vs later)KV is the v1 target; this issue scopes how non-KV state behaves in replica mode and what (if anything) v1 mirrors. Questions: - Leases: do we mirror upstream lease grants/TTLs/expiry so locally-served keys show correct lease attachment, or pass-through lease ops to upstream? At minimum the lease_id on replicated kvs must round-trip; full lease lifecycle mirroring may be later. - Auth: mirror upstream users/roles/permissions, or run replica reads unauthenticated/locally-authed? Decide whether auth is enforced on the replica or delegated. - Watch served to local clients: our Watch subsystem already reconstructs from kv_history, which the tail populates — confirm local watchers work transparently once KV sync lands. Output: a documented v1 scope (likely 'KV + lease_id round-trip only; auth+full-lease+ deferred') appended to docs/TODO.md, plus any follow-up issues.Likely mostly a scoping/decision issue that spawns concrete follow-ups; keep v1 KV-only.open2taskNULLNULL2026-05-20T06:17:57ZEugene Blikhbigbes@gmail.com2026-05-20T06:17:57ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-tzk3118f9aa62633997a681d45e16a91f84bed91eb8b469c292c08f094d08840e0dAcked writes are not fsynced — shipped config.yaml has no wal sectionetcd fdatasyncs the WAL before responding, so an acked write survives power loss. Our ack ORDERING is correct (box.commit() before the response — app/etcd/kv.lua:566-586, app/etcd/txn.lua:278-292), but the shipped config.yaml:30-34 has NO wal: section, so Tarantool defaults to wal_mode='write' (page cache, no fsync). An acked write survives a process crash but NOT power loss — weaker than etcd on the same wire ack. Telling detail: the bench harness explicitly sets WalMode="fsync" 'for symmetric durability' (tests/conformance/bench/benchharness/spawn.go:42-49). So our benchmarks measure fsync durability while the shipped default does not provide it. That asymmetry should be a decision, not an accident. Judgment call: arguably deployment config rather than a code gap — one line closes it. Filed so the choice is explicit either way.Either wal.mode: fsync in config.yaml, or an explicit docs/TODO.md divergence stating the shipped default trades durability for throughput and why the bench harness differs.open2bugNULLNULL2026-07-17T13:20:53ZEugene Blikhbigbes@gmail.com2026-07-17T13:20:53ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-ubj9fa2b019230fef524e54fc2a1286772300f5f2b92da91b3bef00be8c5e8c8613No auto-compaction (--auto-compaction-mode / --auto-compaction-retention)etcd advances compact_revision autonomously via --auto-compaction-mode=periodic|revision plus --auto-compaction-retention. Client-driven Compact is correct (app/etcd/compact.lua:28) but there is NO background fiber and NO config knob — zero hits for 'auto.?compact|retention' across app/, lib/, docs/. app/roles/etcd.lua:6-44 has no compaction block. Impact: history grows without bound unless an external actor calls Compact. A drop-in etcd replacement configured with auto-compaction gets none, and the operator has no signal — this is the kind of gap that surfaces as unbounded memory growth in production weeks later.Background fiber + roles_cfg.compaction.{mode,retention}. Interacts with the compact-revision boundary bug — land that first or the auto-compactor will mass-produce the watch livelock.periodic and revision modes both advance compact_revision; configurable via roles_cfg; disabled by default matching etcd; test covers both modes.open2featureNULLNULL2026-07-17T13:20:52ZEugene Blikhbigbes@gmail.com2026-07-17T13:20:52ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-uee9e6ca4342149c79eee16a063f881ae3eb1683b9de5774eae58d2b37bfc8bcb9bReplica route: bootstrap snapshot importOn entering replica mode, pull the full upstream keyspace once and load it into our etcd_* spaces, recording the upstream revision the snapshot was taken at (the Watch tail starts from here). Approach: a clientv3-style Range over the whole keyspace ([\\x00, \\x00) with WithRange/WithFromKey) reading at a single revision (use the header.revision from the first Range, or RangeRequest with revision pinned + paging via WithLimit + WithMinModRev/last-key continuation). Persist each kv with its create_revision/mod_revision/version/lease intact. Alternatively consume Maintenance.Snapshot if/when interop lands (tarantool-etcd-16d) — but Range is the dependency-free path for v1. Output: etcd_* spaces populated, and the 'synced through revision R' watermark persisted for the tail phase to resume from.Must preserve upstream create_revision/mod_revision/version exactly — feeds revision-alignment. Page large keyspaces to avoid one giant Range.open2taskNULLNULL2026-05-20T06:17:54ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1
tarantool-etcd-umcd8474c3796303d77baae0a958775f27a81b09ad2e0afaa4a00084cdf0ab6b7e9Learner: config block + role plumbingAdd the role config surface for learner mode and wire validate/apply/stop in app/roles/etcd.lua. New block under app.roles.etcd: learner: enabled: false cluster_peers: ['http://etcd-a:2380', ...] # PEER (2380) URLs, not client (2379) advertise_peer_url: 'http://this-node:2380' # how the cluster reaches us peer_tls: {...} Validate: when learner.enabled, the local store refuses normal client write registration; cluster_peers + advertise_peer_url non-empty; peer URLs are well-formed. No raft/transport logic here — just config parsing, validation, and the on/off switch the other phases hang off. Mirrors the existing grpc/json/cmux config validation in app/roles/etcd.lua.Distinct from the watch-mirror config (tarantool-etcd-jg3): peer (2380) URLs not client (2379), advertise_peer_url required for the cluster to dial us back.open2taskNULLNULL2026-05-20T06:45:13ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-unwf79d825d667d7493d1fbad53e65026978cea04ebcf650b8c77456f87f7698c30Tarantool metrics integrationRoadmap P0 blocker. Expose etcd-server operational metrics through Tarantool's metrics subsystem (request counts/latencies per RPC, watch/lease counts, revision, compaction, store size). Should surface via the standard Tarantool metrics collectors so existing TT monitoring (Prometheus exporter) picks them up. Consider parity with etcd's own /metrics names where it aids dashboards.Roadmap-only item; no metrics issue existed in beads.open0featureNULLNULL2026-05-20T06:55:23ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-uudfd643145ab1fb8229578bc7680a265a12fc602153dfd6682b33c3090c46b73d7WatchProgressRequest not stream-synchronised; wrong response shape (per-watcher ids instead of watch_id=-1)etcd sends ONE broadcast WatchResponse with watch_id=-1, deferred until every watcher on the stream is synced (watchable_store.go:510-539). We send one empty response PER WATCHER carrying its real watch id, emitted immediately from the recv-loop fiber while event fibers are still delivering asynchronously (app/etcd/grpc.lua:522-533 vs grpc.lua:336-427). Impact: clients keying on watch_id=-1 to recognise a progress broadcast do not see it; the 'all watchers caught up' guarantee is not provided. Race-shaped: argued from code structure, not from an observed failure — needs a reproducer first.RequestProgress emits a single watch_id=-1 response gated on all stream watchers being caught up; reproducer test exists.open2bugNULLNULL2026-07-17T13:20:15ZEugene Blikhbigbes@gmail.com2026-07-17T13:20:15ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-v01c1e139e7df4222c2657166c19c9aa0a7382320d2dfd608fc98ced8da400afee2Auth never re-checked on a live Watch stream (expired token / auth revision bump ignored)etcd surfaces ErrInvalidAuthToken / ErrAuthOldRevision on a live stream when the token expires or the auth revision changes, cancelling affected watchers. We resolve the token once at stream open and ignore failure (app/etcd/grpc.lua:263-267); READ permission is checked only at watcher create (app/etcd/watch.lua:250-252). The stream then keeps delivering with the cached identity forever. Grep: ErrAuthOldRevision has no analogue anywhere in app/. Impact: revoking a user's permission or deleting the user does not stop an already-open watch from streaming their keys. Security-relevant, hence P1 rather than P2.Re-validate the token per event batch and cancel watchers on increment_auth_revision. Needs a way to reach live watchers from the auth module — check whether the watcher registry (watch.lua:42-44) can be indexed by identity without adding contention on the hot delivery path.Revoking a role mid-stream cancels the watch with the etcd-shaped error; token expiry surfaces on the stream; a test pins both.open1bugNULLNULL2026-07-17T13:20:13ZEugene Blikhbigbes@gmail.com2026-07-17T13:20:13ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-vab80fa81f1f20e735f77e593b5e9975547a508502d5de68535ea41ac50091e1f7fAuthStatus must not require prior authentication (etcd 3.7)etcd 3.7: 'AuthStatus retrieval no longer requires prior authentication.' A client should be able to ask whether auth is enabled without already holding a token — which is the only sane ordering, since you need the answer to know whether to authenticate. Our JSON gateway gates every method except 'authenticate' behind a valid bearer token (app/etcd/json.lua:662), and the gRPC path uses the same predicate shape, so POST /v3/auth/status with no token returns 16 (Unauthenticated) once auth is enabled. Upstream 3.7 answers it. Fix: widen the exemption from a single method name to a small unauthenticated-method set containing 'authenticate' and 'status' (Auth.AuthStatus only — NOT Maintenance.Status, which routes to {'maintenance','status'} and shares the bare method name). Keep the gRPC and JSON gates driven by one predicate so they cannot drift.Match on the (service, method) pair, not the bare method string — routes.lua has both {'auth','status'} and {'maintenance','status'}. Check upstream 3.7 whether Maintenance.Status is also unauthenticated before changing it; this issue only claims Auth.AuthStatus.With auth enabled and no token: Auth.AuthStatus returns enabled=true + authRevision on both wires; every other method still returns Unauthenticated; a Lua test in test/auth_test.lua pins it.open3bugNULLNULL2026-07-17T11:30:22ZEugene Blikhbigbes@gmail.com2026-07-17T11:30:22ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-vnp1d9a61e1b6a987a8cb6ddae9833f7073ce29f05f3b852205dae442bfa24f3819Watch/Range reject compact_revision itself — off-by-one vs etcd, livelocks watch recoveryetcd keeps the tombstone whose revision == the compact revision and rejects only rev < compactMainRev (verified at server/storage/mvcc/kvstore_txn.go:80 — 'if rev < tr.s.compactMainRev'). A watch or range started exactly AT compact_revision succeeds. We reject at <= on both paths and delete the boundary row outright: - app/etcd/compact.lua:63-84 — the batch loop breaks on 'tuple.mod_revision > target_rev', so it deletes every kv_history row with mod_revision <= target_rev, INCLUDING the entry at target_rev. - app/etcd/watch.lua:281 — 'if opts.start_revision <= compacted then return errors.revision_compacted(compacted)' - app/etcd/kv.lua:261 — 'if target_rev <= compacted then error(errors.revision_compacted())' Why this is more than cosmetic: etcd's documented watch-recovery pattern is 'on a canceled response carrying compact_revision, restart the watch at compact_revision'. Against this server that revision is itself rejected, so a conforming clientv3 watcher that survives a compaction retries forever against the same rejection — an infinite cancel loop. Verified by hand (not just by the sweep agent) against both our source and the upstream clone.Two coupled changes; do them together or the boundary stays inconsistent: (1) compaction must retain rows with mod_revision == target_rev — break the loop on '>=' rather than deleting through the boundary; (2) both guards move from '<=' to '<'. Note the retained boundary row must be the tombstone/latest version at that revision, matching etcd's key_index.go behavior of keeping the generation's last entry.Compact(N) then Watch(start_revision=N) succeeds and delivers the event at N, matching etcd; Range(revision=N) after Compact(N) succeeds; a conformance test probes the compact-revision boundary itself on both backends (current compact_test.go only probes strictly below it); clientv3 restart-at-CompactRevision terminates.open0bugNULLNULL2026-07-17T13:18:41ZEugene Blikhbigbes@gmail.com2026-07-17T13:18:41ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-vruacf76de231548d849879d27ab87990e26de3591d1efb476d9744376f2e7c1a50SETTINGS_MAX_CONCURRENT_STREAMS advertised as 100; etcd advertises MaxUint32lib/nghttp2/grpc.lua:49-54 hardcodes {MAX_CONCURRENT_STREAMS, 100} in DEFAULT_SETTINGS, submitted on every connection (grpc.lua:381-385); lib/nghttp2/server.lua:18 carries the same 100. etcd's DefaultMaxConcurrentStreams = math.MaxUint32 (verified at server/embed/config.go:68), i.e. effectively unlimited since 3.2, tunable via --max-concurrent-streams. opts.session_options (grpc.lua:373) only reaches the peer-side knob (session.lua:260-261) — it never overrides DEFAULT_SETTINGS, so there is currently no way to raise this from config. Impact: past 100 concurrent streams on one connection (in-flight unary + Watch/Lease/Lock/Election all multiplex onto a single clientv3 connection), further streams are queued or REFUSED_STREAM where etcd never throttles. Scale-dependent: harmless for small deployments, a hard ceiling for watch-heavy ones.Raise the default to 0xFFFFFFFF and expose grpc.max_concurrent_streams in roles_cfg, wiring it through to DEFAULT_SETTINGS rather than the peer-side knob. Check whether our per-stream fiber model imposes a real resource ceiling that 100 was silently protecting — if so, the right number may not be MaxUint32, and that reasoning belongs in docs/TODO.md.Server advertises the configured value (default effectively unlimited); >100 concurrent streams on one connection all proceed; role option documented.open2bugNULLNULL2026-07-17T13:19:35ZEugene Blikhbigbes@gmail.com2026-07-17T13:19:35ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-w5r3a127352109f5847f90475b493f71b010fa77a962ab2ab7fd469ed01e25e9984iproto-impl: etcd_election_campaign vs Election.CampaignWrap the existing handler election.campaign (already invoked by gRPC/JSON via defs.lua route v3electionpb.Election.Campaign) as a Lua global etcd_election_campaign callable via iproto CALL. Mechanical change in app/etcd/api.lua: add rawset(_G, 'etcd_election_campaign', wrap('election', 'campaign')) under the appropriate service block; append 'etcd_election_campaign' to the funcs table so box.schema.func.create plus guest grant runs at role apply; document the wrapper in the module-level LDoc comment. If Election.Campaign returns UNIMPLEMENTED today, the wrapper still gets added so the iproto caller observes the same UNIMPLEMENTED error gRPC would return. Election.Observe is server-streaming over gRPC; iproto has no stream primitive, so propose a polling shape (observe_create/_pull/_cancel) mirroring the Watch precedent at api.lua:81-83.etcd_election_campaign is callable from net.box:call('etcd_election_campaign', {req}) with no Lua error before handler dispatch. Successful request shape produces same successful response shape as gRPC. box.schema.func.create plus guest grant idempotent on role re-apply.Epic tarantool-etcd-w8q. Handler at app/etcd/election.lua.open3taskNULLNULL2026-05-19T14:54:43ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-w6za1dfa11cf641fccae10ce051a21c5eaca457020ec09cb52c85f4e5cc8d0062c1iproto-verify: etcd_election_campaign vs Election.CampaignAdd Lua testkit verification for etcd_election_campaign (mirrors gRPC v3electionpb.Election.Campaign). Same test pattern as verify-only tasks: positive path plus at least one error path, response shape matches gRPC, error codes from app/etcd/errors.lua. For UNIMPLEMENTED-by-design endpoints (Cluster.MemberAdd/Remove/Update/Promote), assert the wrapper returns the canonical UNIMPLEMENTED error consistent with gRPC rather than skipping. Goes in test/iproto_parity_test.lua under group iproto/parity/election.Epic tarantool-etcd-w8q. Blocked on the matching impl issue.open3taskNULLNULL2026-05-19T14:54:44ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1
tarantool-etcd-w8q472222f8e9e50041b82f23593de29308c6f31d0649b21012f82ea3df9f38020ciproto parity for the gRPC surfaceBring app/etcd/api.lua iproto CALL surface to functional parity with the gRPC surface in app/etcd/pb/defs.lua. 32 already-implemented endpoints need Lua verification tests; 16 missing endpoints (Cluster, Maintenance, Election) need both implementation wrappers and tests. Out of scope: Watch and LeaseKeepAlive bidi-streams, Maintenance.Snapshot/Downgrade. For UNIMPLEMENTED-by-design endpoints, wrappers are created anyway and propagate the same UNIMPLEMENTED error so the surface is uniform.Every gRPC method in defs.lua (excluding streaming divergences) has a matching etcd_* iproto wrapper. Every wrapper has a Lua testkit test in test/iproto_parity_test.lua asserting response shape and error mapping match gRPC. test/run.lua group iproto/parity passes.open2epicNULLNULL2026-05-19T14:53:51ZEugene Blikhbigbes@gmail.com2026-05-19T14:53:51ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-wgx5a455111b3144aca15dfc7df9cce92515e6000d9cd0fa1e761c4ba493c83082fiproto-verify: etcd_authenticate vs Auth.AuthenticateAdd Lua testkit verification for etcd_authenticate (mirrors gRPC Auth.Authenticate). Successful path: assert response shape matches the gRPC response message after MsgPack round-trip. Error path: at least one error case asserting {code, message} matches app/etcd/errors.lua catalog. Exercise at least one non-default option if applicable. Goes in test/iproto_parity_test.lua under group iproto/parity/auth. Call shape: local resp = etcd_authenticate(req).Verification-only. Wrapper exists at app/etcd/api.lua. Epic tarantool-etcd-w8q.open3taskNULLNULL2026-05-19T14:54:05ZEugene Blikhbigbes@gmail.com2026-05-19T14:54:05ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-wqna7a2759a10e1b49b4c98ebf2bcc0979aff638f3764809fdee4605d8441e1adb6iproto-impl: etcd_maintenance_alarm vs Maintenance.AlarmWrap the existing handler maintenance.alarm (already invoked by gRPC/JSON via defs.lua route etcdserverpb.Maintenance.Alarm) as a Lua global etcd_maintenance_alarm callable via iproto CALL. Mechanical change in app/etcd/api.lua: add rawset(_G, 'etcd_maintenance_alarm', wrap('maintenance', 'alarm')) under the appropriate service block; append 'etcd_maintenance_alarm' to the funcs table so box.schema.func.create plus guest grant runs at role apply; document the wrapper in the module-level LDoc comment. If Maintenance.Alarm returns UNIMPLEMENTED today, the wrapper still gets added so the iproto caller observes the same UNIMPLEMENTED error gRPC would return. Election.Observe is server-streaming over gRPC; iproto has no stream primitive, so propose a polling shape (observe_create/_pull/_cancel) mirroring the Watch precedent at api.lua:81-83.etcd_maintenance_alarm is callable from net.box:call('etcd_maintenance_alarm', {req}) with no Lua error before handler dispatch. Successful request shape produces same successful response shape as gRPC. box.schema.func.create plus guest grant idempotent on role re-apply.Epic tarantool-etcd-w8q. Handler at app/etcd/maintenance.lua.open3taskNULLNULL2026-05-19T14:54:30ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-ymm5abccb539c482913cabd242acf2b32afd6eb182fda57781a502ac4411d704a0diproto-impl: etcd_maintenance_move_leader vs Maintenance.MoveLeaderWrap the existing handler maintenance.move_leader (already invoked by gRPC/JSON via defs.lua route etcdserverpb.Maintenance.MoveLeader) as a Lua global etcd_maintenance_move_leader callable via iproto CALL. Mechanical change in app/etcd/api.lua: add rawset(_G, 'etcd_maintenance_move_leader', wrap('maintenance', 'move_leader')) under the appropriate service block; append 'etcd_maintenance_move_leader' to the funcs table so box.schema.func.create plus guest grant runs at role apply; document the wrapper in the module-level LDoc comment. If Maintenance.MoveLeader returns UNIMPLEMENTED today, the wrapper still gets added so the iproto caller observes the same UNIMPLEMENTED error gRPC would return. Election.Observe is server-streaming over gRPC; iproto has no stream primitive, so propose a polling shape (observe_create/_pull/_cancel) mirroring the Watch precedent at api.lua:81-83.etcd_maintenance_move_leader is callable from net.box:call('etcd_maintenance_move_leader', {req}) with no Lua error before handler dispatch. Successful request shape produces same successful response shape as gRPC. box.schema.func.create plus guest grant idempotent on role re-apply.Epic tarantool-etcd-w8q. Handler at app/etcd/maintenance.lua.open3taskNULLNULL2026-05-19T14:54:41ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:36ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-zc747699b40eacc700cd65c09da563573fbd2ac63d67fd9ba709b6178394119ad21CLI utility: migrate existing config/data into tarantool-etcdProvide a configuration migration utility that moves an existing etcd configuration (and KV data) into a tarantool-etcd store, so users can adopt tarantool-etcd without hand-copying keys. Should cover the Tarantool EE centralized-config flow demonstrated in examples/tarantool-ee-self-hosted-etcd and examples/tarantool-ee-config: read config from a source (local cluster-config.yaml or a real etcd endpoint) and write it under the configured prefix into tarantool-etcd over the /v3 wire. Scope to nail down: source modes (file vs live etcd), key-range copy semantics, prefix handling, dry-run/diff output, and idempotency on re-run.open2featureNULLNULL2026-05-20T14:33:31ZEugene Blikhbigbes@gmail.com2026-05-20T14:33:31ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-zkg72a34c0ab42f90eede7cbbfd3ec5f7ef4f98cdbb5232099ad2926aaa87a61111Learner: conformance — join real etcd cluster, assert log replicationEnd-to-end test in tests/conformance: SpawnEtcd (or a small etcd cluster), MemberAddAsLearner the tarantool node with its advertise_peer_url, wait for it to become a healthy learner, then: (1) Put on the real etcd leader, assert it appears via Range on the tarantool learner with the SAME mod_revision; (2) assert MemberList on the cluster shows the tarantool node as IsLearner=true; (3) lease grant + key-with-lease on the leader replicates and the learner reflects lease attachment; (4) auth enable on the leader propagates; (5) force a snapshot (lower --snapshot-count, churn keys past compaction) and assert the learner recovers via snapshot receive. Use the harness, never tt start (memory feedback_use_harness_not_tt).This is the correctness oracle for the whole epic — each phase above is unverifiable in isolation against real etcd without it. Gate behind a build tag / env since it boots a foreign etcd cluster.open2taskNULLNULL2026-05-20T06:45:18ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1
tarantool-etcd-zl3a8b07f282a651d33df22d0c4f016408f1864fdcea3d059bb2d8b617e9585f9d3Maintenance RPCs not root-gated — and docs/TODO.md justifies it with a false claim about etcd 3.6TWO problems; the doc one is confirmed and independently verified, the code one is a real decision to make. 1) DOC BUG (verified by hand at the v3.6.0 tag). docs/TODO.md:130-132 states: 'Maintenance RPCs are not RBAC-gated — matches etcd 3.6, which doesn't permission-check Status/Hash/HashKV/Defragment/Alarm/MoveLeader.' That is factually wrong for 5 of the 6. At v3.6.0, NewMaintenanceServer returns &authMaintenanceServer{srv, &AuthAdmin{s}} — always wrapped — and Defragment/Snapshot/Hash/HashKV/Status/MoveLeader/Downgrade each call ams.isPermitted(ctx). Only Alarm is genuinely ungated, matching us. A deliberate divergence resting on a false premise is not a deliberate divergence; fix the entry regardless of what we decide to implement. 2) CODE GAP. Zero require_admin/require_permission in app/etcd/maintenance.lua and cluster.lua; auth.begin_grpc_request passes anonymous through (auth.lua:356-358). So any unauthenticated client can call Status/Hash/HashKV/Defragment/MoveLeader with auth enabled. Scope correction from the sweep's refuter: Cluster.MemberList non-gating DOES match v3.6.0; the requireAuthInfo relaxations are 3.7-dev only. Real scope is the 5 maintenance RPCs (Snapshot/Downgrade are unimplemented here anyway).docs/TODO.md:130-132 corrected to state what etcd 3.6.0 actually does; the 5 RPCs root-gated (or a corrected, true rationale recorded for keeping them open); Alarm stays ungated.open2bugNULLNULL2026-07-17T13:20:14ZEugene Blikhbigbes@gmail.com2026-07-17T13:20:14ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-zwg5f1f8ad7dadfa6be0ecaa0dc1d71f690076e930bc5073feb538b71faf1f2d7d6iproto-verify: etcd_user_get vs Auth.UserGetAdd Lua testkit verification for etcd_user_get (mirrors gRPC Auth.UserGet). Successful path: assert response shape matches the gRPC response message after MsgPack round-trip. Error path: at least one error case asserting {code, message} matches app/etcd/errors.lua catalog. Exercise at least one non-default option if applicable. Goes in test/iproto_parity_test.lua under group iproto/parity/auth. Call shape: local resp = etcd_user_get(req).Verification-only. Wrapper exists at app/etcd/api.lua. Epic tarantool-etcd-w8q.open3taskNULLNULL2026-05-19T14:54:08ZEugene Blikhbigbes@gmail.com2026-05-19T14:54:08ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL0
tarantool-etcd-zwsb99aec7b0025f26f4e59b5bf3e6287ee00f117d8f119775c13dcfbb23132d00eReplica route: revision alignment with upstreamIn replica mode the local global revision sequence (box.sequence in revision.lua) must mirror upstream's revisions exactly so that Range/Watch reads return etcd-consistent create_revision/mod_revision/header.revision. Work: - While replica mode is active, the local revision allocator must NOT mint its own revisions for replicated writes; it must set revision = the upstream mod_revision carried by each Range/Watch event. - Decide handling of the box.sequence: drive it to upstream's current revision (set, not next()) as events apply, or bypass it entirely for replicated mutations via the *_internal write paths. - Ensure header.revision returned to local clients equals the latest synced upstream revision, and compact_revision tracks upstream compaction. This is cross-cutting with bootstrap + watch-tail; it defines the invariant both must uphold.revision.lua owns the box.sequence. Replicated writes likely go through put_internal/delete_range_internal with an explicit revision arg to avoid double-allocation.open2taskNULLNULL2026-05-20T06:17:55ZEugene Blikhbigbes@gmail.com2026-07-17T14:27:37ZNULLNULL0NULLNULLNULL000<binary>0NULLNULLNULL0NULL1