diff --git a/lib/tdeck_ui/Telemetry/LocationPersistenceController.cpp b/lib/tdeck_ui/Telemetry/LocationPersistenceController.cpp new file mode 100644 index 00000000..6cbcc4ba --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationPersistenceController.cpp @@ -0,0 +1,256 @@ +#include "LocationPersistenceController.h" + +#include +#include +#include + +namespace Telemetry { + +bool LocationPersistenceController::observeMonotonic(uint64_t now_millis) { + if (has_monotonic_observation_ && now_millis < last_monotonic_millis_) { + state_ = LocationControllerState::BLOCKED; + return false; + } + last_monotonic_millis_ = now_millis; + has_monotonic_observation_ = true; + return true; +} + +bool LocationPersistenceController::collectSnapshot() { + std::size_t session_count = 0; + if (scheduler_.snapshot(scratch_.sessions, MAX_SHARE_SESSIONS, + session_count) != ShareSnapshotResult::OK) { + return false; + } + scratch_.session_count = session_count; + scratch_.location_count = peers_.durableSnapshot( + scratch_.locations, MAX_PEER_LOCATIONS); + return scratch_.location_count == peers_.size(); +} + +void LocationPersistenceController::markSaved() { + saved_scheduler_revision_ = scheduler_.revision(); + saved_peer_revision_ = peers_.revision(); + dirty_ = false; + dirty_since_monotonic_millis_ = 0; + next_save_attempt_monotonic_millis_ = 0; +} + +void LocationPersistenceController::observeDirty( + uint64_t monotonic_now_millis) { + if (scheduler_.revision() == saved_scheduler_revision_ && + peers_.revision() == saved_peer_revision_) { + return; + } + if (!dirty_) { + dirty_ = true; + dirty_since_monotonic_millis_ = monotonic_now_millis; + } +} + +bool LocationPersistenceController::restoreSnapshot( + uint64_t wall_now_millis, + uint64_t monotonic_now_millis) { + bool pruned = false; + for (std::size_t index = 0; index < scratch_.session_count; ++index) { + const ShareRestoreEntry& entry = scratch_.sessions[index]; + if (!entry.record.cease_pending && entry.record.has_expiry && + wall_now_millis >= entry.record.expires_at_millis) { + pruned = true; + continue; + } + if (scheduler_.restore(entry.peer, entry.record, wall_now_millis) != + ShareSessionResult::RESTORED) { + return false; + } + } + for (std::size_t index = 0; index < scratch_.location_count; ++index) { + const PeerLocationRecord& record = scratch_.locations[index]; + if (record.has_expiry && wall_now_millis >= record.expires_at_millis) { + pruned = true; + continue; + } + const PeerLocationResult result = peers_.restore(record); + if (result != PeerLocationResult::INSERTED && + result != PeerLocationResult::UPDATED) { + return false; + } + } + if (peers_.prune(wall_now_millis, + std::numeric_limits::max()) != 0) { + pruned = true; + } + saved_scheduler_revision_ = scheduler_.revision(); + saved_peer_revision_ = peers_.revision(); + if (pruned) { + dirty_ = true; + dirty_since_monotonic_millis_ = monotonic_now_millis; + } + return true; +} + +LocationControllerState LocationPersistenceController::service( + uint64_t wall_now_millis, + uint64_t monotonic_now_millis) { + if (state_ == LocationControllerState::BLOCKED || + !observeMonotonic(monotonic_now_millis)) { + return LocationControllerState::BLOCKED; + } + + if (state_ == LocationControllerState::WAITING_FOR_CLOCK) { + if (wall_now_millis < TRUSTED_WALL_CLOCK_MIN_MILLIS) return state_; + if (monotonic_now_millis < next_restore_attempt_monotonic_millis_) { + return state_; + } + const LocationPersistenceResult loaded = persistence_.load(scratch_); + if (loaded == LocationPersistenceResult::IO_ERROR) { + next_restore_attempt_monotonic_millis_ = + monotonic_now_millis > + std::numeric_limits::max() - + LOCATION_PERSISTENCE_CADENCE_MILLIS + ? std::numeric_limits::max() + : monotonic_now_millis + + LOCATION_PERSISTENCE_CADENCE_MILLIS; + return state_; + } + if (loaded == LocationPersistenceResult::UNAVAILABLE || + loaded == LocationPersistenceResult::INVALID_STATE || + loaded == LocationPersistenceResult::ENCODE_ERROR || + loaded == LocationPersistenceResult::SAVED) { + state_ = LocationControllerState::BLOCKED; + return state_; + } + if (loaded != LocationPersistenceResult::NOT_FOUND && + !restoreSnapshot(wall_now_millis, monotonic_now_millis)) { + state_ = LocationControllerState::BLOCKED; + return state_; + } + if (loaded == LocationPersistenceResult::NOT_FOUND) markSaved(); + state_ = LocationControllerState::READY; + } + + peers_.prune(wall_now_millis, std::numeric_limits::max()); + observeDirty(monotonic_now_millis); + if (dirty_ && + monotonic_now_millis - dirty_since_monotonic_millis_ >= + LOCATION_PERSISTENCE_CADENCE_MILLIS && + monotonic_now_millis >= next_save_attempt_monotonic_millis_) { + if (collectSnapshot() && + persistence_.save(scratch_) == LocationPersistenceResult::SAVED) { + markSaved(); + } else { + next_save_attempt_monotonic_millis_ = + monotonic_now_millis > + std::numeric_limits::max() - + LOCATION_PERSISTENCE_CADENCE_MILLIS + ? std::numeric_limits::max() + : monotonic_now_millis + + LOCATION_PERSISTENCE_CADENCE_MILLIS; + } + } + return state_; +} + +LocationControllerSaveResult LocationPersistenceController::urgentSave( + uint64_t monotonic_now_millis) { + if (state_ != LocationControllerState::READY || + !observeMonotonic(monotonic_now_millis)) { + return LocationControllerSaveResult::NOT_READY; + } + if (!collectSnapshot() || + persistence_.save(scratch_) != LocationPersistenceResult::SAVED) { + observeDirty(monotonic_now_millis); + next_save_attempt_monotonic_millis_ = + monotonic_now_millis > + std::numeric_limits::max() - + LOCATION_PERSISTENCE_CADENCE_MILLIS + ? std::numeric_limits::max() + : monotonic_now_millis + LOCATION_PERSISTENCE_CADENCE_MILLIS; + return LocationControllerSaveResult::STORAGE_FAILURE; + } + markSaved(); + return LocationControllerSaveResult::SAVED; +} + +LocationConsentResult LocationPersistenceController::mapSessionResult( + ShareSessionResult result) { + switch (result) { + case ShareSessionResult::STARTED: return LocationConsentResult::STARTED; + case ShareSessionResult::UPDATED: return LocationConsentResult::UPDATED; + case ShareSessionResult::STOPPING: return LocationConsentResult::STOPPING; + case ShareSessionResult::NOT_FOUND: return LocationConsentResult::NOT_FOUND; + case ShareSessionResult::CAPACITY: return LocationConsentResult::CAPACITY; + case ShareSessionResult::CLOCK_UNAVAILABLE: + return LocationConsentResult::CLOCK_UNAVAILABLE; + case ShareSessionResult::INVALID_ARGUMENT: + return LocationConsentResult::INVALID_ARGUMENT; + case ShareSessionResult::BUSY: return LocationConsentResult::BUSY; + case ShareSessionResult::RESTORED: + case ShareSessionResult::EXPIRED: + return LocationConsentResult::INVALID_ARGUMENT; + } + return LocationConsentResult::INVALID_ARGUMENT; +} + +void LocationPersistenceController::captureRollback(const PeerId& peer) { + const std::size_t index = scheduler_.find(peer); + rollback_existed_ = index != MAX_SHARE_SESSIONS; + rollback_session_ = rollback_existed_ + ? scheduler_.slots_[index].session + : ShareSession{}; + rollback_revision_ = scheduler_.revision_; +} + +void LocationPersistenceController::restoreRollback(const PeerId& peer) { + const std::size_t index = scheduler_.find(peer); + if (rollback_existed_) { + if (index != MAX_SHARE_SESSIONS) { + scheduler_.slots_[index].session = rollback_session_; + } + } else if (index != MAX_SHARE_SESSIONS) { + scheduler_.clear(index); + } + scheduler_.revision_ = rollback_revision_; +} + +LocationConsentResult LocationPersistenceController::startSharing( + const PeerId& peer, + const ShareStartOptions& options, + uint64_t wall_now_millis, + uint64_t monotonic_now_millis) { + if (state_ != LocationControllerState::READY || + !observeMonotonic(monotonic_now_millis)) { + return LocationConsentResult::NOT_READY; + } + captureRollback(peer); + const ShareSessionResult result = scheduler_.start(peer, options, wall_now_millis); + if (result != ShareSessionResult::STARTED && + result != ShareSessionResult::UPDATED) { + return mapSessionResult(result); + } + if (urgentSave(monotonic_now_millis) != LocationControllerSaveResult::SAVED) { + restoreRollback(peer); + return LocationConsentResult::STORAGE_FAILURE; + } + return mapSessionResult(result); +} + +LocationConsentResult LocationPersistenceController::stopSharing( + const PeerId& peer, + uint64_t wall_now_millis, + uint64_t monotonic_now_millis) { + if (state_ != LocationControllerState::READY || + !observeMonotonic(monotonic_now_millis)) { + return LocationConsentResult::NOT_READY; + } + captureRollback(peer); + const ShareSessionResult result = scheduler_.stop(peer, wall_now_millis); + if (result != ShareSessionResult::STOPPING) return mapSessionResult(result); + if (urgentSave(monotonic_now_millis) != LocationControllerSaveResult::SAVED) { + restoreRollback(peer); + return LocationConsentResult::STORAGE_FAILURE; + } + return LocationConsentResult::STOPPING; +} + +} // namespace Telemetry diff --git a/lib/tdeck_ui/Telemetry/LocationPersistenceController.h b/lib/tdeck_ui/Telemetry/LocationPersistenceController.h new file mode 100644 index 00000000..5a9c72d9 --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationPersistenceController.h @@ -0,0 +1,94 @@ +#ifndef PYXIS_TELEMETRY_LOCATION_PERSISTENCE_CONTROLLER_H +#define PYXIS_TELEMETRY_LOCATION_PERSISTENCE_CONTROLLER_H + +#include + +#include "LocationPersistence.h" + +namespace Telemetry { + +constexpr uint64_t TRUSTED_WALL_CLOCK_MIN_MILLIS = 1577836800000ULL; +constexpr uint64_t LOCATION_PERSISTENCE_CADENCE_MILLIS = 5000ULL; + +enum class LocationControllerState : uint8_t { + WAITING_FOR_CLOCK, + READY, + BLOCKED, +}; + +enum class LocationControllerSaveResult : uint8_t { + SAVED, + NOT_READY, + STORAGE_FAILURE, +}; + +enum class LocationConsentResult : uint8_t { + STARTED, + UPDATED, + STOPPING, + NOT_READY, + STORAGE_FAILURE, + NOT_FOUND, + CAPACITY, + CLOCK_UNAVAILABLE, + INVALID_ARGUMENT, + BUSY, +}; + +// Owns the bounded restore/save scratch. This object and the transactional +// persistence object must live in durable storage, never on a task stack. +class LocationPersistenceController { +public: + LocationPersistenceController(LocationShareScheduler& scheduler, + PeerLocationStore& peers, + TransactionalLocationPersistence& persistence) + : scheduler_(scheduler), peers_(peers), persistence_(persistence) {} + + LocationControllerState service(uint64_t wall_now_millis, + uint64_t monotonic_now_millis); + LocationControllerState state() const { return state_; } + bool dirty() const { return dirty_; } + + LocationControllerSaveResult urgentSave(uint64_t monotonic_now_millis); + LocationConsentResult startSharing(const PeerId& peer, + const ShareStartOptions& options, + uint64_t wall_now_millis, + uint64_t monotonic_now_millis); + LocationConsentResult stopSharing(const PeerId& peer, + uint64_t wall_now_millis, + uint64_t monotonic_now_millis); + +private: + bool observeMonotonic(uint64_t now_millis); + bool collectSnapshot(); + bool restoreSnapshot(uint64_t wall_now_millis, + uint64_t monotonic_now_millis); + void observeDirty(uint64_t monotonic_now_millis); + void markSaved(); + static LocationConsentResult mapSessionResult(ShareSessionResult result); + void captureRollback(const PeerId& peer); + void restoreRollback(const PeerId& peer); + + LocationShareScheduler& scheduler_; + PeerLocationStore& peers_; + TransactionalLocationPersistence& persistence_; + LocationStateSnapshot scratch_{}; + + LocationControllerState state_ = LocationControllerState::WAITING_FOR_CLOCK; + uint64_t saved_scheduler_revision_ = 0; + uint64_t saved_peer_revision_ = 0; + uint64_t dirty_since_monotonic_millis_ = 0; + uint64_t next_save_attempt_monotonic_millis_ = 0; + uint64_t next_restore_attempt_monotonic_millis_ = 0; + uint64_t last_monotonic_millis_ = 0; + bool has_monotonic_observation_ = false; + bool dirty_ = false; + + ShareSession rollback_session_{}; + uint64_t rollback_revision_ = 0; + bool rollback_existed_ = false; +}; + +} // namespace Telemetry + +#endif // PYXIS_TELEMETRY_LOCATION_PERSISTENCE_CONTROLLER_H diff --git a/lib/tdeck_ui/Telemetry/LocationShareScheduler.cpp b/lib/tdeck_ui/Telemetry/LocationShareScheduler.cpp index 6de4c683..133020f0 100644 --- a/lib/tdeck_ui/Telemetry/LocationShareScheduler.cpp +++ b/lib/tdeck_ui/Telemetry/LocationShareScheduler.cpp @@ -116,7 +116,7 @@ uint64_t LocationShareScheduler::retryDelay(uint8_t failure_count) { return delay > MAX_RETRY_MILLIS ? MAX_RETRY_MILLIS : delay; } -void LocationShareScheduler::scheduleRejectedWork( +bool LocationShareScheduler::scheduleRejectedWork( ShareSession& session, ShareWorkType type, uint64_t now_millis) { @@ -126,10 +126,11 @@ void LocationShareScheduler::scheduleRejectedWork( if (type == ShareWorkType::LOCATION && (session.cease_pending || (session.has_expiry && now_millis >= session.expires_at_millis))) { + const bool transitioned = !session.cease_pending; session.cease_pending = true; session.failure_count = 0; session.next_attempt_millis = now_millis; - return; + return transitioned; } if (session.failure_count < std::numeric_limits::max()) { ++session.failure_count; @@ -140,6 +141,7 @@ void LocationShareScheduler::scheduleRejectedWork( next = session.expires_at_millis; } session.next_attempt_millis = next; + return false; } std::size_t LocationShareScheduler::find(const PeerId& peer) const { @@ -222,6 +224,15 @@ ShareSessionResult LocationShareScheduler::start( if (target == NO_SLOT) return ShareSessionResult::CAPACITY; } + const bool durable_changed = !updating || + slots_[target].session.cadence_millis != options.cadence_millis || + slots_[target].session.has_approx_radius != + (options.has_approx_radius || options.approx_radius_meters != 0) || + slots_[target].session.approx_radius_meters != options.approx_radius_meters || + slots_[target].session.has_expiry != has_expiry || + slots_[target].session.expires_at_millis != expires_at_millis || + slots_[target].session.cease_pending; + ShareSession session{}; session.peer = peer; session.cadence_millis = options.cadence_millis; @@ -236,6 +247,7 @@ ShareSessionResult LocationShareScheduler::start( slots_[target].occupied = true; ++size_; } + if (durable_changed) ++revision_; return updating ? ShareSessionResult::UPDATED : ShareSessionResult::STARTED; } @@ -259,6 +271,7 @@ ShareSessionResult LocationShareScheduler::restore( observeClock(now_millis); std::size_t target = find(peer); + const bool existed = target != NO_SLOT; if (target != NO_SLOT && slots_[target].session.awaiting_ack) { return ShareSessionResult::BUSY; } @@ -267,6 +280,16 @@ ShareSessionResult LocationShareScheduler::restore( if (target == NO_SLOT) return ShareSessionResult::CAPACITY; } + const bool durable_changed = !existed || + slots_[target].session.cadence_millis != record.cadence_millis || + slots_[target].session.has_approx_radius != + (record.has_approx_radius || record.approx_radius_meters != 0) || + slots_[target].session.approx_radius_meters != record.approx_radius_meters || + slots_[target].session.has_expiry != record.has_expiry || + slots_[target].session.expires_at_millis != + (record.has_expiry ? record.expires_at_millis : 0) || + slots_[target].session.cease_pending != record.cease_pending; + ShareSession session{}; session.peer = peer; session.cadence_millis = record.cadence_millis; @@ -282,6 +305,7 @@ ShareSessionResult LocationShareScheduler::restore( slots_[target].occupied = true; ++size_; } + if (durable_changed) ++revision_; return ShareSessionResult::RESTORED; } @@ -297,7 +321,9 @@ ShareSessionResult LocationShareScheduler::stop( if (index == NO_SLOT) return ShareSessionResult::NOT_FOUND; observeClock(now_millis); ShareSession& session = slots_[index].session; + const bool first_stop = !session.cease_pending; session.cease_pending = true; + if (first_stop) ++revision_; if (session.awaiting_ack) { return ShareSessionResult::STOPPING; } @@ -311,6 +337,7 @@ bool LocationShareScheduler::cancelWithoutCease(const PeerId& peer) { if (index == NO_SLOT) return false; if (slots_[index].session.awaiting_ack) return false; clear(index); + ++revision_; return true; } @@ -335,6 +362,7 @@ SharePollResult LocationShareScheduler::poll( if (!session.cease_pending && session.has_expiry && wall_now_millis >= session.expires_at_millis) { session.cease_pending = true; + ++revision_; session.failure_count = 0; if (!session.awaiting_ack) { session.next_attempt_millis = wall_now_millis; @@ -343,7 +371,9 @@ SharePollResult LocationShareScheduler::poll( if (session.awaiting_ack && monotonic_now_millis >= session.ack_deadline_monotonic_millis) { const ShareWorkType expired_type = session.pending_type; - scheduleRejectedWork(session, expired_type, wall_now_millis); + if (scheduleRejectedWork(session, expired_type, wall_now_millis)) { + ++revision_; + } } if (session.awaiting_ack || wall_now_millis < session.next_attempt_millis) { @@ -405,7 +435,9 @@ ShareAckResult LocationShareScheduler::acknowledge( const ShareWorkType acknowledged_type = session.pending_type; if (monotonic_now_millis >= session.ack_deadline_monotonic_millis) { - scheduleRejectedWork(session, acknowledged_type, wall_now_millis); + if (scheduleRejectedWork(session, acknowledged_type, wall_now_millis)) { + ++revision_; + } return ShareAckResult::STALE_TOKEN; } session.awaiting_ack = false; @@ -415,6 +447,7 @@ ShareAckResult LocationShareScheduler::acknowledge( session.failure_count = 0; if (acknowledged_type == ShareWorkType::CEASE) { clear(index); + ++revision_; return ShareAckResult::CEASED; } @@ -423,7 +456,10 @@ ShareAckResult LocationShareScheduler::acknowledge( if (session.cease_pending || (session.has_expiry && wall_now_millis >= session.expires_at_millis)) { - session.cease_pending = true; + if (!session.cease_pending) { + session.cease_pending = true; + ++revision_; + } session.next_attempt_millis = wall_now_millis; } else { uint64_t next = boundedAdd( @@ -436,7 +472,9 @@ ShareAckResult LocationShareScheduler::acknowledge( return ShareAckResult::ACCEPTED; } - scheduleRejectedWork(session, acknowledged_type, wall_now_millis); + if (scheduleRejectedWork(session, acknowledged_type, wall_now_millis)) { + ++revision_; + } return ShareAckResult::RETRY_SCHEDULED; } diff --git a/lib/tdeck_ui/Telemetry/LocationShareScheduler.h b/lib/tdeck_ui/Telemetry/LocationShareScheduler.h index 920a2b7c..81ffc17e 100644 --- a/lib/tdeck_ui/Telemetry/LocationShareScheduler.h +++ b/lib/tdeck_ui/Telemetry/LocationShareScheduler.h @@ -8,6 +8,8 @@ namespace Telemetry { +class LocationPersistenceController; + constexpr std::size_t MAX_SHARE_SESSIONS = 32; constexpr uint32_t MIN_SHARE_CADENCE_MILLIS = 1000; constexpr uint32_t MAX_SHARE_CADENCE_MILLIS = 24U * 60U * 60U * 1000U; @@ -174,8 +176,10 @@ public: std::size_t capacity, std::size_t& written_or_required) const; std::size_t size() const { return size_; } + uint64_t revision() const { return revision_; } private: + friend class LocationPersistenceController; struct Slot { bool occupied = false; ShareSession session{}; @@ -185,7 +189,7 @@ private: static bool validCadence(uint32_t cadence_millis); static uint64_t boundedAdd(uint64_t value, uint64_t delta); static uint64_t retryDelay(uint8_t failure_count); - static void scheduleRejectedWork( + static bool scheduleRejectedWork( ShareSession& session, ShareWorkType type, uint64_t now_millis); @@ -202,6 +206,7 @@ private: uint64_t last_observed_millis_ = 0; uint64_t last_monotonic_millis_ = 0; bool has_monotonic_observation_ = false; + uint64_t revision_ = 0; }; } // namespace Telemetry diff --git a/lib/tdeck_ui/Telemetry/LocationShareState.cpp b/lib/tdeck_ui/Telemetry/LocationShareState.cpp index 21a10905..b107ae1a 100644 --- a/lib/tdeck_ui/Telemetry/LocationShareState.cpp +++ b/lib/tdeck_ui/Telemetry/LocationShareState.cpp @@ -116,6 +116,7 @@ void PeerLocationStore::clear(std::size_t index) { if (index >= MAX_PEER_LOCATIONS || !slots_[index].occupied) return; slots_[index] = Slot{}; --size_; + ++revision_; } PeerLocationResult PeerLocationStore::apply( @@ -164,6 +165,7 @@ PeerLocationResult PeerLocationStore::apply( record.received_at_millis = received_at_millis; record.has_expiry = meta.has_expires; record.expires_at_millis = expires_at_millis; + record.has_approx_radius = meta.has_approx_radius; record.approx_radius_meters = meta.has_approx_radius ? static_cast(meta.approx_radius_meters) @@ -171,6 +173,7 @@ PeerLocationResult PeerLocationStore::apply( if (existing != NO_SLOT) { slots_[existing].record = record; + ++revision_; return PeerLocationResult::UPDATED; } @@ -180,9 +183,43 @@ PeerLocationResult PeerLocationStore::apply( if (!slots_[target].occupied) ++size_; slots_[target].record = record; slots_[target].occupied = true; + ++revision_; return PeerLocationResult::INSERTED; } +PeerLocationResult PeerLocationStore::restore( + const PeerLocationRecord& record) { + if (record.source_timestamp_millis > + static_cast(std::numeric_limits::max()) || + record.expires_at_millis > + static_cast(std::numeric_limits::max()) || + record.approx_radius_meters > + static_cast(std::numeric_limits::max())) { + return PeerLocationResult::INVALID_ARGUMENT; + } + CustomLocationMeta meta{}; + meta.has_timestamp = true; + meta.timestamp_millis = static_cast(record.source_timestamp_millis); + meta.has_expires = record.has_expiry; + meta.expires_millis = static_cast(record.expires_at_millis); + meta.has_approx_radius = record.has_approx_radius; + meta.approx_radius_meters = static_cast(record.approx_radius_meters); + return apply(record.peer, record.location, meta, record.received_at_millis); +} + +std::size_t PeerLocationStore::durableSnapshot( + PeerLocationRecord* output, + std::size_t capacity) const { + if (output == nullptr || capacity == 0) return 0; + std::size_t copied = 0; + for (std::size_t index = 0; + index < MAX_PEER_LOCATIONS && copied < capacity; + ++index) { + if (slots_[index].occupied) output[copied++] = slots_[index].record; + } + return copied; +} + bool PeerLocationStore::get( const PeerId& peer, PeerLocationRecord& output) const { diff --git a/lib/tdeck_ui/Telemetry/LocationShareState.h b/lib/tdeck_ui/Telemetry/LocationShareState.h index 4b9cfbf5..db04ee83 100644 --- a/lib/tdeck_ui/Telemetry/LocationShareState.h +++ b/lib/tdeck_ui/Telemetry/LocationShareState.h @@ -23,6 +23,7 @@ struct PeerLocationRecord { uint64_t received_at_millis = 0; bool has_expiry = false; uint64_t expires_at_millis = 0; + bool has_approx_radius = false; uint32_t approx_radius_meters = 0; }; @@ -63,6 +64,10 @@ public: std::size_t prune(uint64_t now_millis, uint64_t maximum_age_millis); std::size_t size() const { return size_; } + uint64_t revision() const { return revision_; } + PeerLocationResult restore(const PeerLocationRecord& record); + std::size_t durableSnapshot(PeerLocationRecord* output, + std::size_t capacity) const; private: struct Slot { @@ -81,6 +86,7 @@ private: Slot slots_[MAX_PEER_LOCATIONS]{}; std::size_t size_ = 0; + uint64_t revision_ = 0; }; } // namespace Telemetry diff --git a/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp b/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp index d660e629..3c9168d5 100644 --- a/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp +++ b/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp @@ -13,6 +13,7 @@ constexpr uint8_t SESSION_HAS_EXPIRY = 0x01; constexpr uint8_t SESSION_CEASE_PENDING = 0x02; constexpr uint8_t SESSION_HAS_APPROX_RADIUS = 0x04; constexpr uint8_t LOCATION_HAS_EXPIRY = 0x01; +constexpr uint8_t LOCATION_HAS_APPROX_RADIUS = 0x02; uint16_t readU16(const uint8_t* data) { return static_cast( @@ -155,7 +156,9 @@ ShareRestoreEntry decodeSession(const uint8_t* data) { void encodeLocation(const PeerLocationRecord& record, uint8_t* output) { std::memcpy(output, record.peer.bytes, PEER_ID_SIZE); - output[16] = record.has_expiry ? LOCATION_HAS_EXPIRY : 0; + output[16] = static_cast( + (record.has_expiry ? LOCATION_HAS_EXPIRY : 0U) | + (record.has_approx_radius ? LOCATION_HAS_APPROX_RADIUS : 0U)); output[17] = 0; writeI32(output + 18, record.location.latitude_e6); writeI32(output + 22, record.location.longitude_e6); @@ -176,6 +179,8 @@ PeerLocationRecord decodeLocation(const uint8_t* data) { PeerLocationRecord record{}; std::memcpy(record.peer.bytes, data, PEER_ID_SIZE); record.has_expiry = (data[16] & LOCATION_HAS_EXPIRY) != 0; + record.has_approx_radius = + (data[16] & LOCATION_HAS_APPROX_RADIUS) != 0; record.location.latitude_e6 = readI32(data + 18); record.location.longitude_e6 = readI32(data + 22); record.location.altitude_cm = readI32(data + 26); @@ -214,7 +219,7 @@ LocationStateRecordResult validateEncodedRecords( payload + session_count * LOCATION_STATE_SESSION_BYTES; for (std::size_t index = 0; index < location_count; ++index) { const uint8_t* current = locations + index * LOCATION_STATE_LOCATION_BYTES; - if ((current[16] & ~0x01U) != 0 || current[17] != 0 || + if ((current[16] & ~0x03U) != 0 || current[17] != 0 || readU16(current + 40) != 0) { return LocationStateRecordResult::MALFORMED; } diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 4febe065..44c2940f 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -26,6 +26,8 @@ #include #include #include +#include +#include using namespace RNS; @@ -119,6 +121,21 @@ bool peerIdFromHash(const Bytes& hash, Telemetry::PeerId& output) { return true; } +template +T* allocateLocationObject(Args&&... args) { + void* memory = heap_caps_calloc( + 1, sizeof(T), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + return memory ? new (memory) T(std::forward(args)...) : nullptr; +} + +template +void releaseLocationObject(T*& object) { + if (!object) return; + object->~T(); + heap_caps_free(object); + object = nullptr; +} + class LiveLocationEnvelopeRouter : public Telemetry::LocationEnvelopeRouter { public: explicit LiveLocationEnvelopeRouter(::LXMF::LXMRouter& router) @@ -196,8 +213,13 @@ int UIManager::profile_to_codec2_mode(int profile) { return ULBWVoiceProfilePolicy::codecModeForProfile(profile); } -UIManager::UIManager(Reticulum& reticulum, ::LXMF::LXMRouter& router, ::LXMF::MessageStore& store) +UIManager::UIManager(Reticulum& reticulum, ::LXMF::LXMRouter& router, + ::LXMF::MessageStore& store, + bool location_filesystem_available) : _reticulum(reticulum), _router(router), _store(store), + _location_storage(nullptr), + _location_transaction(nullptr), + _location_persistence_controller(nullptr), _gps(nullptr), // Vanilla upstream RNS::Destination has no default ctor; construct in // a Type::NONE state, then assign a real Destination later. (The fork @@ -233,9 +255,26 @@ UIManager::UIManager(Reticulum& reticulum, ::LXMF::LXMRouter& router, ::LXMF::Me _pending_conversation_refresh(false), _last_conversation_refresh_ms(0) { memset((void*)_call_signal_queue, 0, sizeof(_call_signal_queue)); + _location_storage = allocateLocationObject< + Telemetry::LocationPersistenceLittleFS>(location_filesystem_available); + if (_location_storage) { + _location_transaction = allocateLocationObject< + Telemetry::TransactionalLocationPersistence>(*_location_storage); + } + if (_location_transaction) { + _location_persistence_controller = allocateLocationObject< + Telemetry::LocationPersistenceController>( + _location_shares, _peer_locations, *_location_transaction); + } + if (!_location_persistence_controller) { + ERROR("Location persistence allocation failed; sharing remains disabled"); + } } UIManager::~UIManager() { + releaseLocationObject(_location_persistence_controller); + releaseLocationObject(_location_transaction); + releaseLocationObject(_location_storage); // Clean up call state if (_call_state != CallState::IDLE || call_current_generation() != 0) { @@ -549,6 +588,12 @@ void UIManager::update() { // a rendering operation. With no explicit sessions this remains a no-op. const uint64_t wall_now_millis = static_cast(RNS::Utilities::OS::ltime()); + const uint64_t monotonic_now_millis = monotonicMillis(); + const Telemetry::LocationControllerState location_state = + _location_persistence_controller + ? _location_persistence_controller->service( + wall_now_millis, monotonic_now_millis) + : Telemetry::LocationControllerState::BLOCKED; Telemetry::GpsFixSample gps_sample{}; if (_gps) { gps_sample.location_valid = _gps->location.isValid(); @@ -566,15 +611,17 @@ void UIManager::update() { Telemetry::LocationTelemetry current_location{}; const bool current_location_valid = Telemetry::locationTelemetryFromGpsFix( gps_sample, wall_now_millis, current_location); - LiveLocationEnvelopeRouter location_router(_router); - const Telemetry::DispatchResult location_result = - Telemetry::dispatchLocationShare( + Telemetry::DispatchResult location_result = Telemetry::DispatchResult::NO_WORK; + if (location_state == Telemetry::LocationControllerState::READY) { + LiveLocationEnvelopeRouter location_router(_router); + location_result = Telemetry::dispatchLocationShare( _location_shares, wall_now_millis, - monotonicMillis(), + monotonic_now_millis, current_location_valid, current_location, location_router); + } if (location_result == Telemetry::DispatchResult::QUEUED) { INFO("Location telemetry queued"); } else if (location_result == Telemetry::DispatchResult::CEASE_QUEUED) { @@ -951,26 +998,39 @@ void UIManager::set_gps(TinyGPSPlus* gps) { } } -Telemetry::ShareSessionResult UIManager::start_location_sharing( +Telemetry::LocationConsentResult UIManager::start_location_sharing( const Bytes& peer_hash, const Telemetry::ShareStartOptions& options) { Telemetry::PeerId peer{}; if (!peerIdFromHash(peer_hash, peer)) { - return Telemetry::ShareSessionResult::INVALID_ARGUMENT; + return Telemetry::LocationConsentResult::INVALID_ARGUMENT; } - return _location_shares.start( - peer, options, - static_cast(RNS::Utilities::OS::ltime())); + if (!_location_persistence_controller) { + return Telemetry::LocationConsentResult::NOT_READY; + } + const uint64_t wall_now = + static_cast(RNS::Utilities::OS::ltime()); + const uint64_t monotonic_now = monotonicMillis(); + _location_persistence_controller->service(wall_now, monotonic_now); + return _location_persistence_controller->startSharing( + peer, options, wall_now, monotonic_now); } -Telemetry::ShareSessionResult UIManager::stop_location_sharing( +Telemetry::LocationConsentResult UIManager::stop_location_sharing( const Bytes& peer_hash) { Telemetry::PeerId peer{}; if (!peerIdFromHash(peer_hash, peer)) { - return Telemetry::ShareSessionResult::INVALID_ARGUMENT; + return Telemetry::LocationConsentResult::INVALID_ARGUMENT; } - return _location_shares.stop( - peer, static_cast(RNS::Utilities::OS::ltime())); + if (!_location_persistence_controller) { + return Telemetry::LocationConsentResult::NOT_READY; + } + const uint64_t wall_now = + static_cast(RNS::Utilities::OS::ltime()); + const uint64_t monotonic_now = monotonicMillis(); + _location_persistence_controller->service(wall_now, monotonic_now); + return _location_persistence_controller->stopSharing( + peer, wall_now, monotonic_now); } bool UIManager::get_location_share_session( @@ -1264,11 +1324,20 @@ void UIManager::on_message_received(::LXMF::LXMessage& message) { WARNING("Malformed inbound location field ignored"); } if (location_decision.apply_location) { - (void)_peer_locations.apply( - location_decision.authenticated_sender, - location_decision.location, - location_decision.meta, - location_decision.received_at_millis); + const Telemetry::PeerLocationResult location_result = + _peer_locations.apply( + location_decision.authenticated_sender, + location_decision.location, + location_decision.meta, + location_decision.received_at_millis); + if (_location_persistence_controller && + location_result != Telemetry::PeerLocationResult::STALE && + location_result != Telemetry::PeerLocationResult::NOT_FOUND && + location_result != Telemetry::PeerLocationResult::INVALID_ARGUMENT) { + _location_persistence_controller->service( + static_cast(RNS::Utilities::OS::ltime()), + monotonicMillis()); + } } if (!location_decision.persist) { INFO(" Location telemetry processed without chat persistence"); diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.h b/lib/tdeck_ui/UI/LXMF/UIManager.h index 2d999cb9..5a349e74 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.h +++ b/lib/tdeck_ui/UI/LXMF/UIManager.h @@ -43,6 +43,8 @@ #include "Telemetry/LocationMessagePolicy.h" #include "Telemetry/LocationFixAdapter.h" #include "Telemetry/LocationLxmfAdapter.h" +#include "Telemetry/LocationPersistenceController.h" +#include "Telemetry/LocationPersistenceLittleFS.h" #include #include @@ -76,7 +78,9 @@ public: * @param router LXMF router instance * @param store Message store instance */ - UIManager(RNS::Reticulum& reticulum, ::LXMF::LXMRouter& router, ::LXMF::MessageStore& store); + UIManager(RNS::Reticulum& reticulum, ::LXMF::LXMRouter& router, + ::LXMF::MessageStore& store, + bool location_filesystem_available); /** * Destructor @@ -191,10 +195,10 @@ public: // Location sharing is always explicit and peer-scoped. No session exists // until the UI calls start_location_sharing(). - Telemetry::ShareSessionResult start_location_sharing( + Telemetry::LocationConsentResult start_location_sharing( const RNS::Bytes& peer_hash, const Telemetry::ShareStartOptions& options); - Telemetry::ShareSessionResult stop_location_sharing( + Telemetry::LocationConsentResult stop_location_sharing( const RNS::Bytes& peer_hash); bool get_location_share_session( const RNS::Bytes& peer_hash, @@ -332,6 +336,9 @@ private: ::LXMF::MessageStore& _store; Telemetry::PeerLocationStore _peer_locations; Telemetry::LocationShareScheduler _location_shares; + Telemetry::LocationPersistenceLittleFS* _location_storage; + Telemetry::TransactionalLocationPersistence* _location_transaction; + Telemetry::LocationPersistenceController* _location_persistence_controller; TinyGPSPlus* _gps; RNS::Destination _lxst_destination; diff --git a/src/main.cpp b/src/main.cpp index 8e419106..4d711702 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -127,6 +127,7 @@ LXMRouter* router = nullptr; MessageStore* message_store = nullptr; PropagationNodeManager* propagation_manager = nullptr; UI::LXMF::UIManager* ui_manager = nullptr; +bool location_filesystem_available = false; TCPClientInterface* tcp_interface_impl = nullptr; Interface* tcp_interface = nullptr; SX1262Interface* lora_interface_impl = nullptr; @@ -955,6 +956,7 @@ void setup_hardware() { // can be deleted once the graft lands. static microStore::Adapters::LittleFSFileSystem fs("/littlefs"); persistent_storage_ready = fs.init(false); + location_filesystem_available = persistent_storage_ready; if (!persistent_storage_ready) { ERROR("FileSystem mount failed; preserving persistent data"); } else { @@ -1334,7 +1336,7 @@ void setup_ui_manager() { INFO("\n=== UI Manager Initialization ==="); // Create UI manager - ui_manager = new UI::LXMF::UIManager(*reticulum, *router, *message_store); + ui_manager = new UI::LXMF::UIManager(*reticulum, *router, *message_store, location_filesystem_available); if (!ui_manager->init()) { ERROR("UI manager initialization failed!"); diff --git a/tests/build_scripts/test_location_live_integration.py b/tests/build_scripts/test_location_live_integration.py index 0e761bb8..ce41e5e0 100644 --- a/tests/build_scripts/test_location_live_integration.py +++ b/tests/build_scripts/test_location_live_integration.py @@ -32,6 +32,33 @@ def test_live_location_control_surface_is_explicit_opt_in(): assert "start_location_sharing(" in header assert "stop_location_sharing(" in header assert "get_location_share_session(" in header + assert "LocationConsentResult start_location_sharing(" in header + + +def test_live_persistence_is_owned_and_serviced_before_dispatch_and_lvgl(): + cpp = CPP.read_text() + header = HEADER.read_text() + main = (ROOT / "src/main.cpp").read_text() + update = cpp[cpp.index("void UIManager::update()") : cpp.index("void UIManager::show_conversation_list")] + assert "LocationPersistenceController* _location_persistence_controller;" in header + assert "LocationStateSnapshot" not in update + assert "TransactionalLocationPersistence persistence" not in update + assert update.index("_location_persistence_controller->service") < update.index("dispatchLocationShare") + assert update.index("_location_persistence_controller->service") < update.index("LVGL_LOCK();") + assert "location_filesystem_available" in main + assert "fs.init(false)" in main + assert "UIManager(*reticulum, *router, *message_store, location_filesystem_available)" in main + assert "heap_caps_calloc" in cpp + + +def test_inbound_location_and_consent_controls_use_controller_durability(): + cpp = CPP.read_text() + inbound = cpp[cpp.index("void UIManager::on_message_received") : cpp.index("void UIManager::on_message_delivered")] + start = cpp[cpp.index("UIManager::start_location_sharing") : cpp.index("UIManager::get_location_share_session")] + assert "_location_persistence_controller->service" in inbound + assert "_peer_locations.apply" in inbound + assert "_location_persistence_controller->startSharing" in start + assert "_location_persistence_controller->stopSharing" in start def test_router_pump_is_not_run_under_lvgl_and_transient_delivery_skips_store_update(): diff --git a/tests/native/test_location_persistence_controller.cpp b/tests/native/test_location_persistence_controller.cpp new file mode 100644 index 00000000..5184f4b2 --- /dev/null +++ b/tests/native/test_location_persistence_controller.cpp @@ -0,0 +1,220 @@ +#include +#include +#include +#include +#include + +#include "Telemetry/LocationPersistenceController.h" + +namespace { +int passed = 0; +int failures = 0; +#define CHECK(expr) do { if (expr) { ++passed; } else { ++failures; std::cerr << "FAIL line " << __LINE__ << ": " #expr << '\n'; } } while (false) + +constexpr uint64_t WALL = Telemetry::TRUSTED_WALL_CLOCK_MIN_MILLIS + 100000; + +Telemetry::PeerId peer(uint8_t seed) { + Telemetry::PeerId id{}; + for (std::size_t i = 0; i < Telemetry::PEER_ID_SIZE; ++i) id.bytes[i] = static_cast(seed + i); + return id; +} + +Telemetry::LocationTelemetry location(uint64_t seconds) { + Telemetry::LocationTelemetry value{}; + value.latitude_e6 = 123; + value.longitude_e6 = -456; + value.timestamp_seconds = seconds; + value.sensor_timestamp_seconds = seconds; + return value; +} + +class MemoryStorage : public Telemetry::LocationPersistenceStorage { +public: + struct Slot { + bool exists = false; + std::size_t size = 0; + uint8_t bytes[Telemetry::MAX_LOCATION_STATE_RECORD_BYTES]{}; + } slots[3]; + bool mounted = true; + bool fail_io = false; + int stat_calls = 0; + int write_calls = 0; + + bool available() const override { return mounted; } + bool stat(Telemetry::LocationPersistenceSlot slot, bool& exists) override { + ++stat_calls; + exists = false; + if (fail_io || !mounted) return false; + exists = slots[static_cast(slot)].exists; + return true; + } + bool read(Telemetry::LocationPersistenceSlot slot, uint8_t* out, + std::size_t capacity, std::size_t& size) override { + size = 0; + Slot& value = slots[static_cast(slot)]; + if (fail_io || !value.exists || value.size > capacity) return false; + std::memcpy(out, value.bytes, value.size); + size = value.size; + return true; + } + bool write(Telemetry::LocationPersistenceSlot slot, const uint8_t* data, + std::size_t size) override { + ++write_calls; + if (fail_io || size > sizeof(slots[0].bytes)) return false; + Slot& value = slots[static_cast(slot)]; + value.exists = true; + value.size = size; + std::memcpy(value.bytes, data, size); + return true; + } + bool remove(Telemetry::LocationPersistenceSlot slot) override { + if (fail_io) return false; + slots[static_cast(slot)] = Slot{}; + return true; + } + bool rename(Telemetry::LocationPersistenceSlot from, + Telemetry::LocationPersistenceSlot to) override { + if (fail_io) return false; + slots[static_cast(to)] = slots[static_cast(from)]; + slots[static_cast(from)] = Slot{}; + return true; + } +}; + +void put(MemoryStorage& storage, const Telemetry::LocationStateSnapshot& state) { + std::size_t size = 0; + CHECK(Telemetry::encodeLocationStateRecord( + state, storage.slots[0].bytes, sizeof(storage.slots[0].bytes), size) == + Telemetry::LocationStateRecordResult::OK); + storage.slots[0].exists = true; + storage.slots[0].size = size; +} + +void clockGateRestoreAndPrune() { + MemoryStorage storage; + Telemetry::LocationStateSnapshot saved{}; + saved.session_count = 2; + saved.sessions[0].peer = peer(1); + saved.sessions[0].record.has_expiry = true; + saved.sessions[0].record.expires_at_millis = WALL - 1; + saved.sessions[1].peer = peer(2); + saved.sessions[1].record.cease_pending = true; + saved.location_count = 1; + saved.locations[0].peer = peer(3); + saved.locations[0].location = location(WALL / 1000); + saved.locations[0].source_timestamp_millis = WALL; + saved.locations[0].received_at_millis = WALL; + saved.locations[0].has_approx_radius = true; + put(storage, saved); + + Telemetry::PeerLocationStore peers; + Telemetry::LocationShareScheduler shares; + Telemetry::TransactionalLocationPersistence persistence(storage); + Telemetry::LocationPersistenceController controller(shares, peers, persistence); + CHECK(controller.service(1000, 10) == Telemetry::LocationControllerState::WAITING_FOR_CLOCK); + CHECK(storage.stat_calls == 0); + CHECK(controller.service(WALL, 20) == Telemetry::LocationControllerState::READY); + CHECK(shares.size() == 1); + Telemetry::ShareSession session{}; + CHECK(shares.get(peer(2), session) && session.cease_pending); + Telemetry::PeerLocationRecord record{}; + CHECK(peers.get(peer(3), record)); + CHECK(record.has_approx_radius && record.approx_radius_meters == 0); +} + +void unavailableCorruptAndIoRetryFailClosed() { + MemoryStorage unavailable; + unavailable.mounted = false; + Telemetry::PeerLocationStore p1; + Telemetry::LocationShareScheduler s1; + Telemetry::TransactionalLocationPersistence x1(unavailable); + Telemetry::LocationPersistenceController c1(s1, p1, x1); + CHECK(c1.service(WALL, 1) == Telemetry::LocationControllerState::BLOCKED); + + MemoryStorage corrupt; + corrupt.slots[0].exists = true; + corrupt.slots[0].size = 20; + Telemetry::PeerLocationStore p2; + Telemetry::LocationShareScheduler s2; + Telemetry::TransactionalLocationPersistence x2(corrupt); + Telemetry::LocationPersistenceController c2(s2, p2, x2); + CHECK(c2.service(WALL, 1) == Telemetry::LocationControllerState::BLOCKED); + + MemoryStorage retry; + retry.fail_io = true; + Telemetry::PeerLocationStore p3; + Telemetry::LocationShareScheduler s3; + Telemetry::TransactionalLocationPersistence x3(retry); + Telemetry::LocationPersistenceController c3(s3, p3, x3); + CHECK(c3.service(WALL, 100) == Telemetry::LocationControllerState::WAITING_FOR_CLOCK); + const int calls = retry.stat_calls; + retry.fail_io = false; + CHECK(c3.service(WALL, 5099) == Telemetry::LocationControllerState::WAITING_FOR_CLOCK); + CHECK(retry.stat_calls == calls); + CHECK(c3.service(WALL, 5100) == Telemetry::LocationControllerState::READY); +} + +void dirtyCadenceIsNonSlidingAndRetries() { + MemoryStorage storage; + Telemetry::PeerLocationStore peers; + Telemetry::LocationShareScheduler shares; + Telemetry::TransactionalLocationPersistence persistence(storage); + Telemetry::LocationPersistenceController controller(shares, peers, persistence); + CHECK(controller.service(WALL, 100) == Telemetry::LocationControllerState::READY); + Telemetry::CustomLocationMeta meta{}; + CHECK(peers.apply(peer(4), location(WALL / 1000), meta, WALL) == + Telemetry::PeerLocationResult::INSERTED); + CHECK(controller.service(WALL, 200) == Telemetry::LocationControllerState::READY); + CHECK(storage.write_calls == 0); + CHECK(peers.apply(peer(4), location(WALL / 1000 + 1), meta, WALL + 1) == + Telemetry::PeerLocationResult::UPDATED); + CHECK(controller.service(WALL + 1, 5199) == Telemetry::LocationControllerState::READY); + CHECK(storage.write_calls == 0); + storage.fail_io = true; + const int failed_attempt_calls = storage.stat_calls; + CHECK(controller.service(WALL + 1, 5200) == Telemetry::LocationControllerState::READY); + CHECK(storage.stat_calls > failed_attempt_calls); + storage.fail_io = false; + const int retry_wait_calls = storage.stat_calls; + CHECK(controller.service(WALL + 1, 10199) == Telemetry::LocationControllerState::READY); + CHECK(storage.stat_calls == retry_wait_calls); + CHECK(controller.service(WALL + 1, 10200) == Telemetry::LocationControllerState::READY); + CHECK(storage.write_calls >= 1); + CHECK(!controller.dirty()); +} + +void urgentConsentSaveRollsBackAndRollbackClockBlocks() { + MemoryStorage storage; + Telemetry::PeerLocationStore peers; + Telemetry::LocationShareScheduler shares; + Telemetry::TransactionalLocationPersistence persistence(storage); + Telemetry::LocationPersistenceController controller(shares, peers, persistence); + CHECK(controller.service(WALL, 100) == Telemetry::LocationControllerState::READY); + Telemetry::ShareStartOptions options{}; + options.duration = Telemetry::ShareDuration::INDEFINITE; + storage.fail_io = true; + CHECK(controller.startSharing(peer(5), options, WALL, 101) == + Telemetry::LocationConsentResult::STORAGE_FAILURE); + CHECK(shares.size() == 0); + storage.fail_io = false; + CHECK(controller.startSharing(peer(5), options, WALL, 102) == + Telemetry::LocationConsentResult::STARTED); + CHECK(shares.size() == 1); + storage.fail_io = true; + CHECK(controller.stopSharing(peer(5), WALL + 1, 103) == + Telemetry::LocationConsentResult::STORAGE_FAILURE); + Telemetry::ShareSession session{}; + CHECK(shares.get(peer(5), session) && !session.cease_pending); + CHECK(controller.service(WALL + 2, 102) == Telemetry::LocationControllerState::BLOCKED); +} +} // namespace + +int main() { + clockGateRestoreAndPrune(); + unavailableCorruptAndIoRetryFailClosed(); + dirtyCadenceIsNonSlidingAndRetries(); + urgentConsentSaveRollsBackAndRollbackClockBlocks(); + std::cout << "location persistence controller: " << passed << " passed, " + << failures << " failed\n"; + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/native/test_location_persistence_controller.py b/tests/native/test_location_persistence_controller.py new file mode 100644 index 00000000..141c5483 --- /dev/null +++ b/tests/native/test_location_persistence_controller.py @@ -0,0 +1,26 @@ +"""Compile and execute live location persistence-controller tests.""" +from pathlib import Path + +from native_test import compile_and_run + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] + + +def test_location_persistence_controller(tmp_path): + ran = compile_and_run( + tmp_path, + name="test_location_persistence_controller", + sources=[ + HERE / "test_location_persistence_controller.cpp", + ROOT / "lib/tdeck_ui/Telemetry/LocationPersistenceController.cpp", + ROOT / "lib/tdeck_ui/Telemetry/LocationPersistence.cpp", + ROOT / "lib/tdeck_ui/Telemetry/LocationStateRecord.cpp", + ROOT / "lib/tdeck_ui/Telemetry/LocationShareState.cpp", + ROOT / "lib/tdeck_ui/Telemetry/LocationShareScheduler.cpp", + ], + include_dirs=[ROOT / "lib/tdeck_ui"], + sanitize=True, + timeout=60, + ) + assert "0 failed" in ran.stdout diff --git a/tests/native/test_location_share_scheduler.cpp b/tests/native/test_location_share_scheduler.cpp index feb7798b..9a4201c1 100644 --- a/tests/native/test_location_share_scheduler.cpp +++ b/tests/native/test_location_share_scheduler.cpp @@ -66,6 +66,49 @@ void defaultsToNoSharing() { CHECK(!scheduler.get(peer(1), session)); } +void revisionsTrackOnlySerializedSessionMutations() { + Telemetry::LocationShareScheduler scheduler; + const auto id = peer(21); + CHECK(scheduler.revision() == 0); + CHECK(scheduler.start(id, options(Telemetry::ShareDuration::INDEFINITE), 1000) == + Telemetry::ShareSessionResult::STARTED); + CHECK(scheduler.revision() == 1); + + Telemetry::ShareWork work{}; + CHECK(scheduler.poll(1000, 5000, true, work) == + Telemetry::SharePollResult::WORK); + CHECK(scheduler.revision() == 1); + CHECK(scheduler.acknowledge(id, work.token, false, 1000, 5001) == + Telemetry::ShareAckResult::RETRY_SCHEDULED); + CHECK(scheduler.revision() == 1); + CHECK(scheduler.stop(id, 1001) == Telemetry::ShareSessionResult::STOPPING); + CHECK(scheduler.revision() == 2); + CHECK(scheduler.stop(id, 1002) == Telemetry::ShareSessionResult::STOPPING); + CHECK(scheduler.revision() == 2); + + CHECK(scheduler.poll(1002, 5002, false, work) == + Telemetry::SharePollResult::WORK); + CHECK(scheduler.revision() == 2); + CHECK(scheduler.acknowledge(id, work.token, true, 1002, 5003) == + Telemetry::ShareAckResult::CEASED); + CHECK(scheduler.revision() == 3); + CHECK(scheduler.stop(id, 1003) == Telemetry::ShareSessionResult::NOT_FOUND); + CHECK(scheduler.revision() == 3); + + Telemetry::LocationShareScheduler expiry; + CHECK(expiry.start(peer(22), options(), 1000) == + Telemetry::ShareSessionResult::STARTED); + const uint64_t before_expiry = expiry.revision(); + Telemetry::ShareSession session{}; + CHECK(expiry.get(peer(22), session)); + CHECK(expiry.poll(session.expires_at_millis, 1, false, work) == + Telemetry::SharePollResult::WORK); + CHECK(expiry.revision() == before_expiry + 1); + CHECK(expiry.poll(session.expires_at_millis, 2, false, work) == + Telemetry::SharePollResult::NO_WORK); + CHECK(expiry.revision() == before_expiry + 1); +} + void computesDurationAndMidnightBoundaries() { constexpr uint64_t now = 1700000000000ULL; struct Case { @@ -591,6 +634,7 @@ void expirationPreemptsAQueuedRetry() { int main() { defaultsToNoSharing(); + revisionsTrackOnlySerializedSessionMutations(); computesDurationAndMidnightBoundaries(); requestsImmediateWorkAndAdvancesOnlyAfterAcceptance(); retriesFailuresWithBoundedBackoffWithoutExtendingExpiry(); diff --git a/tests/native/test_location_share_state.cpp b/tests/native/test_location_share_state.cpp index a0900b1a..57b04bef 100644 --- a/tests/native/test_location_share_state.cpp +++ b/tests/native/test_location_share_state.cpp @@ -87,6 +87,45 @@ void insertsUpdatesAndRejectsStaleData() { CHECK(record.location.latitude_e6 == 400); } +void revisionsTrackOnlyDurableMutationsAndRadiusPresence() { + Telemetry::PeerLocationStore store; + const auto id = peer(70); + Telemetry::CustomLocationMeta meta{}; + CHECK(store.revision() == 0); + CHECK(store.apply(id, location(10), meta, 1000) == + Telemetry::PeerLocationResult::INSERTED); + CHECK(store.revision() == 1); + + Telemetry::PeerLocationRecord record{}; + CHECK(store.get(id, record)); + CHECK(!record.has_approx_radius); + CHECK(record.approx_radius_meters == 0); + + auto stale = metaTimestamp(9999); + CHECK(store.apply(id, location(9), stale, 1001) == + Telemetry::PeerLocationResult::STALE); + CHECK(store.revision() == 1); + + meta.has_approx_radius = true; + meta.approx_radius_meters = 0; + CHECK(store.apply(id, location(11), meta, 1002) == + Telemetry::PeerLocationResult::UPDATED); + CHECK(store.revision() == 2); + CHECK(store.get(id, record)); + CHECK(record.has_approx_radius); + CHECK(record.approx_radius_meters == 0); + + auto missing_cease = metaTimestamp(12000); + missing_cease.has_cease = true; + missing_cease.cease = true; + CHECK(store.apply(peer(71), location(12), missing_cease, 1003) == + Telemetry::PeerLocationResult::NOT_FOUND); + CHECK(store.revision() == 2); + CHECK(store.apply(id, location(12), missing_cease, 1003) == + Telemetry::PeerLocationResult::CEASED); + CHECK(store.revision() == 3); +} + void appliesOrderedCeaseWithoutTouchingOtherPeers() { Telemetry::PeerLocationStore store; Telemetry::CustomLocationMeta no_meta{}; @@ -329,6 +368,7 @@ void survivesDeterministicHundredThousandOperationStress() { int main() { insertsUpdatesAndRejectsStaleData(); + revisionsTrackOnlyDurableMutationsAndRadiusPresence(); appliesOrderedCeaseWithoutTouchingOtherPeers(); reusesVacanciesBeforeDeterministicEviction(); enforcesExpiryAndStaleDisplayBoundaries(); diff --git a/tests/native/test_location_state_record.cpp b/tests/native/test_location_state_record.cpp index d7dca1e1..f57c61da 100644 --- a/tests/native/test_location_state_record.cpp +++ b/tests/native/test_location_state_record.cpp @@ -301,6 +301,25 @@ void preservesPresentZeroApproximateRadius() { Telemetry::LocationStateRecordResult::OK); CHECK(decoded.sessions[0].record.has_approx_radius); CHECK(decoded.sessions[0].record.approx_radius_meters == 0); + + state.locations[0].has_approx_radius = false; + state.locations[0].approx_radius_meters = 0; + CHECK(Telemetry::encodeLocationStateRecord( + state, encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, decoded) == + Telemetry::LocationStateRecordResult::OK); + CHECK(!decoded.locations[0].has_approx_radius); + + state.locations[0].has_approx_radius = true; + CHECK(Telemetry::encodeLocationStateRecord( + state, encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, decoded) == + Telemetry::LocationStateRecordResult::OK); + CHECK(decoded.locations[0].has_approx_radius); + CHECK(decoded.locations[0].approx_radius_meters == 0); + CHECK(written == 140); } } // namespace