Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.14)
project(sero
VERSION 0.2.1
VERSION 0.2.2
DESCRIPTION "Sero: Embedded-first realtime service-oriented communication"
LANGUAGES CXX
)
Expand Down
2 changes: 1 addition & 1 deletion examples/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class Calculator : public sero::IService<Calculator> {

int main() {
example::UdpTransport transport;
if (!transport.init("127.0.0.1", SERVER_PORT)) return 1;
if (!transport.init("192.168.1.132", SERVER_PORT)) return 1;

Runtime rt(transport, MY_CLIENT_ID);
rt.set_local_address(transport.local_addr());
Expand Down
32 changes: 19 additions & 13 deletions include/sero/service/service_discovery.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class ServiceDiscovery {
st->requested_ttl = ttl_seconds;
st->granted_ttl = 0;
st->active = true;
st->app_registered = true;
st->needs_send = true;
st->last_sent_ms = now_ms;
return true;
Expand All @@ -225,6 +226,7 @@ class ServiceDiscovery {
bool unsubscribe_event(uint16_t service_id, uint16_t event_id) {
SubTracker* st = find_sub_tracker(service_id, event_id);
if (!st) return false;
st->app_registered = false; // app no longer wants this — never reactivate on reconnect
st->needs_unsubscribe = true;
return true;
}
Expand Down Expand Up @@ -570,15 +572,16 @@ class ServiceDiscovery {
};

struct SubTracker {
uint16_t service_id = 0;
uint16_t event_id = 0;
uint16_t requested_ttl = 0;
uint16_t granted_ttl = 0;
bool active = false;
bool needs_send = false;
uint16_t service_id = 0;
uint16_t event_id = 0;
uint16_t requested_ttl = 0;
uint16_t granted_ttl = 0;
bool active = false;
bool app_registered = false; ///< True while the app wants this subscription.
bool needs_send = false;
bool needs_unsubscribe = false;
uint32_t last_sent_ms = 0;
uint8_t ack_retry_count = 0; ///< Retries when no ACK received.
uint32_t last_sent_ms = 0;
uint8_t ack_retry_count = 0; ///< Retries when no ACK received.
};

std::array<ProviderEntry, Config::MaxServices> providers_{};
Expand Down Expand Up @@ -661,14 +664,17 @@ class ServiceDiscovery {

// ── Resubscription on reboot detection (§4.7) ────────────────

/// Flag all active subscriptions for a service as needing re-send.
/// Flag all app-registered subscriptions for a service as needing re-send.
/// Revives trackers whose retries were exhausted while the provider was away.
void flag_resubscribe(uint16_t service_id, uint32_t now_ms) {
for (std::size_t i = 0; i < sub_tracker_count_; ++i) {
if (sub_trackers_[i].active &&
if (sub_trackers_[i].app_registered &&
sub_trackers_[i].service_id == service_id) {
sub_trackers_[i].needs_send = true;
sub_trackers_[i].granted_ttl = 0;
sub_trackers_[i].last_sent_ms = now_ms;
sub_trackers_[i].active = true;
sub_trackers_[i].ack_retry_count = 0;
sub_trackers_[i].needs_send = true;
sub_trackers_[i].granted_ttl = 0;
sub_trackers_[i].last_sent_ms = now_ms;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion node-module/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@asl-gokart/sero-node",
"version": "0.2.1",
"version": "0.2.2",

"description": "sero Node.js bindings",
"main": "index.js",
Expand Down
40 changes: 40 additions & 0 deletions test/test_service_discovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,46 @@ TEST(ServiceDiscovery_UnsubscribeEvent, NotSubscribed_ReturnsFalse) {
EXPECT_FALSE(sd.unsubscribe_event(0x1000, 0x8001));
}

// ── Reconnect after retry exhaustion ───────────────────────────

/// Regression test: if ACK retries are exhausted while the provider is away
/// (inactive ≈ 30 s gap), a fresh SD_OFFER from the same service must
/// reactivate the subscription and trigger a new SD_SUBSCRIBE_EVENT send.
TEST(ServiceDiscovery_SubscribeEvent, RetryExhausted_ReactivatesOnReconnect) {
SD sd;
sd.set_client_id(0x0001);
SdCallbackState cb_state;
setup_callbacks(sd, cb_state);

sd.find(0x1000, 1, 0);
Addr provider = make_addr(42);
sd.handle_offer(0x1000, 1, 30, 0x0001, provider, 0);
sd.subscribe_event(0x1000, 0x8001, 30, 0);

// Exhaust all retries without receiving any ACK.
// Retry fires at t = SubscriptionTtlSeconds, 2×, 3× (3rd = exhaustion).
constexpr uint32_t ttl_ms = static_cast<uint32_t>(SmallConfig::SubscriptionTtlSeconds) * 1000u;
UnicastCapture uc;
uint32_t t = 0;
for (int retry = 0; retry <= 3; ++retry) {
t += ttl_ms;
sd.process_subscription_renewals(t, uc.fn());
}
// Subscription should now be inactive (retries exhausted).
uc.count = 0;
sd.process_subscription_renewals(t + ttl_ms, uc.fn());
EXPECT_EQ(uc.count, 0) << "No send expected while subscription is inactive";

// Provider comes back — new session_id simulates reboot / reconnect.
UnicastCapture resubscribe_uc;
t += ttl_ms;
sd.handle_offer(0x1000, 1, 30, 0x0002, provider, t);

// The next process cycle must send SD_SUBSCRIBE_EVENT.
sd.process_subscription_renewals(t, resubscribe_uc.fn());
EXPECT_GE(resubscribe_uc.count, 1) << "Expected SD_SUBSCRIBE_EVENT after provider reconnect";
}

// ── Provider address change ─────────────────────────────────────

TEST(ServiceDiscovery_HandleOffer, ProviderAddressChange_RenotifiesFound) {
Expand Down
Loading