From 300f942d017eefae21c7c9bbaae1c1ac90ac52bb Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Sun, 13 Aug 2023 10:26:40 -0400 Subject: [PATCH 01/27] Added heartbeat bully leader election example --- .../C/src/leader-election/HeartbeatBully.lf | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 examples/C/src/leader-election/HeartbeatBully.lf diff --git a/examples/C/src/leader-election/HeartbeatBully.lf b/examples/C/src/leader-election/HeartbeatBully.lf new file mode 100644 index 00000000..8bf6d320 --- /dev/null +++ b/examples/C/src/leader-election/HeartbeatBully.lf @@ -0,0 +1,201 @@ +/** + * This program models a redundant fault tolerant system where a primary node, if and when it fails, + * is replaced by one of several backup nodes. The protocol is described in this paper: + * + * Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Heartbeat Bully: + * Failure Detection and Redundancy Role Selection for Network-Centric Controller," Proc. of the + * 46th Annual Conference of the IEEE Industrial Electronics Society (IECON), 8-21 October 2020. + * https://doi.org/10.1109/IECON43393.2020.9254494 + * + * The program has a bank of redundant nodes where exactly one is the primary node and the rest are + * backups. The primary node is always the one with the highest bank index that has not failed. The + * primary sends a heartbeat message once per second (by default). When the primary fails, a leader + * election protocol selects a new primary which then starts sending heartbeat messages. The program + * is set so that each primary fails after sending three heartbeat messages. When all nodes have + * failed, then the program exits. + * + * This example is designed to be run as a federated program with decentralized coordination. + * However, as of this writing, bugs in the federated code generator cause the program to fail to + * compile when you change it to be federated. See: + * + * - https://github.com/lf-lang/lingua-franca/issues/1942 + * - https://github.com/lf-lang/lingua-franca/issues/1940 + * + * When these bugs are fixed, then the federated version should operate exactly the same as the + * unfederated version except that it will become possible to kill the federates instead of having + * them fail on their own. The program should also be extended to include STP violation handlers to + * deal with the fundamental CAL theorem limitations, where unexpected network delays make it + * impossible to execute the program as designed. For example, if the network becomes partitioned, + * then it becomes possible to have two primary nodes simultaenously active. + * + * @author Edward A. Lee + * @author Marjan Sirjani + */ +target C + +preamble {= + enum message_type { + heartbeat, + reveal, + sorry + }; + typedef struct message_t { + enum message_type type; + int id; + } message_t; +=} + +reactor Node( + bank_index: int = 0, + num_nodes: int = 3, + heartbeat_period: time = 1 s, + max_missed_heartbeats: int = 2, + primary_fails_after_heartbeats: int = 3) { + input[num_nodes] in: message_t + output[num_nodes] out: message_t + + state heartbeats_missed: int = 0 + state primary_heartbeats_counter: int = 0 + + initial mode Idle { + reaction(startup) -> reset(Backup), reset(Primary) {= + if (self->bank_index == self->num_nodes - 1) { + lf_set_mode(Primary); + } else { + lf_set_mode(Backup); + } + =} + } + + mode Backup { + timer t(heartbeat_period, heartbeat_period) + reaction(in) -> out, reset(Prospect) {= + int primary_id = -1; + for (int i = 0; i < in_width; i++) { + if (in[i]->is_present && in[i]->value.id != self->bank_index) { + if (in[i]->value.type == heartbeat) { + if (primary_id >= 0) { + lf_print_error("Multiple primaries detected!!"); + } + primary_id = in[i]->value.id; + lf_print("Node %d received heartbeat from node %d.", self->bank_index, primary_id); + self->heartbeats_missed = 0; + } else if (in[i]->value.type == reveal && in[i]->value.id < self->bank_index) { + // NOTE: This will not occur if the LF semantics are followed because + // all nodes will (logically) simultaneously detect heartbeat failure and + // transition to the Prospect mode. But we include this anyway in case + // a federated version experiences a fault. + + // Send a sorry message. + message_t message; + message.type = sorry; + message.id = self->bank_index; + lf_set(out[in[i]->value.id], message); + lf_print("Node %d sends sorry to node %d", self->bank_index, in[i]->value.id); + // Go to Prospect mode to send reveal to any higher-priority nodes. + lf_set_mode(Prospect); + } + } + } + // FIXME + // =} STP (0) {= + // FIXME: What should we do here. + // lf_print_error("Node %d had an STP violation. Ignoring heartbeat as if it didn't arrive at all.", self->bank_index); + =} + + reaction(t) -> reset(Prospect) {= + if (self->heartbeats_missed > self->max_missed_heartbeats) { + lf_set_mode(Prospect); + } + // Increment the counter so if it's not reset to 0 by the next time, + // we detect the missed heartbeat. + self->heartbeats_missed++; + =} + } + + mode Primary { + timer heartbeat(0, heartbeat_period) + reaction(heartbeat) -> out, reset(Failed) {= + if (self->primary_heartbeats_counter++ >= self->primary_fails_after_heartbeats) { + // Stop sending heartbeats. + lf_print("**** Primary node %d fails.", self->bank_index); + lf_set_mode(Failed); + } else { + lf_print("Primary node %d sends heartbeat.", self->bank_index); + for (int i = 0; i < out_width; i++) { + if (i != self->bank_index) { + message_t message; + message.type = heartbeat; + message.id = self->bank_index; + lf_set(out[i], message); + } + } + } + =} + } + + mode Failed { + } + + mode Prospect { + logical action wait_for_sorry + reaction(reset) -> out, wait_for_sorry {= + lf_print("***** Node %d entered Prospect mode.", self->bank_index); + // Send a reveal message with my ID in a bid to become primary. + // NOTE: It is not necessary to send to nodes that have a lower + // priority than this node, but the connection is broadcast, so + // we send to all. + message_t message; + message.type = reveal; + message.id = self->bank_index; + for (int i = self->bank_index + 1; i < self->num_nodes; i++) { + lf_print("Node %d sends reveal to node %d", self->bank_index, i); + lf_set(out[i], message); + } + // The reveal message is delayed by heartbeat_period, and if + // there is a sorry response, it too will be delayed by heartbeat_period, + // so the total logical delay is twice heartbeat_period. + lf_schedule(wait_for_sorry, 2 * self->heartbeat_period); + =} + + reaction(in) -> out {= + for (int i = 0; i < in_width; i++) { + if (in[i]->value.type == reveal && in[i]->value.id < self->bank_index) { + // Send a sorry message. + message_t message; + message.type = sorry; + message.id = self->bank_index; + lf_set(out[in[i]->value.id], message); + lf_print("Node %d sends sorry to node %d", self->bank_index, in[i]->value.id); + } + } + =} + + reaction(wait_for_sorry) in -> reset(Backup), reset(Primary) {= + // Check for sorry messages. + // Sorry messages are guaranteed to be logically simultaneous + // with the wait_for_sorry event, so we just need to check for + // presence of sorry inputs. + int i; + for (i = 0; i < in_width; i++) { + if (in[i]->is_present && in[i]->value.type == sorry) { + // A sorry message arrived. Go to Backup mode. + lf_set_mode(Backup); + break; + } + } + if (i == in_width) { + // No sorry message arrived. Go to Primary mode. + lf_set_mode(Primary); + } + =} + } +} + +// FIXME: This should be federated, but it fails: +// See https://github.com/lf-lang/lingua-franca/issues/1942 +// and https://github.com/lf-lang/lingua-franca/issues/1940. +main reactor(num_nodes: int = 4, heartbeat_period: time = 1 s) { + nodes = new[num_nodes] Node(num_nodes=num_nodes, heartbeat_period=heartbeat_period) + nodes.out -> interleaved(nodes.in) after heartbeat_period +} From 8d9437b837c0a80191f5c7996767e3d9cfbc181a Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Sun, 13 Aug 2023 10:34:57 -0400 Subject: [PATCH 02/27] Typo --- examples/C/src/leader-election/HeartbeatBully.lf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/C/src/leader-election/HeartbeatBully.lf b/examples/C/src/leader-election/HeartbeatBully.lf index 8bf6d320..1bc25405 100644 --- a/examples/C/src/leader-election/HeartbeatBully.lf +++ b/examples/C/src/leader-election/HeartbeatBully.lf @@ -26,7 +26,7 @@ * them fail on their own. The program should also be extended to include STP violation handlers to * deal with the fundamental CAL theorem limitations, where unexpected network delays make it * impossible to execute the program as designed. For example, if the network becomes partitioned, - * then it becomes possible to have two primary nodes simultaenously active. + * then it becomes possible to have two primary nodes simultaneously active. * * @author Edward A. Lee * @author Marjan Sirjani From 044fbaf165daa73367b0ec9c5940acbd514e017f Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Tue, 15 Aug 2023 14:43:30 -0400 Subject: [PATCH 03/27] Another fault tolerance strategy --- examples/C/src/leader-election/NRP_FD.lf | 292 +++++++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 examples/C/src/leader-election/NRP_FD.lf diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf new file mode 100644 index 00000000..c041ab96 --- /dev/null +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -0,0 +1,292 @@ +/** + * This program models a redundant fault tolerant system where a primary node, if and when it fails, + * is replaced by a backup node. The protocol is described in this paper: + * + * Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Consistency Before + * Availability: Network Reference Point based Failure Detection for Controller Redundancy," + * paper draft 8/15/23. + * + * @author Edward A. Lee + * @author Marjan Sirjani + */ +target C + +preamble {= + enum message_type { + heartbeat, + reveal, + sorry, + pingNRP, + pingNRP_response + }; + typedef struct message_t { + enum message_type type; + int source; + int destination; + } message_t; +=} + +reactor Node( + id: int = 0, + heartbeat_period: time = 1 s, + max_missed_heartbeats: int = 2, + primary_fails_after_heartbeats: int = 6 +) { + + // There are two network interfaces: + @side("east") + input in1: message_t + @side("east") + input in2: message_t + output out1: message_t + output out2: message_t + + state heartbeats_missed_1: int = 0 + state heartbeats_missed_2: int = 0 + + state NRP_network: int = 1 + state NRP_switch_id: int = 1 + + state primary_heartbeats_counter: int = 0 + + initial mode Waiting { + reaction(startup) -> out1, out2 {= + message_t ping_message; + ping_message.type = pingNRP; + ping_message.source = self->id; + // We send a ping only to switch 1 at startup for this test case. + ping_message.destination = 1; + lf_set(out1, ping_message); + =} + reaction(in1, in2) -> reset(Backup), reset(Primary) {= + // Got a response to the ping from one or both switches. + // FIXME: The paper calls for user intervention to select which is primary. + // Here, we just choose id 1 to be primary. + if (self->id == 1) { + // Become primary. + lf_set_mode(Primary); + if (in1->is_present && in1->value.type == pingNRP_response) { + lf_print("Node %d received ping response from network 1. Primary is making switch %d the NRP.", self->id, in1->value.source); + self->NRP_network = 1; + self->NRP_switch_id = in1->value.source; + } else if (in2->is_present && in2->value.type == pingNRP) { + lf_print("Node %d received ping response from network 2. Primary is making switch %d the NRP.", self->id, in2->value.source); + self->NRP_network = 2; + self->NRP_switch_id = in2->value.source; + } else if (in1->is_present && in1->value.type == heartbeat) { + lf_print("Node %d received heartbeat from network 1.", self->id); + lf_set_mode(Backup); + } else if (in2->is_present && in2->value.type == heartbeat) { + lf_print("Node %d received heartbeat from network 2.", self->id); + lf_set_mode(Backup); + } + } else { + lf_set_mode(Backup); + } + =} + } + + mode Backup { + timer t(heartbeat_period, heartbeat_period) + // FIXME: Need SENDIMHERETOPRIMARY with "longer interval" + + reaction(in1) {= + if (in1->value.type == heartbeat) { + lf_print("Node %d received heartbeat from node %d on network 1.", self->id, in1->value.source); + self->heartbeats_missed_1 = 0; + } + =} + + reaction(in2) {= + if (in2->value.type == heartbeat) { + lf_print("Node %d received heartbeat from node %d on network 2.", self->id, in2->value.source); + self->heartbeats_missed_2 = 0; + } + =} + + reaction(t) -> reset(Primary) {= + if (self->heartbeats_missed_1 > self->max_missed_heartbeats) { + if (self->heartbeats_missed_2 > self->max_missed_heartbeats) { + // Simultaneous heartbeat misses. Assume the primary failed. + lf_set_mode(Primary); + } else { + // Heartbeat missed on network 1 but not yet on network 2. + // Possible network failure. + // FIXME + } + } else { + // Heartbeat missed on network 2 but not yet on network 1. + // Possible network failure. + // FIXME + } + // Increment the counters so if they are not reset to 0 by the next time, + // we detect the missed heartbeat. + self->heartbeats_missed_1++; + self->heartbeats_missed_2++; + =} + } + + mode Primary { + timer heartbeat(0, heartbeat_period) + reaction(reset) {= + lf_print("--- Node %d becomes primary.", self->id); + =} + reaction(heartbeat) -> out1, out2, reset(Failed) {= + if (self->primary_heartbeats_counter++ >= self->primary_fails_after_heartbeats) { + // Stop sending heartbeats. + lf_print("**** Primary node %d fails.", self->id); + lf_set_mode(Failed); + } else { + lf_print("Primary node %d sends heartbeat on both networks.", self->id); + message_t message; + message.type = heartbeat; + message.source = self->id; + lf_set(out1, message); + lf_set(out2, message); + } + =} + } + + mode Failed { + } +} + +/** + * Switch with two interfaces. Interface 1 is meant to be connected directly + * to a node, while interface 2 is meant to be connected to another switch. + * When a pingNRP message arrives on interface 1, the switch responds on interface 1 + * with the specified delay with a copy of the ping message. + * When any other message arrives on interface 1, the switch forwards a copy + * of the message to interface 2 with the specified delay. + * When a message arrives on interface 2, the message is forwarded to + * interface 1. + * If a pingNRP and message from the network are simultaneous, the + * response to the ping will be further delayed by the specified delay. + */ +reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { + input in1: message_t + @side("east") + input in2: message_t + @side("west") + output out1: message_t + output out2: message_t + + logical action pending_out1: message_t + logical action pending_out2: message_t + + timer switch_fails(switch_fails_after) + + reaction(pending_out1) -> out1 {= + lf_set(out1, pending_out1->value); + =} + + reaction(pending_out2) -> out2 {= + lf_set(out2, pending_out2->value); + =} + + reaction(in1, in2) -> out1, out2, pending_out1, pending_out2 {= + if (in1->is_present) { + if (in1->value.type == pingNRP) { + if (in1->value.destination == self->id) { + // Respond to the ping. + message_t message; + message.source = self->id; + message.destination = in1->value.source; + message.type = pingNRP_response; + if (!out1->is_present) { + lf_set(out1, message); + } else { + lf_schedule_copy(pending_out1, 0, &message, 1); + } + } else { + // Forward the ping. + if (!out2->is_present) { + lf_set(out2, in1->value); + } else { + lf_schedule_copy(pending_out2, 0, &in1->value, 1); + } + } + } else { + // Forward the message. + if (!out2->is_present) { + lf_set(out2, in1->value); + } else { + lf_schedule_copy(pending_out2, 0, &in1->value, 1); + } + } + } + if (in2->is_present) { + if (in2->value.type == pingNRP) { + if (in2->value.destination == self->id) { + // Construct a response to the ping. + message_t message; + message.source = self->id; + message.destination = in1->value.source; + message.type = pingNRP_response; + // Respond to the ping if out2 is available. + if (!out2->is_present) { + lf_set(out2, message); + } else { + lf_schedule_copy(pending_out2, 0, &message, 1); + } + } else { + // Forward the ping to out1 if out1 is available. + if (!out1->is_present) { + lf_set(out1, in2->value); + } else { + lf_schedule_copy(pending_out1, 0, &in2->value, 1); + } + } + } else { + // Forward the message if out1 is available. + if (!out1->is_present) { + lf_set(out1, in2->value); + } else { + lf_schedule_copy(pending_out1, 0, &in2->value, 1); + } + } + } + =} + + initial mode Working { + reaction(switch_fails) -> Failed {= + // Only switch 1 fails. + if (self->id == 1) { + lf_set_mode(Failed); + } + =} + } + mode Failed {} +} + +// FIXME: This should be federated, but it fails: +// See https://github.com/lf-lang/lingua-franca/issues/1942 +// and https://github.com/lf-lang/lingua-franca/issues/1940. +main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { + node1 = new Node(heartbeat_period=heartbeat_period, id = 1) + node2 = new Node(heartbeat_period=heartbeat_period, id = 2) + + switch1 = new Switch(id = 1) + switch2 = new Switch(id = 2) + + node1.out1 -> switch1.in1 after delay + switch1.out1 -> node1.in1 after delay + + switch1.out2 -> switch2.in2 after delay + switch2.out2 -> switch1.in2 after delay + + switch2.out1 -> node2.in1 after delay + node2.out1 -> switch2.in1 after delay + + switch3 = new Switch(id = 3) + switch4 = new Switch(id = 4) + + node1.out2 -> switch3.in1 after delay + switch3.out1 -> node1.in2 after delay + + switch3.out2 -> switch4.in2 after delay + switch4.out2 -> switch3.in2 after delay + + switch4.out1 -> node2.in2 after delay + node2.out2 -> switch4.in1 after delay +} From f92d08d2d47dc02c9b2093d14ef9cdaec65c8e4a Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Tue, 15 Aug 2023 14:47:16 -0400 Subject: [PATCH 04/27] Have switch failing --- examples/C/src/leader-election/NRP_FD.lf | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index c041ab96..da46f7d9 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -176,6 +176,8 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { timer switch_fails(switch_fails_after) + initial mode Working { + reaction(pending_out1) -> out1 {= lf_set(out1, pending_out1->value); =} @@ -248,15 +250,18 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { } =} - initial mode Working { - reaction(switch_fails) -> Failed {= + reaction(switch_fails) -> reset(Failed) {= // Only switch 1 fails. if (self->id == 1) { lf_set_mode(Failed); } =} } - mode Failed {} + mode Failed { + reaction(reset) {= + lf_print("######## Switch %d fails.", self->id); + =} + } } // FIXME: This should be federated, but it fails: From ec1ae5df565e139b959e90f4f74b3fb1b09703aa Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Tue, 15 Aug 2023 18:48:05 -0400 Subject: [PATCH 05/27] Ping NRP --- examples/C/src/leader-election/NRP_FD.lf | 68 +++++++++++++++--------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index da46f7d9..64a0e4b7 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -30,7 +30,7 @@ reactor Node( id: int = 0, heartbeat_period: time = 1 s, max_missed_heartbeats: int = 2, - primary_fails_after_heartbeats: int = 6 + primary_fails_after_heartbeats: int = 10 ) { // There are two network interfaces: @@ -88,12 +88,17 @@ reactor Node( mode Backup { timer t(heartbeat_period, heartbeat_period) + // FIXME: Need SENDIMHERETOPRIMARY with "longer interval" reaction(in1) {= if (in1->value.type == heartbeat) { lf_print("Node %d received heartbeat from node %d on network 1.", self->id, in1->value.source); self->heartbeats_missed_1 = 0; + } else if (in1->value.type == pingNRP_response && in1->value.destination == self->id) { + // Got a response from the NRP to a ping we sent after a partial timeout. + lf_print("Node %d received ping response on network 1 from NRP on switch %d.", self->id, in1->value.source); + // Don't do anything. Remain the backup. } =} @@ -101,23 +106,35 @@ reactor Node( if (in2->value.type == heartbeat) { lf_print("Node %d received heartbeat from node %d on network 2.", self->id, in2->value.source); self->heartbeats_missed_2 = 0; + } else if (in2->value.type == pingNRP_response && in2->value.destination == self->id) { + // Got a response from the NRP to a ping we sent after a partial timeout. + lf_print("Node %d received ping response on network 2 from NRP on switch %d.", self->id, in2->value.source); + // Don't do anything. Remain the backup. } =} - reaction(t) -> reset(Primary) {= - if (self->heartbeats_missed_1 > self->max_missed_heartbeats) { - if (self->heartbeats_missed_2 > self->max_missed_heartbeats) { - // Simultaneous heartbeat misses. Assume the primary failed. - lf_set_mode(Primary); + reaction(t) -> reset(Primary), out1, out2 {= + if (self->heartbeats_missed_1 > self->max_missed_heartbeats + && self->heartbeats_missed_2 > self->max_missed_heartbeats) { + // Simultaneous heartbeat misses. Assume the primary failed. + lf_set_mode(Primary); + } else if (self->heartbeats_missed_1 > self->max_missed_heartbeats + || self->heartbeats_missed_2 > self->max_missed_heartbeats) { + // Heartbeat missed on one network but not yet on the other. + // Possible network failure. + lf_print("**** Backup detects missing heartbeats on one network!"); + // Ping the NRP. + message_t message; + message.source = self->id; + message.type = pingNRP; + message.destination = self->NRP_switch_id; + if (self->NRP_network == 1) { + lf_set(out1, message); } else { - // Heartbeat missed on network 1 but not yet on network 2. - // Possible network failure. - // FIXME + lf_set(out2, message); } - } else { - // Heartbeat missed on network 2 but not yet on network 1. - // Possible network failure. - // FIXME + lf_print("Backup node %d pings NRP on network %d, switch %d", self->id, self->NRP_network, self->NRP_switch_id); + // FIXME: Set a timeout in case we don't get a pingNRP_response. } // Increment the counters so if they are not reset to 0 by the next time, // we detect the missed heartbeat. @@ -152,16 +169,13 @@ reactor Node( } /** - * Switch with two interfaces. Interface 1 is meant to be connected directly - * to a node, while interface 2 is meant to be connected to another switch. - * When a pingNRP message arrives on interface 1, the switch responds on interface 1 - * with the specified delay with a copy of the ping message. - * When any other message arrives on interface 1, the switch forwards a copy - * of the message to interface 2 with the specified delay. - * When a message arrives on interface 2, the message is forwarded to - * interface 1. - * If a pingNRP and message from the network are simultaneous, the - * response to the ping will be further delayed by the specified delay. + * Switch with two interfaces. + * When a pingNRP message arrives on either interface, if the destination + * matches the ID of this switch, then the switch responds on the same interface + * with a pingNRP_response message. + * When any other message arrives on either interface, the switch forwards a copy + * of the message to the other interface. + * If any two messages would be simultaneous on an output, one will be sent one microstep later. */ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { input in1: message_t @@ -190,6 +204,7 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { if (in1->is_present) { if (in1->value.type == pingNRP) { if (in1->value.destination == self->id) { + lf_print("\n=== Switch %d pinged by node %d. Responding.\n", self->id, in1->value.source); // Respond to the ping. message_t message; message.source = self->id; @@ -220,10 +235,11 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { if (in2->is_present) { if (in2->value.type == pingNRP) { if (in2->value.destination == self->id) { + lf_print("\n=== Switch %d pinged by node %d. Responding.\n", self->id, in2->value.source); // Construct a response to the ping. message_t message; message.source = self->id; - message.destination = in1->value.source; + message.destination = in2->value.source; message.type = pingNRP_response; // Respond to the ping if out2 is available. if (!out2->is_present) { @@ -251,8 +267,8 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { =} reaction(switch_fails) -> reset(Failed) {= - // Only switch 1 fails. - if (self->id == 1) { + // Only switch 3 fails. + if (self->id == 3) { lf_set_mode(Failed); } =} From d572b190c5ba13e5f4b420b63517366a143136ac Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Wed, 16 Aug 2023 05:56:59 -0400 Subject: [PATCH 06/27] Regularized how to test failures --- examples/C/src/leader-election/NRP_FD.lf | 118 ++++++++++++----------- 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index 64a0e4b7..ffb03041 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -30,7 +30,7 @@ reactor Node( id: int = 0, heartbeat_period: time = 1 s, max_missed_heartbeats: int = 2, - primary_fails_after_heartbeats: int = 10 + fails_at_time: time = 0 // For testing. 0 for no failure. ) { // There are two network interfaces: @@ -41,44 +41,44 @@ reactor Node( output out1: message_t output out2: message_t + timer node_fails(fails_at_time) + state heartbeats_missed_1: int = 0 state heartbeats_missed_2: int = 0 state NRP_network: int = 1 state NRP_switch_id: int = 1 - state primary_heartbeats_counter: int = 0 - + state primary: int = 0 // The known primary node. + initial mode Waiting { reaction(startup) -> out1, out2 {= - message_t ping_message; - ping_message.type = pingNRP; - ping_message.source = self->id; - // We send a ping only to switch 1 at startup for this test case. - ping_message.destination = 1; - lf_set(out1, ping_message); + message_t ping_message; + ping_message.type = pingNRP; + ping_message.source = self->id; + // NOTE: The paper does not specify how to select the initial NRP. + // Here, we just choose id 1 to be the NRP. + // Hence, we send a ping only to id 1 at startup, then wait for a reply before + // actually becoming the primary or backup. + ping_message.destination = 1; + lf_set(out1, ping_message); =} reaction(in1, in2) -> reset(Backup), reset(Primary) {= // Got a response to the ping from one or both switches. - // FIXME: The paper calls for user intervention to select which is primary. + // NOTE: The paper calls for user intervention to select which is primary. // Here, we just choose id 1 to be primary. + self->primary = 1; if (self->id == 1) { // Become primary. lf_set_mode(Primary); if (in1->is_present && in1->value.type == pingNRP_response) { - lf_print("Node %d received ping response from network 1. Primary is making switch %d the NRP.", self->id, in1->value.source); + lf_print("Primary node %d received ping response on network 1. Making switch %d the NRP.", self->id, in1->value.source); self->NRP_network = 1; self->NRP_switch_id = in1->value.source; } else if (in2->is_present && in2->value.type == pingNRP) { - lf_print("Node %d received ping response from network 2. Primary is making switch %d the NRP.", self->id, in2->value.source); + lf_print("Primary node %d received ping response on network 2. Making switch %d the NRP.", self->id, in2->value.source); self->NRP_network = 2; self->NRP_switch_id = in2->value.source; - } else if (in1->is_present && in1->value.type == heartbeat) { - lf_print("Node %d received heartbeat from network 1.", self->id); - lf_set_mode(Backup); - } else if (in2->is_present && in2->value.type == heartbeat) { - lf_print("Node %d received heartbeat from network 2.", self->id); - lf_set_mode(Backup); } } else { lf_set_mode(Backup); @@ -90,25 +90,33 @@ reactor Node( timer t(heartbeat_period, heartbeat_period) // FIXME: Need SENDIMHERETOPRIMARY with "longer interval" + + reaction(reset) {= + lf_print("---- Node %d becomes backup.", self->id); + =} + + reaction(node_fails) -> reset(Failed) {= + if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); + =} reaction(in1) {= if (in1->value.type == heartbeat) { - lf_print("Node %d received heartbeat from node %d on network 1.", self->id, in1->value.source); + lf_print("Backup node %d received heartbeat from node %d on network 1.", self->id, in1->value.source); self->heartbeats_missed_1 = 0; } else if (in1->value.type == pingNRP_response && in1->value.destination == self->id) { // Got a response from the NRP to a ping we sent after a partial timeout. - lf_print("Node %d received ping response on network 1 from NRP on switch %d.", self->id, in1->value.source); + lf_print("Backup node %d received ping response on network 1 from NRP on switch %d.", self->id, in1->value.source); // Don't do anything. Remain the backup. } =} reaction(in2) {= if (in2->value.type == heartbeat) { - lf_print("Node %d received heartbeat from node %d on network 2.", self->id, in2->value.source); + lf_print("Backup node %d received heartbeat from node %d on network 2.", self->id, in2->value.source); self->heartbeats_missed_2 = 0; } else if (in2->value.type == pingNRP_response && in2->value.destination == self->id) { // Got a response from the NRP to a ping we sent after a partial timeout. - lf_print("Node %d received ping response on network 2 from NRP on switch %d.", self->id, in2->value.source); + lf_print("Backup node %d received ping response on network 2 from NRP on switch %d.", self->id, in2->value.source); // Don't do anything. Remain the backup. } =} @@ -117,12 +125,13 @@ reactor Node( if (self->heartbeats_missed_1 > self->max_missed_heartbeats && self->heartbeats_missed_2 > self->max_missed_heartbeats) { // Simultaneous heartbeat misses. Assume the primary failed. + lf_print("**** Backup node %d detects missing heartbeats on both networks.", self->id); lf_set_mode(Primary); } else if (self->heartbeats_missed_1 > self->max_missed_heartbeats || self->heartbeats_missed_2 > self->max_missed_heartbeats) { // Heartbeat missed on one network but not yet on the other. // Possible network failure. - lf_print("**** Backup detects missing heartbeats on one network!"); + lf_print("**** Backup node %d detects missing heartbeats on one network.", self->id); // Ping the NRP. message_t message; message.source = self->id; @@ -134,7 +143,7 @@ reactor Node( lf_set(out2, message); } lf_print("Backup node %d pings NRP on network %d, switch %d", self->id, self->NRP_network, self->NRP_switch_id); - // FIXME: Set a timeout in case we don't get a pingNRP_response. + // FIXME: Set a timeout in case we don't get a pingNRP_response, in which case we should ask the primary to switch NRP. } // Increment the counters so if they are not reset to 0 by the next time, // we detect the missed heartbeat. @@ -146,25 +155,25 @@ reactor Node( mode Primary { timer heartbeat(0, heartbeat_period) reaction(reset) {= - lf_print("--- Node %d becomes primary.", self->id); + lf_print("---- Node %d becomes primary.", self->id); =} - reaction(heartbeat) -> out1, out2, reset(Failed) {= - if (self->primary_heartbeats_counter++ >= self->primary_fails_after_heartbeats) { - // Stop sending heartbeats. - lf_print("**** Primary node %d fails.", self->id); - lf_set_mode(Failed); - } else { - lf_print("Primary node %d sends heartbeat on both networks.", self->id); - message_t message; - message.type = heartbeat; - message.source = self->id; - lf_set(out1, message); - lf_set(out2, message); - } + reaction(node_fails) -> reset(Failed) {= + if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); + =} + reaction(heartbeat) -> out1, out2 {= + lf_print("Primary node %d sends heartbeat on both networks.", self->id); + message_t message; + message.type = heartbeat; + message.source = self->id; + lf_set(out1, message); + lf_set(out2, message); =} } mode Failed { + reaction(reset) {= + lf_print("#### Node %d fails.", self->id); + =} } } @@ -177,7 +186,10 @@ reactor Node( * of the message to the other interface. * If any two messages would be simultaneous on an output, one will be sent one microstep later. */ -reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { +reactor Switch( + id: int = 0, + fails_at_time: time = 0 // For testing. 0 for no failure. +) { input in1: message_t @side("east") input in2: message_t @@ -188,10 +200,14 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { logical action pending_out1: message_t logical action pending_out2: message_t - timer switch_fails(switch_fails_after) + timer switch_fails(fails_at_time) initial mode Working { + reaction(switch_fails) -> reset(Failed) {= + if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); + =} + reaction(pending_out1) -> out1 {= lf_set(out1, pending_out1->value); =} @@ -204,7 +220,7 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { if (in1->is_present) { if (in1->value.type == pingNRP) { if (in1->value.destination == self->id) { - lf_print("\n=== Switch %d pinged by node %d. Responding.\n", self->id, in1->value.source); + lf_print("==== Switch %d pinged by node %d. Responding.", self->id, in1->value.source); // Respond to the ping. message_t message; message.source = self->id; @@ -235,7 +251,7 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { if (in2->is_present) { if (in2->value.type == pingNRP) { if (in2->value.destination == self->id) { - lf_print("\n=== Switch %d pinged by node %d. Responding.\n", self->id, in2->value.source); + lf_print("==== Switch %d pinged by node %d. Responding.", self->id, in2->value.source); // Construct a response to the ping. message_t message; message.source = self->id; @@ -265,27 +281,17 @@ reactor Switch(id: int = 0, switch_fails_after: time = 3 s) { } } =} - - reaction(switch_fails) -> reset(Failed) {= - // Only switch 3 fails. - if (self->id == 3) { - lf_set_mode(Failed); - } - =} } mode Failed { reaction(reset) {= - lf_print("######## Switch %d fails.", self->id); + lf_print("==== Switch %d fails.", self->id); =} } } -// FIXME: This should be federated, but it fails: -// See https://github.com/lf-lang/lingua-franca/issues/1942 -// and https://github.com/lf-lang/lingua-franca/issues/1940. main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { - node1 = new Node(heartbeat_period=heartbeat_period, id = 1) - node2 = new Node(heartbeat_period=heartbeat_period, id = 2) + node1 = new Node(heartbeat_period=heartbeat_period, id = 1, fails_at_time = 10 s) + node2 = new Node(heartbeat_period=heartbeat_period, id = 2, fails_at_time = 15 s) switch1 = new Switch(id = 1) switch2 = new Switch(id = 2) @@ -299,7 +305,7 @@ main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { switch2.out1 -> node2.in1 after delay node2.out1 -> switch2.in1 after delay - switch3 = new Switch(id = 3) + switch3 = new Switch(id = 3, fails_at_time = 3 s) switch4 = new Switch(id = 4) node1.out2 -> switch3.in1 after delay From 60c4461e1ed59d0ce25258353b2b9e56fb9489a0 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Wed, 16 Aug 2023 05:58:01 -0400 Subject: [PATCH 07/27] Formatted --- examples/C/src/leader-election/NRP_FD.lf | 98 +++++++++++------------- 1 file changed, 43 insertions(+), 55 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index ffb03041..c81d8cd3 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -3,8 +3,8 @@ * is replaced by a backup node. The protocol is described in this paper: * * Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Consistency Before - * Availability: Network Reference Point based Failure Detection for Controller Redundancy," - * paper draft 8/15/23. + * Availability: Network Reference Point based Failure Detection for Controller Redundancy," paper + * draft 8/15/23. * * @author Edward A. Lee * @author Marjan Sirjani @@ -30,9 +30,8 @@ reactor Node( id: int = 0, heartbeat_period: time = 1 s, max_missed_heartbeats: int = 2, - fails_at_time: time = 0 // For testing. 0 for no failure. -) { - + // For testing. 0 for no failure. + fails_at_time: time = 0) { // There are two network interfaces: @side("east") input in1: message_t @@ -45,12 +44,12 @@ reactor Node( state heartbeats_missed_1: int = 0 state heartbeats_missed_2: int = 0 - + state NRP_network: int = 1 state NRP_switch_id: int = 1 - + state primary: int = 0 // The known primary node. - + initial mode Waiting { reaction(startup) -> out1, out2 {= message_t ping_message; @@ -63,6 +62,7 @@ reactor Node( ping_message.destination = 1; lf_set(out1, ping_message); =} + reaction(in1, in2) -> reset(Backup), reset(Primary) {= // Got a response to the ping from one or both switches. // NOTE: The paper calls for user intervention to select which is primary. @@ -88,17 +88,13 @@ reactor Node( mode Backup { timer t(heartbeat_period, heartbeat_period) - // FIXME: Need SENDIMHERETOPRIMARY with "longer interval" - - reaction(reset) {= - lf_print("---- Node %d becomes backup.", self->id); - =} + reaction(reset) {= lf_print("---- Node %d becomes backup.", self->id); =} reaction(node_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} - + reaction(in1) {= if (in1->value.type == heartbeat) { lf_print("Backup node %d received heartbeat from node %d on network 1.", self->id, in1->value.source); @@ -109,7 +105,7 @@ reactor Node( // Don't do anything. Remain the backup. } =} - + reaction(in2) {= if (in2->value.type == heartbeat) { lf_print("Backup node %d received heartbeat from node %d on network 2.", self->id, in2->value.source); @@ -154,12 +150,12 @@ reactor Node( mode Primary { timer heartbeat(0, heartbeat_period) - reaction(reset) {= - lf_print("---- Node %d becomes primary.", self->id); - =} + reaction(reset) {= lf_print("---- Node %d becomes primary.", self->id); =} + reaction(node_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} + reaction(heartbeat) -> out1, out2 {= lf_print("Primary node %d sends heartbeat on both networks.", self->id); message_t message; @@ -178,44 +174,37 @@ reactor Node( } /** - * Switch with two interfaces. - * When a pingNRP message arrives on either interface, if the destination - * matches the ID of this switch, then the switch responds on the same interface - * with a pingNRP_response message. - * When any other message arrives on either interface, the switch forwards a copy - * of the message to the other interface. - * If any two messages would be simultaneous on an output, one will be sent one microstep later. + * Switch with two interfaces. When a pingNRP message arrives on either interface, if the + * destination matches the ID of this switch, then the switch responds on the same interface with a + * pingNRP_response message. When any other message arrives on either interface, the switch forwards + * a copy of the message to the other interface. If any two messages would be simultaneous on an + * output, one will be sent one microstep later. */ reactor Switch( id: int = 0, - fails_at_time: time = 0 // For testing. 0 for no failure. -) { + // For testing. 0 for no failure. + fails_at_time: time = 0) { input in1: message_t @side("east") input in2: message_t @side("west") output out1: message_t output out2: message_t - + logical action pending_out1: message_t logical action pending_out2: message_t - + timer switch_fails(fails_at_time) - + initial mode Working { - reaction(switch_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} - reaction(pending_out1) -> out1 {= - lf_set(out1, pending_out1->value); - =} - - reaction(pending_out2) -> out2 {= - lf_set(out2, pending_out2->value); - =} - + reaction(pending_out1) -> out1 {= lf_set(out1, pending_out1->value); =} + + reaction(pending_out2) -> out2 {= lf_set(out2, pending_out2->value); =} + reaction(in1, in2) -> out1, out2, pending_out1, pending_out2 {= if (in1->is_present) { if (in1->value.type == pingNRP) { @@ -282,38 +271,37 @@ reactor Switch( } =} } + mode Failed { - reaction(reset) {= - lf_print("==== Switch %d fails.", self->id); - =} + reaction(reset) {= lf_print("==== Switch %d fails.", self->id); =} } } main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { - node1 = new Node(heartbeat_period=heartbeat_period, id = 1, fails_at_time = 10 s) - node2 = new Node(heartbeat_period=heartbeat_period, id = 2, fails_at_time = 15 s) - - switch1 = new Switch(id = 1) - switch2 = new Switch(id = 2) - + node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 10 s) + node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) + + switch1 = new Switch(id=1) + switch2 = new Switch(id=2) + node1.out1 -> switch1.in1 after delay switch1.out1 -> node1.in1 after delay - + switch1.out2 -> switch2.in2 after delay switch2.out2 -> switch1.in2 after delay - + switch2.out1 -> node2.in1 after delay node2.out1 -> switch2.in1 after delay - switch3 = new Switch(id = 3, fails_at_time = 3 s) - switch4 = new Switch(id = 4) - + switch3 = new Switch(id=3, fails_at_time = 3 s) + switch4 = new Switch(id=4) + node1.out2 -> switch3.in1 after delay switch3.out1 -> node1.in2 after delay - + switch3.out2 -> switch4.in2 after delay switch4.out2 -> switch3.in2 after delay - + switch4.out1 -> node2.in2 after delay node2.out2 -> switch4.in1 after delay } From 30e5ae02144673dee10a256dc577c0be3d1dce4d Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Wed, 16 Aug 2023 07:43:18 -0400 Subject: [PATCH 08/27] Switch supports broadcast ping --- examples/C/src/leader-election/NRP_FD.lf | 71 ++++++++++++++++++------ 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index c81d8cd3..e9c63996 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -14,10 +14,9 @@ target C preamble {= enum message_type { heartbeat, - reveal, - sorry, pingNRP, - pingNRP_response + pingNRP_response, + request_new_NRP }; typedef struct message_t { enum message_type type; @@ -30,8 +29,9 @@ reactor Node( id: int = 0, heartbeat_period: time = 1 s, max_missed_heartbeats: int = 2, - // For testing. 0 for no failure. - fails_at_time: time = 0) { + fails_at_time: time = 0, // For testing. 0 for no failure. + // Time until ping is deemed to have failed. + ping_timeout: time = 500 ms) { // There are two network interfaces: @side("east") input in1: message_t @@ -50,16 +50,19 @@ reactor Node( state primary: int = 0 // The known primary node. + state ping_pending: bool = false + logical action ping_timed_out(ping_timeout) + initial mode Waiting { reaction(startup) -> out1, out2 {= message_t ping_message; ping_message.type = pingNRP; ping_message.source = self->id; // NOTE: The paper does not specify how to select the initial NRP. - // Here, we just choose id 1 to be the NRP. - // Hence, we send a ping only to id 1 at startup, then wait for a reply before + // Here, we send with destination 0, which the switches interpret as first to respond. + // First to respond will be id 1 at startup; then wait for a reply before // actually becoming the primary or backup. - ping_message.destination = 1; + ping_message.destination = 0; lf_set(out1, ping_message); =} @@ -113,11 +116,12 @@ reactor Node( } else if (in2->value.type == pingNRP_response && in2->value.destination == self->id) { // Got a response from the NRP to a ping we sent after a partial timeout. lf_print("Backup node %d received ping response on network 2 from NRP on switch %d.", self->id, in2->value.source); + self->ping_pending = false; // Don't do anything. Remain the backup. } =} - reaction(t) -> reset(Primary), out1, out2 {= + reaction(t) -> reset(Primary), out1, out2, ping_timed_out {= if (self->heartbeats_missed_1 > self->max_missed_heartbeats && self->heartbeats_missed_2 > self->max_missed_heartbeats) { // Simultaneous heartbeat misses. Assume the primary failed. @@ -139,13 +143,32 @@ reactor Node( lf_set(out2, message); } lf_print("Backup node %d pings NRP on network %d, switch %d", self->id, self->NRP_network, self->NRP_switch_id); - // FIXME: Set a timeout in case we don't get a pingNRP_response, in which case we should ask the primary to switch NRP. + self->ping_pending = true; + lf_schedule(ping_timed_out, 0); } // Increment the counters so if they are not reset to 0 by the next time, // we detect the missed heartbeat. self->heartbeats_missed_1++; self->heartbeats_missed_2++; =} + + reaction(ping_timed_out) -> out1, out2 {= + if (self->ping_pending) { + // Ping timed out. Send request for new NRP on the other network. + lf_print("Backup node %d gets no response from ping. Requesting new NRP."); + message_t message; + message.type = request_new_NRP; + message.source = self->id; + message.destination = self->primary; + if (self->NRP_network == 1) { + // Use network 2. + lf_set(out2, message); + } else { + lf_set(out1, message); + } + self->ping_pending = false; + } + =} } mode Primary { @@ -164,6 +187,20 @@ reactor Node( lf_set(out1, message); lf_set(out2, message); =} + + reaction(in1) {= + if (in1->value.type == request_new_NRP) { + // FIXME: Find a new candidate NRP on network 1. + // FIXME: Confirm new NRP with backup. + } + =} + + reaction(in2) {= + if (in2->value.type == request_new_NRP) { + // FIXME: Find a new candidate NRP on network 1. + // FIXME: Confirm new NRP with backup. + } + =} } mode Failed { @@ -175,10 +212,10 @@ reactor Node( /** * Switch with two interfaces. When a pingNRP message arrives on either interface, if the - * destination matches the ID of this switch, then the switch responds on the same interface with a - * pingNRP_response message. When any other message arrives on either interface, the switch forwards - * a copy of the message to the other interface. If any two messages would be simultaneous on an - * output, one will be sent one microstep later. + * destination matches the ID of this switch or the destination is 0, then the switch responds on + * the same interface with a pingNRP_response message. When any other message arrives on either + * interface, the switch forwards a copy of the message to the other interface. If any two messages + * would be simultaneous on an output, one will be sent one microstep later. */ reactor Switch( id: int = 0, @@ -208,7 +245,7 @@ reactor Switch( reaction(in1, in2) -> out1, out2, pending_out1, pending_out2 {= if (in1->is_present) { if (in1->value.type == pingNRP) { - if (in1->value.destination == self->id) { + if (in1->value.destination == self->id || in1->value.destination == 0) { lf_print("==== Switch %d pinged by node %d. Responding.", self->id, in1->value.source); // Respond to the ping. message_t message; @@ -281,7 +318,7 @@ main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 10 s) node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) - switch1 = new Switch(id=1) + switch1 = new Switch(id=1, fails_at_time = 3 s) switch2 = new Switch(id=2) node1.out1 -> switch1.in1 after delay @@ -293,7 +330,7 @@ main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { switch2.out1 -> node2.in1 after delay node2.out1 -> switch2.in1 after delay - switch3 = new Switch(id=3, fails_at_time = 3 s) + switch3 = new Switch(id=3) switch4 = new Switch(id=4) node1.out2 -> switch3.in1 after delay From 30bae7d600fcb89b47609221a6e28aec5e5ae782 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Wed, 16 Aug 2023 08:43:34 -0400 Subject: [PATCH 09/27] Request new NRP handled --- examples/C/src/leader-election/NRP_FD.lf | 130 +++++++++++++++-------- 1 file changed, 85 insertions(+), 45 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index e9c63996..18ef084d 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -16,7 +16,8 @@ preamble {= heartbeat, pingNRP, pingNRP_response, - request_new_NRP + request_new_NRP, + new_NRP }; typedef struct message_t { enum message_type type; @@ -47,6 +48,7 @@ reactor Node( state NRP_network: int = 1 state NRP_switch_id: int = 1 + state NRP_pending: bool = true state primary: int = 0 // The known primary node. @@ -55,18 +57,15 @@ reactor Node( initial mode Waiting { reaction(startup) -> out1, out2 {= - message_t ping_message; - ping_message.type = pingNRP; - ping_message.source = self->id; // NOTE: The paper does not specify how to select the initial NRP. // Here, we send with destination 0, which the switches interpret as first to respond. // First to respond will be id 1 at startup; then wait for a reply before // actually becoming the primary or backup. - ping_message.destination = 0; + message_t ping_message ={pingNRP, self->id, 0}; lf_set(out1, ping_message); =} - reaction(in1, in2) -> reset(Backup), reset(Primary) {= + reaction(in1, in2) -> out1, out2, reset(Backup), reset(Primary) {= // Got a response to the ping from one or both switches. // NOTE: The paper calls for user intervention to select which is primary. // Here, we just choose id 1 to be primary. @@ -78,10 +77,18 @@ reactor Node( lf_print("Primary node %d received ping response on network 1. Making switch %d the NRP.", self->id, in1->value.source); self->NRP_network = 1; self->NRP_switch_id = in1->value.source; + self->NRP_pending = false; + // Notify the backup of the NRP. + message_t message = {new_NRP, in1->value.source, 0}; + lf_set(out1, message); } else if (in2->is_present && in2->value.type == pingNRP) { lf_print("Primary node %d received ping response on network 2. Making switch %d the NRP.", self->id, in2->value.source); self->NRP_network = 2; self->NRP_switch_id = in2->value.source; + self->NRP_pending = false; + // Notify the backup of the NRP. + message_t message = {new_NRP, in1->value.source, 0}; + lf_set(out2, message); } } else { lf_set_mode(Backup); @@ -106,6 +113,11 @@ reactor Node( // Got a response from the NRP to a ping we sent after a partial timeout. lf_print("Backup node %d received ping response on network 1 from NRP on switch %d.", self->id, in1->value.source); // Don't do anything. Remain the backup. + } else if (in1->value.type == new_NRP) { + // FIXME: Ping the new NRP and send confirmation back to primary. + self->NRP_network = 1; + self->NRP_switch_id = in1->value.source; + self->NRP_pending = false; } =} @@ -118,6 +130,11 @@ reactor Node( lf_print("Backup node %d received ping response on network 2 from NRP on switch %d.", self->id, in2->value.source); self->ping_pending = false; // Don't do anything. Remain the backup. + } else if (in2->value.type == new_NRP) { + // FIXME: Ping the new NRP and send confirmation back to primary. + self->NRP_network = 2; + self->NRP_switch_id = in2->value.source; + self->NRP_pending = false; } =} @@ -133,18 +150,17 @@ reactor Node( // Possible network failure. lf_print("**** Backup node %d detects missing heartbeats on one network.", self->id); // Ping the NRP. - message_t message; - message.source = self->id; - message.type = pingNRP; - message.destination = self->NRP_switch_id; - if (self->NRP_network == 1) { - lf_set(out1, message); - } else { - lf_set(out2, message); + message_t message = {pingNRP, self->id, self->NRP_switch_id}; + if (!self->ping_pending && !self->NRP_pending) { + if (self->NRP_network == 1) { + lf_set(out1, message); + } else { + lf_set(out2, message); + } + lf_print("Backup node %d pings NRP on network %d, switch %d", self->id, self->NRP_network, self->NRP_switch_id); + self->ping_pending = true; + lf_schedule(ping_timed_out, 0); } - lf_print("Backup node %d pings NRP on network %d, switch %d", self->id, self->NRP_network, self->NRP_switch_id); - self->ping_pending = true; - lf_schedule(ping_timed_out, 0); } // Increment the counters so if they are not reset to 0 by the next time, // we detect the missed heartbeat. @@ -154,17 +170,19 @@ reactor Node( reaction(ping_timed_out) -> out1, out2 {= if (self->ping_pending) { - // Ping timed out. Send request for new NRP on the other network. - lf_print("Backup node %d gets no response from ping. Requesting new NRP."); - message_t message; - message.type = request_new_NRP; - message.source = self->id; - message.destination = self->primary; - if (self->NRP_network == 1) { - // Use network 2. - lf_set(out2, message); - } else { - lf_set(out1, message); + // Ping timed out. + lf_print("Backup node %d gets no response from ping.", self->id); + if (!self->NRP_pending) { + // Send request for new NRP on the other network. + lf_print("Backup node %d requests new NRP.", self->id); + message_t message = {request_new_NRP, self->id, self->primary}; + if (self->NRP_network == 1) { + // Use network 2. + lf_set(out2, message); + } else { + lf_set(out1, message); + } + self->NRP_pending = true; } self->ping_pending = false; } @@ -181,24 +199,52 @@ reactor Node( reaction(heartbeat) -> out1, out2 {= lf_print("Primary node %d sends heartbeat on both networks.", self->id); - message_t message; - message.type = heartbeat; - message.source = self->id; + message_t message = {heartbeat, self->id, 0}; lf_set(out1, message); lf_set(out2, message); =} - reaction(in1) {= + reaction(in1) -> out1 {= if (in1->value.type == request_new_NRP) { - // FIXME: Find a new candidate NRP on network 1. - // FIXME: Confirm new NRP with backup. + // Find a new candidate NRP on network 1. + lf_print("Primary node %d looking for new NRP on network 1.", self->id); + message_t message = {pingNRP, self->id, 0}; + lf_set(out1, message); + self->NRP_pending = true; + } else if (in1->value.type == pingNRP_response) { + lf_print("Primary node %d received ping response on network 1. NRP is %d.", self->id, in1->value.source); + self->NRP_network = 1; + self->NRP_switch_id = in1->value.source; + if (self->NRP_pending) { + self->NRP_pending = false; + // Notify backup of new NRP. source field encodes the switch id. + lf_print("Primary node %d notifies backup of new NRP %d.", self->id, in1->value.source); + message_t message = {new_NRP, in1->value.source, 0}; + lf_set(out1, message); + // FIXME: Wait for confirmation of new NRP with backup. + } } =} - reaction(in2) {= + reaction(in2) -> out2 {= if (in2->value.type == request_new_NRP) { - // FIXME: Find a new candidate NRP on network 1. - // FIXME: Confirm new NRP with backup. + // Find a new candidate NRP on network 2. + lf_print("Primary node %d looking for new NRP on network 2.", self->id); + message_t message = {pingNRP, self->id, 0}; + lf_set(out2, message); + self->NRP_pending = true; + } else if (in2->value.type == pingNRP_response) { + lf_print("Primary node %d received ping response on network 2. NRP is %d.", self->id, in2->value.source); + self->NRP_network = 2; + self->NRP_switch_id = in2->value.source; + if (self->NRP_pending) { + self->NRP_pending = false; + // Notify backup of new NRP. source field encodes the switch id. + lf_print("Primary node %d notifies backup of new NRP %d.", self->id, in2->value.source); + message_t message = {new_NRP, in2->value.source, 0}; + lf_set(out2, message); + // FIXME: Wait for confirmation of new NRP with backup. + } } =} } @@ -248,10 +294,7 @@ reactor Switch( if (in1->value.destination == self->id || in1->value.destination == 0) { lf_print("==== Switch %d pinged by node %d. Responding.", self->id, in1->value.source); // Respond to the ping. - message_t message; - message.source = self->id; - message.destination = in1->value.source; - message.type = pingNRP_response; + message_t message = {pingNRP_response, self->id, in1->value.source}; if (!out1->is_present) { lf_set(out1, message); } else { @@ -279,10 +322,7 @@ reactor Switch( if (in2->value.destination == self->id) { lf_print("==== Switch %d pinged by node %d. Responding.", self->id, in2->value.source); // Construct a response to the ping. - message_t message; - message.source = self->id; - message.destination = in2->value.source; - message.type = pingNRP_response; + message_t message = {pingNRP_response, self->id, in2->value.source}; // Respond to the ping if out2 is available. if (!out2->is_present) { lf_set(out2, message); From 022ff27161c241d1c9a715bd1a832d340fee7f65 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Wed, 16 Aug 2023 15:45:17 -0400 Subject: [PATCH 10/27] Added detection of network partitioning and test showing that we don't get two primaries --- examples/C/src/leader-election/NRP_FD.lf | 76 +++++++++++++++++-- .../leader-election/NRP_FD_Partitioning.lf | 56 ++++++++++++++ 2 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 examples/C/src/leader-election/NRP_FD_Partitioning.lf diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index 18ef084d..c3a467cb 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -6,12 +6,22 @@ * Availability: Network Reference Point based Failure Detection for Controller Redundancy," paper * draft 8/15/23. * + * The key idea in this protocol is that when a backup fails to detect the heartbeats of a primary + * node, it becomes primary only if it has access to Network Reference Point (NRP), which is a + * point in the network. This way, if the network becomes partitioned, only a backup that is + * on the side of the partition that still has access to the NRP can become a primary. + * If a primary loses access to the NRP, then it relinquishes its primary role because it + * is now on the wrong side of a network partition. A backup on the right side of the partition + * will take over. + * * @author Edward A. Lee * @author Marjan Sirjani */ target C preamble {= + #ifndef NRF_FD + #define NRF_FD enum message_type { heartbeat, pingNRP, @@ -24,6 +34,7 @@ preamble {= int source; int destination; } message_t; + #endif // NRF_FD =} reactor Node( @@ -32,7 +43,9 @@ reactor Node( max_missed_heartbeats: int = 2, fails_at_time: time = 0, // For testing. 0 for no failure. // Time until ping is deemed to have failed. - ping_timeout: time = 500 ms) { + ping_timeout: time = 500 ms, + // Time until new NRP request is deemed to have failed. + nrp_timeout: time = 500 ms) { // There are two network interfaces: @side("east") input in1: message_t @@ -49,6 +62,8 @@ reactor Node( state NRP_network: int = 1 state NRP_switch_id: int = 1 state NRP_pending: bool = true + state become_primary_on_ping_response: bool = false + logical action new_NRP_request_timed_out(nrp_timeout) state primary: int = 0 // The known primary node. @@ -105,19 +120,28 @@ reactor Node( if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} - reaction(in1) {= + reaction(in1) -> reset(Primary) {= if (in1->value.type == heartbeat) { lf_print("Backup node %d received heartbeat from node %d on network 1.", self->id, in1->value.source); self->heartbeats_missed_1 = 0; } else if (in1->value.type == pingNRP_response && in1->value.destination == self->id) { - // Got a response from the NRP to a ping we sent after a partial timeout. + // Got a response from the NRP to a ping we sent after a partial or complete timeout. lf_print("Backup node %d received ping response on network 1 from NRP on switch %d.", self->id, in1->value.source); - // Don't do anything. Remain the backup. + // If there was a timeout on both networks that was not simultaneous, then + // we tried pinging the NRP before becoming primary. + if (self->become_primary_on_ping_response) { + lf_set_mode(Primary); + self->become_primary_on_ping_response = false; + } } else if (in1->value.type == new_NRP) { // FIXME: Ping the new NRP and send confirmation back to primary. self->NRP_network = 1; self->NRP_switch_id = in1->value.source; self->NRP_pending = false; + if (self->become_primary_on_ping_response) { + lf_set_mode(Primary); + self->become_primary_on_ping_response = false; + } } =} @@ -141,13 +165,37 @@ reactor Node( reaction(t) -> reset(Primary), out1, out2, ping_timed_out {= if (self->heartbeats_missed_1 > self->max_missed_heartbeats && self->heartbeats_missed_2 > self->max_missed_heartbeats) { - // Simultaneous heartbeat misses. Assume the primary failed. + // Simultaneous heartbeat misses. + // In the paper, this is tmoAllNotSimul. + // For the tmoAllSimul optimization in the paper, we assume that if + // self->heartbeats_missed_1 == self->heartbeats_missed_2, then most likely, it is + // the primary that failed, and not the network, so can immediately become the primary. + // Otherwise, it is possible that one network failed, and then the other failed, in which + // case, we may have a partitioned network. lf_print("**** Backup node %d detects missing heartbeats on both networks.", self->id); - lf_set_mode(Primary); + if (self->heartbeats_missed_1 == self->heartbeats_missed_2) { + lf_print("**** Missing heartbeats on both networks were simultaneous. Assume the primary failed."); + lf_set_mode(Primary); + } else { + // Ping the NRP because if we can't access it, we are on the wrong side of + // a network partition and could end up with two primaries. + message_t message = {pingNRP, self->id, self->NRP_switch_id}; + if (self->NRP_network == 1) { + lf_set(out1, message); + } else { + lf_set(out2, message); + } + // Wait for a response before becoming primary. + self->become_primary_on_ping_response = true; + lf_schedule(ping_timed_out, 0); + } + self->heartbeats_missed_1 = 0; // Prevent detecting again immediately. + self->heartbeats_missed_2 = 0; } else if (self->heartbeats_missed_1 > self->max_missed_heartbeats || self->heartbeats_missed_2 > self->max_missed_heartbeats) { // Heartbeat missed on one network but not yet on the other. - // Possible network failure. + // Ping the NRP to make sure we retain access to it so that we can be an effective backup. + // This corresponds to tmoSomeNotAll in the paper. lf_print("**** Backup node %d detects missing heartbeats on one network.", self->id); // Ping the NRP. message_t message = {pingNRP, self->id, self->NRP_switch_id}; @@ -168,7 +216,7 @@ reactor Node( self->heartbeats_missed_2++; =} - reaction(ping_timed_out) -> out1, out2 {= + reaction(ping_timed_out) -> out1, out2, new_NRP_request_timed_out {= if (self->ping_pending) { // Ping timed out. lf_print("Backup node %d gets no response from ping.", self->id); @@ -183,10 +231,22 @@ reactor Node( lf_set(out1, message); } self->NRP_pending = true; + lf_schedule(new_NRP_request_timed_out, 0); } self->ping_pending = false; } =} + + reaction(new_NRP_request_timed_out) {= + if (self->NRP_pending) { + self->NRP_pending = false; + lf_print("Backup node %d new NRP request timed out. Will not function as backup.", self->id); + if (self->become_primary_on_ping_response) { + lf_print("Network is likely partitioned. Remaining as (non-functional) backup."); + self->become_primary_on_ping_response = false; + } + } + =} } mode Primary { diff --git a/examples/C/src/leader-election/NRP_FD_Partitioning.lf b/examples/C/src/leader-election/NRP_FD_Partitioning.lf new file mode 100644 index 00000000..d5480f0c --- /dev/null +++ b/examples/C/src/leader-election/NRP_FD_Partitioning.lf @@ -0,0 +1,56 @@ +// This version partitions the network and shows that the protocol +// prevents the backup from becoming primary, thereby preventing +// two primaries. +target C + +import Switch, Node from "NRP_FD.lf" + +// FIXME: Due to this bug: https://github.com/lf-lang/reactor-c/issues/261 +// we have to repeat the preamble from the imported file here. +preamble {= + #ifndef NRF_FD + #define NRF_FD + enum message_type { + heartbeat, + pingNRP, + pingNRP_response, + request_new_NRP, + new_NRP + }; + typedef struct message_t { + enum message_type type; + int source; + int destination; + } message_t; + #endif // NRF_FD +=} + +main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { + node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 15 s) + node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) + + switch1 = new Switch(id=1, fails_at_time = 3 s) + switch2 = new Switch(id=2) + + node1.out1 -> switch1.in1 after delay + switch1.out1 -> node1.in1 after delay + + switch1.out2 -> switch2.in2 after delay + switch2.out2 -> switch1.in2 after delay + + switch2.out1 -> node2.in1 after delay + node2.out1 -> switch2.in1 after delay + + switch3 = new Switch(id=3) + // Failure of switch4 will partition the network. + switch4 = new Switch(id=4, fails_at_time = 10 s) + + node1.out2 -> switch3.in1 after delay + switch3.out1 -> node1.in2 after delay + + switch3.out2 -> switch4.in2 after delay + switch4.out2 -> switch3.in2 after delay + + switch4.out1 -> node2.in2 after delay + node2.out2 -> switch4.in1 after delay +} From 28898c9d717d3362debefe03e26db2ed3033afed Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Sat, 19 Aug 2023 07:48:43 -0400 Subject: [PATCH 11/27] Removed FIXME --- .../leader-election/NRP_FD_Partitioning.lf | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD_Partitioning.lf b/examples/C/src/leader-election/NRP_FD_Partitioning.lf index d5480f0c..cbe55ca1 100644 --- a/examples/C/src/leader-election/NRP_FD_Partitioning.lf +++ b/examples/C/src/leader-election/NRP_FD_Partitioning.lf @@ -5,26 +5,6 @@ target C import Switch, Node from "NRP_FD.lf" -// FIXME: Due to this bug: https://github.com/lf-lang/reactor-c/issues/261 -// we have to repeat the preamble from the imported file here. -preamble {= - #ifndef NRF_FD - #define NRF_FD - enum message_type { - heartbeat, - pingNRP, - pingNRP_response, - request_new_NRP, - new_NRP - }; - typedef struct message_t { - enum message_type type; - int source; - int destination; - } message_t; - #endif // NRF_FD -=} - main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 15 s) node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) From cb28a057985cdcfe8e9006eb45075e3564786828 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Sat, 19 Aug 2023 09:19:09 -0400 Subject: [PATCH 12/27] Updated FIXMEs and formatted --- .../C/src/leader-election/HeartbeatBully.lf | 16 +++++++------- examples/C/src/leader-election/NRP_FD.lf | 22 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/examples/C/src/leader-election/HeartbeatBully.lf b/examples/C/src/leader-election/HeartbeatBully.lf index 1bc25405..e3b1bbd3 100644 --- a/examples/C/src/leader-election/HeartbeatBully.lf +++ b/examples/C/src/leader-election/HeartbeatBully.lf @@ -15,13 +15,12 @@ * failed, then the program exits. * * This example is designed to be run as a federated program with decentralized coordination. - * However, as of this writing, bugs in the federated code generator cause the program to fail to - * compile when you change it to be federated. See: + * However, as of this writing, bugs in the federated code generator cause the program to fail + * because all federates get the same bank_index == 0. This may be related to this bug: * - * - https://github.com/lf-lang/lingua-franca/issues/1942 - * - https://github.com/lf-lang/lingua-franca/issues/1940 + * - https://github.com/lf-lang/lingua-franca/issues/1961 * - * When these bugs are fixed, then the federated version should operate exactly the same as the + * When this bugs are fixed, then the federated version should operate exactly the same as the * unfederated version except that it will become possible to kill the federates instead of having * them fail on their own. The program should also be extended to include STP violation handlers to * deal with the fundamental CAL theorem limitations, where unexpected network delays make it @@ -59,6 +58,7 @@ reactor Node( initial mode Idle { reaction(startup) -> reset(Backup), reset(Primary) {= + lf_print("Starting node %d", self->bank_index); if (self->bank_index == self->num_nodes - 1) { lf_set_mode(Primary); } else { @@ -192,9 +192,9 @@ reactor Node( } } -// FIXME: This should be federated, but it fails: -// See https://github.com/lf-lang/lingua-franca/issues/1942 -// and https://github.com/lf-lang/lingua-franca/issues/1940. +// FIXME: This should be federated, but it fails because all federates get the bank_index = 0. +// This may be related to this bug: +// https://github.com/lf-lang/lingua-franca/issues/1961 main reactor(num_nodes: int = 4, heartbeat_period: time = 1 s) { nodes = new[num_nodes] Node(num_nodes=num_nodes, heartbeat_period=heartbeat_period) nodes.out -> interleaved(nodes.in) after heartbeat_period diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index c3a467cb..b855f684 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -7,13 +7,12 @@ * draft 8/15/23. * * The key idea in this protocol is that when a backup fails to detect the heartbeats of a primary - * node, it becomes primary only if it has access to Network Reference Point (NRP), which is a - * point in the network. This way, if the network becomes partitioned, only a backup that is - * on the side of the partition that still has access to the NRP can become a primary. - * If a primary loses access to the NRP, then it relinquishes its primary role because it - * is now on the wrong side of a network partition. A backup on the right side of the partition - * will take over. - * + * node, it becomes primary only if it has access to Network Reference Point (NRP), which is a point + * in the network. This way, if the network becomes partitioned, only a backup that is on the side + * of the partition that still has access to the NRP can become a primary. If a primary loses access + * to the NRP, then it relinquishes its primary role because it is now on the wrong side of a + * network partition. A backup on the right side of the partition will take over. + * * @author Edward A. Lee * @author Marjan Sirjani */ @@ -41,9 +40,8 @@ reactor Node( id: int = 0, heartbeat_period: time = 1 s, max_missed_heartbeats: int = 2, - fails_at_time: time = 0, // For testing. 0 for no failure. - // Time until ping is deemed to have failed. - ping_timeout: time = 500 ms, + fails_at_time: time = 0, // For testing. 0 for no failure. + ping_timeout: time = 500 ms, // Time until ping is deemed to have failed. // Time until new NRP request is deemed to have failed. nrp_timeout: time = 500 ms) { // There are two network interfaces: @@ -236,7 +234,7 @@ reactor Node( self->ping_pending = false; } =} - + reaction(new_NRP_request_timed_out) {= if (self->NRP_pending) { self->NRP_pending = false; @@ -304,7 +302,7 @@ reactor Node( message_t message = {new_NRP, in2->value.source, 0}; lf_set(out2, message); // FIXME: Wait for confirmation of new NRP with backup. - } + } } =} } From 573107d91bc89dfb2b4320ccb1ec18d4df158154 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Sat, 19 Aug 2023 09:29:28 -0400 Subject: [PATCH 13/27] Updated FIXMEs --- examples/C/src/leader-election/HeartbeatBully.lf | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/C/src/leader-election/HeartbeatBully.lf b/examples/C/src/leader-election/HeartbeatBully.lf index e3b1bbd3..09d2257d 100644 --- a/examples/C/src/leader-election/HeartbeatBully.lf +++ b/examples/C/src/leader-election/HeartbeatBully.lf @@ -16,11 +16,12 @@ * * This example is designed to be run as a federated program with decentralized coordination. * However, as of this writing, bugs in the federated code generator cause the program to fail - * because all federates get the same bank_index == 0. This may be related to this bug: + * because all federates get the same bank_index == 0. This may be related to these bugs: * * - https://github.com/lf-lang/lingua-franca/issues/1961 + * - https://github.com/lf-lang/lingua-franca/issues/1962 * - * When this bugs are fixed, then the federated version should operate exactly the same as the + * When these bugs are fixed, then the federated version should operate exactly the same as the * unfederated version except that it will become possible to kill the federates instead of having * them fail on their own. The program should also be extended to include STP violation handlers to * deal with the fundamental CAL theorem limitations, where unexpected network delays make it @@ -193,8 +194,9 @@ reactor Node( } // FIXME: This should be federated, but it fails because all federates get the bank_index = 0. -// This may be related to this bug: +// This may be related to these bugs: // https://github.com/lf-lang/lingua-franca/issues/1961 +// https://github.com/lf-lang/lingua-franca/issues/1962 main reactor(num_nodes: int = 4, heartbeat_period: time = 1 s) { nodes = new[num_nodes] Node(num_nodes=num_nodes, heartbeat_period=heartbeat_period) nodes.out -> interleaved(nodes.in) after heartbeat_period From 2ad8cd0dfc067c522a99694c1efe65a8eeba737e Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Mon, 28 Aug 2023 11:06:29 -0400 Subject: [PATCH 14/27] Refined leader election and fixed bug --- examples/C/src/leader-election/NRP_FD.lf | 69 ++++++++++--------- .../leader-election/NRP_FD_PrimaryFails.lf | 34 +++++++++ 2 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 examples/C/src/leader-election/NRP_FD_PrimaryFails.lf diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index b855f684..b871842c 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -21,6 +21,7 @@ target C preamble {= #ifndef NRF_FD #define NRF_FD + #include "platform.h" // Defines PRINTF_TIME enum message_type { heartbeat, pingNRP, @@ -87,7 +88,7 @@ reactor Node( // Become primary. lf_set_mode(Primary); if (in1->is_present && in1->value.type == pingNRP_response) { - lf_print("Primary node %d received ping response on network 1. Making switch %d the NRP.", self->id, in1->value.source); + lf_print(PRINTF_TIME ": Primary node %d received ping response on network 1. Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, in1->value.source); self->NRP_network = 1; self->NRP_switch_id = in1->value.source; self->NRP_pending = false; @@ -95,7 +96,7 @@ reactor Node( message_t message = {new_NRP, in1->value.source, 0}; lf_set(out1, message); } else if (in2->is_present && in2->value.type == pingNRP) { - lf_print("Primary node %d received ping response on network 2. Making switch %d the NRP.", self->id, in2->value.source); + lf_print(PRINTF_TIME ": Primary node %d received ping response on network 2. Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, in2->value.source); self->NRP_network = 2; self->NRP_switch_id = in2->value.source; self->NRP_pending = false; @@ -112,7 +113,7 @@ reactor Node( mode Backup { timer t(heartbeat_period, heartbeat_period) // FIXME: Need SENDIMHERETOPRIMARY with "longer interval" - reaction(reset) {= lf_print("---- Node %d becomes backup.", self->id); =} + reaction(reset) {= lf_print(PRINTF_TIME ": ---- Node %d becomes backup.", lf_time_logical_elapsed(), self->id); =} reaction(node_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); @@ -120,38 +121,40 @@ reactor Node( reaction(in1) -> reset(Primary) {= if (in1->value.type == heartbeat) { - lf_print("Backup node %d received heartbeat from node %d on network 1.", self->id, in1->value.source); + lf_print(PRINTF_TIME ": Backup node %d received heartbeat from node %d on network 1.", lf_time_logical_elapsed(), self->id, in1->value.source); self->heartbeats_missed_1 = 0; } else if (in1->value.type == pingNRP_response && in1->value.destination == self->id) { // Got a response from the NRP to a ping we sent after a partial or complete timeout. - lf_print("Backup node %d received ping response on network 1 from NRP on switch %d.", self->id, in1->value.source); + lf_print(PRINTF_TIME ": Backup node %d received ping response on network 1 from NRP on switch %d.", lf_time_logical_elapsed(), self->id, in1->value.source); // If there was a timeout on both networks that was not simultaneous, then // we tried pinging the NRP before becoming primary. if (self->become_primary_on_ping_response) { lf_set_mode(Primary); self->become_primary_on_ping_response = false; } + self->ping_pending = false; } else if (in1->value.type == new_NRP) { // FIXME: Ping the new NRP and send confirmation back to primary. self->NRP_network = 1; self->NRP_switch_id = in1->value.source; self->NRP_pending = false; - if (self->become_primary_on_ping_response) { - lf_set_mode(Primary); - self->become_primary_on_ping_response = false; - } } =} - reaction(in2) {= + reaction(in2) -> reset(Primary) {= if (in2->value.type == heartbeat) { - lf_print("Backup node %d received heartbeat from node %d on network 2.", self->id, in2->value.source); + lf_print(PRINTF_TIME ": Backup node %d received heartbeat from node %d on network 2.", lf_time_logical_elapsed(), self->id, in2->value.source); self->heartbeats_missed_2 = 0; } else if (in2->value.type == pingNRP_response && in2->value.destination == self->id) { // Got a response from the NRP to a ping we sent after a partial timeout. - lf_print("Backup node %d received ping response on network 2 from NRP on switch %d.", self->id, in2->value.source); + lf_print(PRINTF_TIME ": Backup node %d received ping response on network 2 from NRP on switch %d.", lf_time_logical_elapsed(), self->id, in2->value.source); self->ping_pending = false; - // Don't do anything. Remain the backup. + // If there was a timeout on both networks that was not simultaneous, then + // we tried pinging the NRP before becoming primary. + if (self->become_primary_on_ping_response) { + lf_set_mode(Primary); + self->become_primary_on_ping_response = false; + } } else if (in2->value.type == new_NRP) { // FIXME: Ping the new NRP and send confirmation back to primary. self->NRP_network = 2; @@ -170,9 +173,9 @@ reactor Node( // the primary that failed, and not the network, so can immediately become the primary. // Otherwise, it is possible that one network failed, and then the other failed, in which // case, we may have a partitioned network. - lf_print("**** Backup node %d detects missing heartbeats on both networks.", self->id); + lf_print(PRINTF_TIME ": **** Backup node %d detects missing heartbeats on both networks.", lf_time_logical_elapsed(), self->id); if (self->heartbeats_missed_1 == self->heartbeats_missed_2) { - lf_print("**** Missing heartbeats on both networks were simultaneous. Assume the primary failed."); + lf_print(PRINTF_TIME ": **** Missing heartbeats on both networks were simultaneous. Assume the primary failed.", lf_time_logical_elapsed()); lf_set_mode(Primary); } else { // Ping the NRP because if we can't access it, we are on the wrong side of @@ -194,7 +197,7 @@ reactor Node( // Heartbeat missed on one network but not yet on the other. // Ping the NRP to make sure we retain access to it so that we can be an effective backup. // This corresponds to tmoSomeNotAll in the paper. - lf_print("**** Backup node %d detects missing heartbeats on one network.", self->id); + lf_print(PRINTF_TIME ": **** Backup node %d detects missing heartbeats on one network.", lf_time_logical_elapsed(), self->id); // Ping the NRP. message_t message = {pingNRP, self->id, self->NRP_switch_id}; if (!self->ping_pending && !self->NRP_pending) { @@ -203,7 +206,7 @@ reactor Node( } else { lf_set(out2, message); } - lf_print("Backup node %d pings NRP on network %d, switch %d", self->id, self->NRP_network, self->NRP_switch_id); + lf_print(PRINTF_TIME ": Backup node %d pings NRP on network %d, switch %d", lf_time_logical_elapsed(), self->id, self->NRP_network, self->NRP_switch_id); self->ping_pending = true; lf_schedule(ping_timed_out, 0); } @@ -217,10 +220,10 @@ reactor Node( reaction(ping_timed_out) -> out1, out2, new_NRP_request_timed_out {= if (self->ping_pending) { // Ping timed out. - lf_print("Backup node %d gets no response from ping.", self->id); + lf_print(PRINTF_TIME ": Backup node %d gets no response from ping.", lf_time_logical_elapsed(), self->id); if (!self->NRP_pending) { // Send request for new NRP on the other network. - lf_print("Backup node %d requests new NRP.", self->id); + lf_print(PRINTF_TIME ": Backup node %d requests new NRP.", lf_time_logical_elapsed(), self->id); message_t message = {request_new_NRP, self->id, self->primary}; if (self->NRP_network == 1) { // Use network 2. @@ -238,9 +241,9 @@ reactor Node( reaction(new_NRP_request_timed_out) {= if (self->NRP_pending) { self->NRP_pending = false; - lf_print("Backup node %d new NRP request timed out. Will not function as backup.", self->id); + lf_print(PRINTF_TIME ": Backup node %d new NRP request timed out. Will not function as backup.", lf_time_logical_elapsed(), self->id); if (self->become_primary_on_ping_response) { - lf_print("Network is likely partitioned. Remaining as (non-functional) backup."); + lf_print(PRINTF_TIME ": Network is likely partitioned. Remaining as (non-functional) backup.", lf_time_logical_elapsed()); self->become_primary_on_ping_response = false; } } @@ -249,14 +252,14 @@ reactor Node( mode Primary { timer heartbeat(0, heartbeat_period) - reaction(reset) {= lf_print("---- Node %d becomes primary.", self->id); =} + reaction(reset) {= lf_print(PRINTF_TIME ": ---- Node %d becomes primary.", lf_time_logical_elapsed(), self->id); =} reaction(node_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} reaction(heartbeat) -> out1, out2 {= - lf_print("Primary node %d sends heartbeat on both networks.", self->id); + lf_print(PRINTF_TIME ": Primary node %d sends heartbeat on both networks.", lf_time_logical_elapsed(), self->id); message_t message = {heartbeat, self->id, 0}; lf_set(out1, message); lf_set(out2, message); @@ -265,18 +268,18 @@ reactor Node( reaction(in1) -> out1 {= if (in1->value.type == request_new_NRP) { // Find a new candidate NRP on network 1. - lf_print("Primary node %d looking for new NRP on network 1.", self->id); + lf_print(PRINTF_TIME ": Primary node %d looking for new NRP on network 1.", lf_time_logical_elapsed(), self->id); message_t message = {pingNRP, self->id, 0}; lf_set(out1, message); self->NRP_pending = true; } else if (in1->value.type == pingNRP_response) { - lf_print("Primary node %d received ping response on network 1. NRP is %d.", self->id, in1->value.source); + lf_print(PRINTF_TIME ": Primary node %d received ping response on network 1. NRP is %d.", lf_time_logical_elapsed(), self->id, in1->value.source); self->NRP_network = 1; self->NRP_switch_id = in1->value.source; if (self->NRP_pending) { self->NRP_pending = false; // Notify backup of new NRP. source field encodes the switch id. - lf_print("Primary node %d notifies backup of new NRP %d.", self->id, in1->value.source); + lf_print(PRINTF_TIME ": Primary node %d notifies backup of new NRP %d.", lf_time_logical_elapsed(), self->id, in1->value.source); message_t message = {new_NRP, in1->value.source, 0}; lf_set(out1, message); // FIXME: Wait for confirmation of new NRP with backup. @@ -287,18 +290,18 @@ reactor Node( reaction(in2) -> out2 {= if (in2->value.type == request_new_NRP) { // Find a new candidate NRP on network 2. - lf_print("Primary node %d looking for new NRP on network 2.", self->id); + lf_print(PRINTF_TIME ": Primary node %d looking for new NRP on network 2.", lf_time_logical_elapsed(), self->id); message_t message = {pingNRP, self->id, 0}; lf_set(out2, message); self->NRP_pending = true; } else if (in2->value.type == pingNRP_response) { - lf_print("Primary node %d received ping response on network 2. NRP is %d.", self->id, in2->value.source); + lf_print(PRINTF_TIME ": Primary node %d received ping response on network 2. NRP is %d.", lf_time_logical_elapsed(), self->id, in2->value.source); self->NRP_network = 2; self->NRP_switch_id = in2->value.source; if (self->NRP_pending) { self->NRP_pending = false; // Notify backup of new NRP. source field encodes the switch id. - lf_print("Primary node %d notifies backup of new NRP %d.", self->id, in2->value.source); + lf_print(PRINTF_TIME ": Primary node %d notifies backup of new NRP %d.", lf_time_logical_elapsed(), self->id, in2->value.source); message_t message = {new_NRP, in2->value.source, 0}; lf_set(out2, message); // FIXME: Wait for confirmation of new NRP with backup. @@ -309,7 +312,7 @@ reactor Node( mode Failed { reaction(reset) {= - lf_print("#### Node %d fails.", self->id); + lf_print(PRINTF_TIME ": #### Node %d fails.", lf_time_logical_elapsed(), self->id); =} } } @@ -350,7 +353,7 @@ reactor Switch( if (in1->is_present) { if (in1->value.type == pingNRP) { if (in1->value.destination == self->id || in1->value.destination == 0) { - lf_print("==== Switch %d pinged by node %d. Responding.", self->id, in1->value.source); + lf_print(PRINTF_TIME ": ==== Switch %d pinged by node %d. Responding.", lf_time_logical_elapsed(), self->id, in1->value.source); // Respond to the ping. message_t message = {pingNRP_response, self->id, in1->value.source}; if (!out1->is_present) { @@ -378,7 +381,7 @@ reactor Switch( if (in2->is_present) { if (in2->value.type == pingNRP) { if (in2->value.destination == self->id) { - lf_print("==== Switch %d pinged by node %d. Responding.", self->id, in2->value.source); + lf_print(PRINTF_TIME ": ==== Switch %d pinged by node %d. Responding.", lf_time_logical_elapsed(), self->id, in2->value.source); // Construct a response to the ping. message_t message = {pingNRP_response, self->id, in2->value.source}; // Respond to the ping if out2 is available. @@ -408,7 +411,7 @@ reactor Switch( } mode Failed { - reaction(reset) {= lf_print("==== Switch %d fails.", self->id); =} + reaction(reset) {= lf_print(PRINTF_TIME ": ==== Switch %d fails.", lf_time_logical_elapsed(), self->id); =} } } diff --git a/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf b/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf new file mode 100644 index 00000000..57f7fc17 --- /dev/null +++ b/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf @@ -0,0 +1,34 @@ +// This version simply has the primary failing after 5 seconds. +// Switch 1 remains the NRP. +target C + +import Switch, Node from "NRP_FD.lf" + +main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { + node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 5 s) + node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) + + switch1 = new Switch(id=1) + switch2 = new Switch(id=2) + + node1.out1 -> switch1.in1 after delay + switch1.out1 -> node1.in1 after delay + + switch1.out2 -> switch2.in2 after delay + switch2.out2 -> switch1.in2 after delay + + switch2.out1 -> node2.in1 after delay + node2.out1 -> switch2.in1 after delay + + switch3 = new Switch(id=3) + switch4 = new Switch(id=4) + + node1.out2 -> switch3.in1 after delay + switch3.out1 -> node1.in2 after delay + + switch3.out2 -> switch4.in2 after delay + switch4.out2 -> switch3.in2 after delay + + switch4.out1 -> node2.in2 after delay + node2.out2 -> switch4.in1 after delay +} From ef9269d63f9f02b6a093244615d6aa9ecd4ef4e8 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Fri, 8 Sep 2023 08:31:33 -0400 Subject: [PATCH 15/27] Fix reference and update FIXME about not running federated --- examples/C/src/leader-election/NRP_FD.lf | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index b871842c..d98cfd3e 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -95,7 +95,7 @@ reactor Node( // Notify the backup of the NRP. message_t message = {new_NRP, in1->value.source, 0}; lf_set(out1, message); - } else if (in2->is_present && in2->value.type == pingNRP) { + } else if (in2->is_present && in2->value.type == pingNRP_response) { lf_print(PRINTF_TIME ": Primary node %d received ping response on network 2. Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, in2->value.source); self->NRP_network = 2; self->NRP_switch_id = in2->value.source; @@ -415,6 +415,9 @@ reactor Switch( } } +// FIXME: This should be federated, but bugs in federated execution make it fail. +// Specifically: +// FATAL ERROR: Received a message at tag (4000000, 0) that has a tag (4000000, 0) that has violated the STP offset. Centralized coordination should not have these types of messages. main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 10 s) node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) From e10b187daa85d7c143bdb1fbfe4227fae76b47e3 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Fri, 8 Sep 2023 08:43:30 -0400 Subject: [PATCH 16/27] Added times to printf and timeout --- .../C/src/leader-election/HeartbeatBully.lf | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/C/src/leader-election/HeartbeatBully.lf b/examples/C/src/leader-election/HeartbeatBully.lf index 09d2257d..5a1875e8 100644 --- a/examples/C/src/leader-election/HeartbeatBully.lf +++ b/examples/C/src/leader-election/HeartbeatBully.lf @@ -31,9 +31,12 @@ * @author Edward A. Lee * @author Marjan Sirjani */ -target C +target C { + timeout: 30 s +} preamble {= + #include "platform.h" // Defines PRINTF_TIME enum message_type { heartbeat, reveal, @@ -59,7 +62,7 @@ reactor Node( initial mode Idle { reaction(startup) -> reset(Backup), reset(Primary) {= - lf_print("Starting node %d", self->bank_index); + lf_print(PRINTF_TIME ": Starting node %d", lf_time_logical_elapsed(), self->bank_index); if (self->bank_index == self->num_nodes - 1) { lf_set_mode(Primary); } else { @@ -79,7 +82,7 @@ reactor Node( lf_print_error("Multiple primaries detected!!"); } primary_id = in[i]->value.id; - lf_print("Node %d received heartbeat from node %d.", self->bank_index, primary_id); + lf_print(PRINTF_TIME ": Node %d received heartbeat from node %d.", lf_time_logical_elapsed(), self->bank_index, primary_id); self->heartbeats_missed = 0; } else if (in[i]->value.type == reveal && in[i]->value.id < self->bank_index) { // NOTE: This will not occur if the LF semantics are followed because @@ -92,7 +95,7 @@ reactor Node( message.type = sorry; message.id = self->bank_index; lf_set(out[in[i]->value.id], message); - lf_print("Node %d sends sorry to node %d", self->bank_index, in[i]->value.id); + lf_print(PRINTF_TIME ": Node %d sends sorry to node %d", lf_time_logical_elapsed(), self->bank_index, in[i]->value.id); // Go to Prospect mode to send reveal to any higher-priority nodes. lf_set_mode(Prospect); } @@ -119,10 +122,10 @@ reactor Node( reaction(heartbeat) -> out, reset(Failed) {= if (self->primary_heartbeats_counter++ >= self->primary_fails_after_heartbeats) { // Stop sending heartbeats. - lf_print("**** Primary node %d fails.", self->bank_index); + lf_print(PRINTF_TIME ": **** Primary node %d fails.", lf_time_logical_elapsed(), self->bank_index); lf_set_mode(Failed); } else { - lf_print("Primary node %d sends heartbeat.", self->bank_index); + lf_print(PRINTF_TIME ": Primary node %d sends heartbeat.", lf_time_logical_elapsed(), self->bank_index); for (int i = 0; i < out_width; i++) { if (i != self->bank_index) { message_t message; @@ -141,7 +144,7 @@ reactor Node( mode Prospect { logical action wait_for_sorry reaction(reset) -> out, wait_for_sorry {= - lf_print("***** Node %d entered Prospect mode.", self->bank_index); + lf_print(PRINTF_TIME ": ***** Node %d entered Prospect mode.", lf_time_logical_elapsed(), self->bank_index); // Send a reveal message with my ID in a bid to become primary. // NOTE: It is not necessary to send to nodes that have a lower // priority than this node, but the connection is broadcast, so @@ -150,7 +153,7 @@ reactor Node( message.type = reveal; message.id = self->bank_index; for (int i = self->bank_index + 1; i < self->num_nodes; i++) { - lf_print("Node %d sends reveal to node %d", self->bank_index, i); + lf_print(PRINTF_TIME ": Node %d sends reveal to node %d", lf_time_logical_elapsed(), self->bank_index, i); lf_set(out[i], message); } // The reveal message is delayed by heartbeat_period, and if @@ -167,7 +170,7 @@ reactor Node( message.type = sorry; message.id = self->bank_index; lf_set(out[in[i]->value.id], message); - lf_print("Node %d sends sorry to node %d", self->bank_index, in[i]->value.id); + lf_print(PRINTF_TIME ": Node %d sends sorry to node %d", lf_time_logical_elapsed(), self->bank_index, in[i]->value.id); } } =} @@ -197,6 +200,8 @@ reactor Node( // This may be related to these bugs: // https://github.com/lf-lang/lingua-franca/issues/1961 // https://github.com/lf-lang/lingua-franca/issues/1962 +// Although these issues have been closed, this program still does not bahave the same +// federated as unfederated. main reactor(num_nodes: int = 4, heartbeat_period: time = 1 s) { nodes = new[num_nodes] Node(num_nodes=num_nodes, heartbeat_period=heartbeat_period) nodes.out -> interleaved(nodes.in) after heartbeat_period From 85737db8f05356a19c5cdcedb447c6fb0fc26139 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Fri, 8 Sep 2023 08:46:40 -0400 Subject: [PATCH 17/27] Made example federated --- examples/C/src/leader-election/HeartbeatBully.lf | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/C/src/leader-election/HeartbeatBully.lf b/examples/C/src/leader-election/HeartbeatBully.lf index 5a1875e8..31362d82 100644 --- a/examples/C/src/leader-election/HeartbeatBully.lf +++ b/examples/C/src/leader-election/HeartbeatBully.lf @@ -196,13 +196,7 @@ reactor Node( } } -// FIXME: This should be federated, but it fails because all federates get the bank_index = 0. -// This may be related to these bugs: -// https://github.com/lf-lang/lingua-franca/issues/1961 -// https://github.com/lf-lang/lingua-franca/issues/1962 -// Although these issues have been closed, this program still does not bahave the same -// federated as unfederated. -main reactor(num_nodes: int = 4, heartbeat_period: time = 1 s) { +federated reactor(num_nodes: int = 4, heartbeat_period: time = 1 s) { nodes = new[num_nodes] Node(num_nodes=num_nodes, heartbeat_period=heartbeat_period) nodes.out -> interleaved(nodes.in) after heartbeat_period } From 435aa26c72df437e39e0bf28e225d28df2eac619 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Wed, 13 Sep 2023 11:34:23 +0200 Subject: [PATCH 18/27] Use federated execution --- examples/C/src/leader-election/NRP_FD.lf | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index d98cfd3e..41c09766 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -16,7 +16,9 @@ * @author Edward A. Lee * @author Marjan Sirjani */ -target C +target C { + timeout: 20 s +} preamble {= #ifndef NRF_FD @@ -415,10 +417,7 @@ reactor Switch( } } -// FIXME: This should be federated, but bugs in federated execution make it fail. -// Specifically: -// FATAL ERROR: Received a message at tag (4000000, 0) that has a tag (4000000, 0) that has violated the STP offset. Centralized coordination should not have these types of messages. -main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { +federated reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 10 s) node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) From ae076cf77dcc19a281fadfb568f8a27b206b3226 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Wed, 13 Sep 2023 17:20:56 +0200 Subject: [PATCH 19/27] Formatted --- examples/C/src/leader-election/NRP_FD.lf | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index 41c09766..f2c363fe 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -115,7 +115,9 @@ reactor Node( mode Backup { timer t(heartbeat_period, heartbeat_period) // FIXME: Need SENDIMHERETOPRIMARY with "longer interval" - reaction(reset) {= lf_print(PRINTF_TIME ": ---- Node %d becomes backup.", lf_time_logical_elapsed(), self->id); =} + reaction(reset) {= + lf_print(PRINTF_TIME ": ---- Node %d becomes backup.", lf_time_logical_elapsed(), self->id); + =} reaction(node_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); @@ -254,7 +256,9 @@ reactor Node( mode Primary { timer heartbeat(0, heartbeat_period) - reaction(reset) {= lf_print(PRINTF_TIME ": ---- Node %d becomes primary.", lf_time_logical_elapsed(), self->id); =} + reaction(reset) {= + lf_print(PRINTF_TIME ": ---- Node %d becomes primary.", lf_time_logical_elapsed(), self->id); + =} reaction(node_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); @@ -347,9 +351,13 @@ reactor Switch( if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} - reaction(pending_out1) -> out1 {= lf_set(out1, pending_out1->value); =} + reaction(pending_out1) -> out1 {= + lf_set(out1, pending_out1->value); + =} - reaction(pending_out2) -> out2 {= lf_set(out2, pending_out2->value); =} + reaction(pending_out2) -> out2 {= + lf_set(out2, pending_out2->value); + =} reaction(in1, in2) -> out1, out2, pending_out1, pending_out2 {= if (in1->is_present) { @@ -413,7 +421,9 @@ reactor Switch( } mode Failed { - reaction(reset) {= lf_print(PRINTF_TIME ": ==== Switch %d fails.", lf_time_logical_elapsed(), self->id); =} + reaction(reset) {= + lf_print(PRINTF_TIME ": ==== Switch %d fails.", lf_time_logical_elapsed(), self->id); + =} } } From 650ac03114add5bcd5685a2855c8bb50f84dd1b8 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Tue, 31 Oct 2023 16:40:03 -0700 Subject: [PATCH 20/27] Use multiport and message server --- examples/C/src/leader-election/NRP_FD.lf | 470 ++++++++++++----------- 1 file changed, 251 insertions(+), 219 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index f2c363fe..cfc4e235 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -3,8 +3,8 @@ * is replaced by a backup node. The protocol is described in this paper: * * Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Consistency Before - * Availability: Network Reference Point based Failure Detection for Controller Redundancy," paper - * draft 8/15/23. + * Availability: Network Reference Point based Failure Detection for Controller Redundancy," Emerging + * Technologies and Factory Automation (ETFA), 12-15 September 2023, DOI: 10.1109/ETFA54631.2023.10275664 * * The key idea in this protocol is that when a backup fails to detect the heartbeats of a primary * node, it becomes primary only if it has access to Network Reference Point (NRP), which is a point @@ -12,11 +12,14 @@ * of the partition that still has access to the NRP can become a primary. If a primary loses access * to the NRP, then it relinquishes its primary role because it is now on the wrong side of a * network partition. A backup on the right side of the partition will take over. + * + * This implementation omits some details in the paper. See NOTEs in the comments. * * @author Edward A. Lee * @author Marjan Sirjani */ target C { + tracing: true, timeout: 20 s } @@ -24,10 +27,16 @@ preamble {= #ifndef NRF_FD #define NRF_FD #include "platform.h" // Defines PRINTF_TIME + + // Paper calls for manual intervention to set initial primary ID and NRP network. + // Here, we just hardwire this choice using #define. + #define INITIAL_PRIMARY_ID 1 + #define INITIAL_NRP_NETWORK 0 + enum message_type { heartbeat, - pingNRP, - pingNRP_response, + ping_NRP, + ping_NRP_response, request_new_NRP, new_NRP }; @@ -35,6 +44,7 @@ preamble {= enum message_type type; int source; int destination; + int payload; } message_t; #endif // NRF_FD =} @@ -49,273 +59,302 @@ reactor Node( nrp_timeout: time = 500 ms) { // There are two network interfaces: @side("east") - input in1: message_t - @side("east") - input in2: message_t - output out1: message_t - output out2: message_t + input[2] in: message_t + output[2] out: message_t timer node_fails(fails_at_time) - state heartbeats_missed_1: int = 0 - state heartbeats_missed_2: int = 0 - - state NRP_network: int = 1 - state NRP_switch_id: int = 1 - state NRP_pending: bool = true - state become_primary_on_ping_response: bool = false - logical action new_NRP_request_timed_out(nrp_timeout) - + state heartbeats_missed: int[2] = {0} state primary: int = 0 // The known primary node. - state ping_pending: bool = false + state ping_timeout_pending: bool = false + state become_primary_on_ping_response: bool = false + state NRP_network: int = {= INITIAL_NRP_NETWORK =} + state NRP_switch_id: int = 0 // 0 means not known. + logical action ping_timed_out(ping_timeout) + logical action new_NRP_request_timed_out(nrp_timeout) initial mode Waiting { - reaction(startup) -> out1, out2 {= - // NOTE: The paper does not specify how to select the initial NRP. - // Here, we send with destination 0, which the switches interpret as first to respond. - // First to respond will be id 1 at startup; then wait for a reply before - // actually becoming the primary or backup. - message_t ping_message ={pingNRP, self->id, 0}; - lf_set(out1, ping_message); + reaction(startup) -> out {= + // If I am the initial primary, broadcast a ping on network 1. + // The first switch to get this will respond. + if (self->id == INITIAL_PRIMARY_ID) { + message_t ping_message = {ping_NRP, self->id, 0, 0}; + lf_set(out[INITIAL_NRP_NETWORK], ping_message); + // Instead of scheduling ping_timed_out, we just continue waiting until a ping response arrives. + } =} - reaction(in1, in2) -> out1, out2, reset(Backup), reset(Primary) {= - // Got a response to the ping from one or both switches. - // NOTE: The paper calls for user intervention to select which is primary. - // Here, we just choose id 1 to be primary. - self->primary = 1; - if (self->id == 1) { - // Become primary. - lf_set_mode(Primary); - if (in1->is_present && in1->value.type == pingNRP_response) { - lf_print(PRINTF_TIME ": Primary node %d received ping response on network 1. Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, in1->value.source); - self->NRP_network = 1; - self->NRP_switch_id = in1->value.source; - self->NRP_pending = false; - // Notify the backup of the NRP. - message_t message = {new_NRP, in1->value.source, 0}; - lf_set(out1, message); - } else if (in2->is_present && in2->value.type == pingNRP_response) { - lf_print(PRINTF_TIME ": Primary node %d received ping response on network 2. Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, in2->value.source); - self->NRP_network = 2; - self->NRP_switch_id = in2->value.source; - self->NRP_pending = false; - // Notify the backup of the NRP. - message_t message = {new_NRP, in1->value.source, 0}; - lf_set(out2, message); + reaction(in) -> out, reset(Backup), reset(Primary) {= + // Iterate over input channels. + for (int c = 0; c < in_width; c++) { + if (in[c]->is_present) { + // In this mode, primary is waiting for a ping response and backup for a new NRP. + if (self->id == INITIAL_PRIMARY_ID && in[c]->value.type == ping_NRP_response) { + // Become primary. + self->primary = self->id; + lf_set_mode(Primary); + + lf_print(PRINTF_TIME ": Primary node %d received ping response on network %d. " + "Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, c, in[c]->value.source + ); + self->NRP_network = c; + self->NRP_switch_id = in[c]->value.source; + // Notify the backup of the NRP. Destination 0 here means broadcast. + message_t message = {new_NRP, self->id, 0, in[c]->value.source}; + // Send new NRP message on all networks. + for (int i = 0; i < out_width; i++) lf_set(out[i], message); + } else if (in[c]->value.type == new_NRP) { + // Become backup. Source of the message is the primary. + self->primary = in[c]->value.source; + lf_set_mode(Backup); + } } - } else { - lf_set_mode(Backup); } =} - } + } // mode Waiting - mode Backup { - timer t(heartbeat_period, heartbeat_period) - // FIXME: Need SENDIMHERETOPRIMARY with "longer interval" + mode Primary { + timer heartbeat(0, heartbeat_period) reaction(reset) {= - lf_print(PRINTF_TIME ": ---- Node %d becomes backup.", lf_time_logical_elapsed(), self->id); + lf_print(PRINTF_TIME ": ---- Node %d becomes primary.", lf_time_logical_elapsed(), self->id); =} reaction(node_fails) -> reset(Failed) {= if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} - reaction(in1) -> reset(Primary) {= - if (in1->value.type == heartbeat) { - lf_print(PRINTF_TIME ": Backup node %d received heartbeat from node %d on network 1.", lf_time_logical_elapsed(), self->id, in1->value.source); - self->heartbeats_missed_1 = 0; - } else if (in1->value.type == pingNRP_response && in1->value.destination == self->id) { - // Got a response from the NRP to a ping we sent after a partial or complete timeout. - lf_print(PRINTF_TIME ": Backup node %d received ping response on network 1 from NRP on switch %d.", lf_time_logical_elapsed(), self->id, in1->value.source); - // If there was a timeout on both networks that was not simultaneous, then - // we tried pinging the NRP before becoming primary. - if (self->become_primary_on_ping_response) { - lf_set_mode(Primary); - self->become_primary_on_ping_response = false; + reaction(heartbeat) -> out, ping_timed_out {= + lf_print(PRINTF_TIME ": Primary node %d sends heartbeat on both networks.", + lf_time_logical_elapsed(), self->id + ); + message_t message = {heartbeat, self->id, 0, 0}; + for (int i = 0; i < out_width; i++) lf_set(out[i], message); + + // Ping the NRP if there is one and there isn't a ping timeout pending. + if (self->NRP_switch_id != 0 && !self->ping_timeout_pending) { + message_t ping = {ping_NRP, self->id, self->NRP_switch_id, 0}; + lf_set(out[self->NRP_network], ping); + self->ping_pending = true; + self->ping_timeout_pending = true; + lf_schedule(ping_timed_out, 0); + } + =} + + reaction(in) -> out, ping_timed_out {= + // Iterate over input channels. + for (int c = 0; c < in_width; c++) { + if (in[c]->is_present) { + if (in[c]->value.type == request_new_NRP) { + // Backup is asking for a new NRP. Invalidate current NRP. + self->NRP_switch_id = 0; + + // Switch networks. + if (self->NRP_network == 0) self->NRP_network = 1; + else self->NRP_network = 0; + + lf_print(PRINTF_TIME ": Primary node %d looking for new NRP on network %d.", + lf_time_logical_elapsed(), self->id, self->NRP_network + ); + message_t message = {ping_NRP, self->id, 0, 0}; + lf_set(out[self->NRP_network], message); + self->ping_pending = true; + self->ping_timeout_pending = true; + lf_schedule(ping_timed_out, 0); + } else if (in[c]->value.type == ping_NRP_response) { + lf_print(PRINTF_TIME ": Primary node %d received ping response on network %d. NRP is %d.", + lf_time_logical_elapsed(), self->id, c, in[c]->value.source + ); + self->ping_pending = false; + if (self->NRP_switch_id == 0) { + // This is a new NRP. + self->NRP_switch_id = in[c]->value.source; + // Notify the backup of the NRP. Destination 0 here means broadcast. + message_t message = {new_NRP, self->id, 0, in[c]->value.source}; + // Send new NRP message on all networks. + for (int i = 0; i < out_width; i++) lf_set(out[i], message); + lf_print(PRINTF_TIME ": Primary node %d notifies backup of new NRP %d.", + lf_time_logical_elapsed(), self->id, self->NRP_switch_id + ); + // NOTE: Should the primary get some confirmation from the backup? + } + } } + } + =} + + reaction(ping_timed_out) -> out, ping_timed_out, Failed {= + self->ping_timeout_pending = false; + if (self->ping_pending) { + // Ping timed out. self->ping_pending = false; - } else if (in1->value.type == new_NRP) { - // FIXME: Ping the new NRP and send confirmation back to primary. - self->NRP_network = 1; - self->NRP_switch_id = in1->value.source; - self->NRP_pending = false; + lf_print(PRINTF_TIME ": Primary node %d gets no response from ping.", + lf_time_logical_elapsed(), self->id + ); + if (self->NRP_switch_id == 0) { + // Failed to get a new NRP. Declare failure. + lf_set_mode(Failed); + } else { + // Invalidate current NRP. + self->NRP_switch_id = 0; + + // Switch networks. + if (self->NRP_network == 0) self->NRP_network = 1; + else self->NRP_network = 0; + + lf_print(PRINTF_TIME ": Primary node %d looking for new NRP on network %d.", + lf_time_logical_elapsed(), self->id, self->NRP_network + ); + message_t message = {ping_NRP, self->id, 0, 0}; + lf_set(out[self->NRP_network], message); + self->ping_pending = true; + lf_schedule(ping_timed_out, 0); + } } + =} + } // mode Primary + + mode Backup { + timer t(heartbeat_period, heartbeat_period) + // NOTE: Paper says to SENDIMHERETOPRIMARY with "longer interval". + // Is this really necessary? + reaction(reset) {= + lf_print(PRINTF_TIME ": ---- Node %d becomes backup.", lf_time_logical_elapsed(), self->id); =} - reaction(in2) -> reset(Primary) {= - if (in2->value.type == heartbeat) { - lf_print(PRINTF_TIME ": Backup node %d received heartbeat from node %d on network 2.", lf_time_logical_elapsed(), self->id, in2->value.source); - self->heartbeats_missed_2 = 0; - } else if (in2->value.type == pingNRP_response && in2->value.destination == self->id) { - // Got a response from the NRP to a ping we sent after a partial timeout. - lf_print(PRINTF_TIME ": Backup node %d received ping response on network 2 from NRP on switch %d.", lf_time_logical_elapsed(), self->id, in2->value.source); - self->ping_pending = false; - // If there was a timeout on both networks that was not simultaneous, then - // we tried pinging the NRP before becoming primary. - if (self->become_primary_on_ping_response) { - lf_set_mode(Primary); - self->become_primary_on_ping_response = false; + reaction(node_fails) -> reset(Failed) {= + if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); + =} + + reaction(in) -> reset(Primary) {= + // Iterate over input channels. + for (int c = 0; c < in_width; c++) { + if (in[c]->is_present) { + if (in[c]->value.type == heartbeat) { + lf_print(PRINTF_TIME ": Backup node %d received heartbeat from node %d on network %d.", + lf_time_logical_elapsed(), self->id, in[c]->value.source, c + ); + self->heartbeats_missed[c] = 0; + } else if (in[c]->value.type == ping_NRP_response && in[c]->value.destination == self->id) { + // Got a response from the NRP to a ping we sent. + lf_print(PRINTF_TIME ": Backup node %d received ping response on network %d from NRP on switch %d.", + lf_time_logical_elapsed(), self->id, c, in[c]->value.source + ); + // If there was a timeout on both networks that was not simultaneous, then + // we tried pinging the NRP before becoming primary. + if (self->become_primary_on_ping_response) { + lf_set_mode(Primary); + self->become_primary_on_ping_response = false; + } + self->ping_pending = false; + } else if (in[c]->value.type == new_NRP) { + // NOTE: Should ping the new NRP and send confirmation back to primary. + self->NRP_network = c; + self->NRP_switch_id = in[c]->value.source; + } } - } else if (in2->value.type == new_NRP) { - // FIXME: Ping the new NRP and send confirmation back to primary. - self->NRP_network = 2; - self->NRP_switch_id = in2->value.source; - self->NRP_pending = false; } =} - reaction(t) -> reset(Primary), out1, out2, ping_timed_out {= - if (self->heartbeats_missed_1 > self->max_missed_heartbeats - && self->heartbeats_missed_2 > self->max_missed_heartbeats) { + reaction(t) -> reset(Primary), out, ping_timed_out {= + if (self->heartbeats_missed[0] > self->max_missed_heartbeats + && self->heartbeats_missed[1] > self->max_missed_heartbeats) { // Simultaneous heartbeat misses. // In the paper, this is tmoAllNotSimul. // For the tmoAllSimul optimization in the paper, we assume that if - // self->heartbeats_missed_1 == self->heartbeats_missed_2, then most likely, it is + // self->heartbeats_missed[0] == self->heartbeats_missed[1], then most likely, it is // the primary that failed, and not the network, so can immediately become the primary. // Otherwise, it is possible that one network failed, and then the other failed, in which // case, we may have a partitioned network. - lf_print(PRINTF_TIME ": **** Backup node %d detects missing heartbeats on both networks.", lf_time_logical_elapsed(), self->id); - if (self->heartbeats_missed_1 == self->heartbeats_missed_2) { - lf_print(PRINTF_TIME ": **** Missing heartbeats on both networks were simultaneous. Assume the primary failed.", lf_time_logical_elapsed()); + lf_print(PRINTF_TIME ": **** Backup node %d detects missing heartbeats on both networks.", + lf_time_logical_elapsed(), self->id + ); + if (self->heartbeats_missed[0] == self->heartbeats_missed[1]) { + lf_print(PRINTF_TIME ": **** Missing heartbeats on both networks were simultaneous. " + "Assume the primary failed.", + lf_time_logical_elapsed() + ); lf_set_mode(Primary); } else { // Ping the NRP because if we can't access it, we are on the wrong side of // a network partition and could end up with two primaries. - message_t message = {pingNRP, self->id, self->NRP_switch_id}; - if (self->NRP_network == 1) { - lf_set(out1, message); - } else { - lf_set(out2, message); - } + message_t message = {ping_NRP, self->id, self->NRP_switch_id, 0}; + lf_set(out[self->NRP_network], message); // Wait for a response before becoming primary. self->become_primary_on_ping_response = true; lf_schedule(ping_timed_out, 0); } - self->heartbeats_missed_1 = 0; // Prevent detecting again immediately. - self->heartbeats_missed_2 = 0; - } else if (self->heartbeats_missed_1 > self->max_missed_heartbeats - || self->heartbeats_missed_2 > self->max_missed_heartbeats) { + self->heartbeats_missed[0] = 0; // Prevent detecting again immediately. + self->heartbeats_missed[1] = 0; + } else if (self->heartbeats_missed[0] > self->max_missed_heartbeats + || self->heartbeats_missed[1] > self->max_missed_heartbeats) { // Heartbeat missed on one network but not yet on the other. // Ping the NRP to make sure we retain access to it so that we can be an effective backup. // This corresponds to tmoSomeNotAll in the paper. - lf_print(PRINTF_TIME ": **** Backup node %d detects missing heartbeats on one network.", lf_time_logical_elapsed(), self->id); + lf_print(PRINTF_TIME ": **** Backup node %d detects missing heartbeats on one network.", + lf_time_logical_elapsed(), self->id + ); // Ping the NRP. - message_t message = {pingNRP, self->id, self->NRP_switch_id}; - if (!self->ping_pending && !self->NRP_pending) { - if (self->NRP_network == 1) { - lf_set(out1, message); - } else { - lf_set(out2, message); - } - lf_print(PRINTF_TIME ": Backup node %d pings NRP on network %d, switch %d", lf_time_logical_elapsed(), self->id, self->NRP_network, self->NRP_switch_id); + message_t message = {ping_NRP, self->id, self->NRP_switch_id, 0}; + if (!self->ping_pending && self->NRP_switch_id != 0) { + lf_set(out[self->NRP_network], message); + lf_print(PRINTF_TIME ": Backup node %d pings NRP on network %d, switch %d", + lf_time_logical_elapsed(), self->id, self->NRP_network, self->NRP_switch_id + ); self->ping_pending = true; lf_schedule(ping_timed_out, 0); } } // Increment the counters so if they are not reset to 0 by the next time, // we detect the missed heartbeat. - self->heartbeats_missed_1++; - self->heartbeats_missed_2++; + self->heartbeats_missed[0]++; + self->heartbeats_missed[1]++; =} - reaction(ping_timed_out) -> out1, out2, new_NRP_request_timed_out {= + reaction(ping_timed_out) -> out, new_NRP_request_timed_out, Failed {= + self->ping_timeout_pending = false; if (self->ping_pending) { // Ping timed out. lf_print(PRINTF_TIME ": Backup node %d gets no response from ping.", lf_time_logical_elapsed(), self->id); - if (!self->NRP_pending) { + if (self->NRP_switch_id != 0) { // Send request for new NRP on the other network. lf_print(PRINTF_TIME ": Backup node %d requests new NRP.", lf_time_logical_elapsed(), self->id); - message_t message = {request_new_NRP, self->id, self->primary}; - if (self->NRP_network == 1) { - // Use network 2. - lf_set(out2, message); - } else { - lf_set(out1, message); - } - self->NRP_pending = true; + + // Invalidate current NRP. + self->NRP_switch_id = 0; + + // Switch networks. + if (self->NRP_network == 0) self->NRP_network = 1; + else self->NRP_network = 0; + + message_t message = {request_new_NRP, self->id, self->primary, 0}; + lf_set(out[self->NRP_network], message); + lf_schedule(new_NRP_request_timed_out, 0); + } else { + // Failed to connect to new NRP. + lf_set_mode(Failed); } self->ping_pending = false; } =} reaction(new_NRP_request_timed_out) {= - if (self->NRP_pending) { - self->NRP_pending = false; - lf_print(PRINTF_TIME ": Backup node %d new NRP request timed out. Will not function as backup.", lf_time_logical_elapsed(), self->id); + if (self->NRP_switch_id == 0) { + lf_print(PRINTF_TIME ": Backup node %d new NRP request timed out. Will not function as backup.", + lf_time_logical_elapsed(), self->id + ); if (self->become_primary_on_ping_response) { - lf_print(PRINTF_TIME ": Network is likely partitioned. Remaining as (non-functional) backup.", lf_time_logical_elapsed()); + lf_print(PRINTF_TIME ": Network is likely partitioned. Remaining as (non-functional) backup.", + lf_time_logical_elapsed() + ); self->become_primary_on_ping_response = false; } } =} } - mode Primary { - timer heartbeat(0, heartbeat_period) - reaction(reset) {= - lf_print(PRINTF_TIME ": ---- Node %d becomes primary.", lf_time_logical_elapsed(), self->id); - =} - - reaction(node_fails) -> reset(Failed) {= - if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); - =} - - reaction(heartbeat) -> out1, out2 {= - lf_print(PRINTF_TIME ": Primary node %d sends heartbeat on both networks.", lf_time_logical_elapsed(), self->id); - message_t message = {heartbeat, self->id, 0}; - lf_set(out1, message); - lf_set(out2, message); - =} - - reaction(in1) -> out1 {= - if (in1->value.type == request_new_NRP) { - // Find a new candidate NRP on network 1. - lf_print(PRINTF_TIME ": Primary node %d looking for new NRP on network 1.", lf_time_logical_elapsed(), self->id); - message_t message = {pingNRP, self->id, 0}; - lf_set(out1, message); - self->NRP_pending = true; - } else if (in1->value.type == pingNRP_response) { - lf_print(PRINTF_TIME ": Primary node %d received ping response on network 1. NRP is %d.", lf_time_logical_elapsed(), self->id, in1->value.source); - self->NRP_network = 1; - self->NRP_switch_id = in1->value.source; - if (self->NRP_pending) { - self->NRP_pending = false; - // Notify backup of new NRP. source field encodes the switch id. - lf_print(PRINTF_TIME ": Primary node %d notifies backup of new NRP %d.", lf_time_logical_elapsed(), self->id, in1->value.source); - message_t message = {new_NRP, in1->value.source, 0}; - lf_set(out1, message); - // FIXME: Wait for confirmation of new NRP with backup. - } - } - =} - - reaction(in2) -> out2 {= - if (in2->value.type == request_new_NRP) { - // Find a new candidate NRP on network 2. - lf_print(PRINTF_TIME ": Primary node %d looking for new NRP on network 2.", lf_time_logical_elapsed(), self->id); - message_t message = {pingNRP, self->id, 0}; - lf_set(out2, message); - self->NRP_pending = true; - } else if (in2->value.type == pingNRP_response) { - lf_print(PRINTF_TIME ": Primary node %d received ping response on network 2. NRP is %d.", lf_time_logical_elapsed(), self->id, in2->value.source); - self->NRP_network = 2; - self->NRP_switch_id = in2->value.source; - if (self->NRP_pending) { - self->NRP_pending = false; - // Notify backup of new NRP. source field encodes the switch id. - lf_print(PRINTF_TIME ": Primary node %d notifies backup of new NRP %d.", lf_time_logical_elapsed(), self->id, in2->value.source); - message_t message = {new_NRP, in2->value.source, 0}; - lf_set(out2, message); - // FIXME: Wait for confirmation of new NRP with backup. - } - } - =} - } - mode Failed { reaction(reset) {= lf_print(PRINTF_TIME ": #### Node %d fails.", lf_time_logical_elapsed(), self->id); @@ -324,9 +363,9 @@ reactor Node( } /** - * Switch with two interfaces. When a pingNRP message arrives on either interface, if the + * Switch with two interfaces. When a ping_NRP message arrives on either interface, if the * destination matches the ID of this switch or the destination is 0, then the switch responds on - * the same interface with a pingNRP_response message. When any other message arrives on either + * the same interface with a ping_NRP_response message. When any other message arrives on either * interface, the switch forwards a copy of the message to the other interface. If any two messages * would be simultaneous on an output, one will be sent one microstep later. */ @@ -361,11 +400,11 @@ reactor Switch( reaction(in1, in2) -> out1, out2, pending_out1, pending_out2 {= if (in1->is_present) { - if (in1->value.type == pingNRP) { + if (in1->value.type == ping_NRP) { if (in1->value.destination == self->id || in1->value.destination == 0) { lf_print(PRINTF_TIME ": ==== Switch %d pinged by node %d. Responding.", lf_time_logical_elapsed(), self->id, in1->value.source); // Respond to the ping. - message_t message = {pingNRP_response, self->id, in1->value.source}; + message_t message = {ping_NRP_response, self->id, in1->value.source}; if (!out1->is_present) { lf_set(out1, message); } else { @@ -389,11 +428,11 @@ reactor Switch( } } if (in2->is_present) { - if (in2->value.type == pingNRP) { + if (in2->value.type == ping_NRP) { if (in2->value.destination == self->id) { lf_print(PRINTF_TIME ": ==== Switch %d pinged by node %d. Responding.", lf_time_logical_elapsed(), self->id, in2->value.source); // Construct a response to the ping. - message_t message = {pingNRP_response, self->id, in2->value.source}; + message_t message = {ping_NRP_response, self->id, in2->value.source}; // Respond to the ping if out2 is available. if (!out2->is_present) { lf_set(out2, message); @@ -426,32 +465,25 @@ reactor Switch( =} } } - -federated reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { +// FIXME: This should be federated, but bugs in federated execution make it fail. +main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 10 s) - node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) - switch1 = new Switch(id=1, fails_at_time = 3 s) + switch3 = new Switch(id=3) + + node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) switch2 = new Switch(id=2) + switch4 = new Switch(id=4) - node1.out1 -> switch1.in1 after delay - switch1.out1 -> node1.in1 after delay + node1.out -> switch1.in1, switch3.in1 after delay + switch1.out1, switch3.out1 -> node1.in after delay switch1.out2 -> switch2.in2 after delay switch2.out2 -> switch1.in2 after delay - switch2.out1 -> node2.in1 after delay - node2.out1 -> switch2.in1 after delay - - switch3 = new Switch(id=3) - switch4 = new Switch(id=4) - - node1.out2 -> switch3.in1 after delay - switch3.out1 -> node1.in2 after delay + switch2.out1, switch4.out1 -> node2.in after delay + node2.out -> switch2.in1, switch4.in1 after delay switch3.out2 -> switch4.in2 after delay switch4.out2 -> switch3.in2 after delay - - switch4.out1 -> node2.in2 after delay - node2.out2 -> switch4.in1 after delay } From aef6305bcb579df5c836d82a78d84533a558a251 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Mon, 15 Jan 2024 09:01:53 -0800 Subject: [PATCH 21/27] Updated to use multiports and make federated --- examples/C/src/leader-election/NRP_FD.lf | 7 ++--- .../leader-election/NRP_FD_Partitioning.lf | 28 ++++++++----------- .../leader-election/NRP_FD_PrimaryFails.lf | 26 ++++++++--------- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index cfc4e235..ddc77a48 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -1,6 +1,6 @@ /** - * This program models a redundant fault tolerant system where a primary node, if and when it fails, - * is replaced by a backup node. The protocol is described in this paper: + * This program implements a redundant fault-tolerant system where a primary node, if and when + * it fails, is replaced by a backup node. The protocol is described in this paper: * * Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Consistency Before * Availability: Network Reference Point based Failure Detection for Controller Redundancy," Emerging @@ -465,8 +465,7 @@ reactor Switch( =} } } -// FIXME: This should be federated, but bugs in federated execution make it fail. -main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { +federated reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 10 s) switch1 = new Switch(id=1, fails_at_time = 3 s) switch3 = new Switch(id=3) diff --git a/examples/C/src/leader-election/NRP_FD_Partitioning.lf b/examples/C/src/leader-election/NRP_FD_Partitioning.lf index cbe55ca1..62c01a95 100644 --- a/examples/C/src/leader-election/NRP_FD_Partitioning.lf +++ b/examples/C/src/leader-election/NRP_FD_Partitioning.lf @@ -1,36 +1,32 @@ // This version partitions the network and shows that the protocol // prevents the backup from becoming primary, thereby preventing // two primaries. -target C +target C { + tracing: true, + timeout: 20 s +} import Switch, Node from "NRP_FD.lf" -main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { +federated reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 15 s) node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) switch1 = new Switch(id=1, fails_at_time = 3 s) switch2 = new Switch(id=2) + switch3 = new Switch(id=3) + // Failure of switch4 will partition the network. + switch4 = new Switch(id=4, fails_at_time = 10 s) - node1.out1 -> switch1.in1 after delay - switch1.out1 -> node1.in1 after delay + node1.out -> switch1.in1, switch3.in1 after delay + switch1.out1, switch3.out1 -> node1.in after delay switch1.out2 -> switch2.in2 after delay switch2.out2 -> switch1.in2 after delay - switch2.out1 -> node2.in1 after delay - node2.out1 -> switch2.in1 after delay - - switch3 = new Switch(id=3) - // Failure of switch4 will partition the network. - switch4 = new Switch(id=4, fails_at_time = 10 s) - - node1.out2 -> switch3.in1 after delay - switch3.out1 -> node1.in2 after delay + switch2.out1, switch4.out1 -> node2.in after delay + node2.out -> switch2.in1, switch4.in1 after delay switch3.out2 -> switch4.in2 after delay switch4.out2 -> switch3.in2 after delay - - switch4.out1 -> node2.in2 after delay - node2.out2 -> switch4.in1 after delay } diff --git a/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf b/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf index 57f7fc17..b0973995 100644 --- a/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf +++ b/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf @@ -1,34 +1,30 @@ // This version simply has the primary failing after 5 seconds. // Switch 1 remains the NRP. -target C +target C { + tracing: true, + timeout: 20 s +} import Switch, Node from "NRP_FD.lf" -main reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { +federated reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 5 s) node2 = new Node(heartbeat_period=heartbeat_period, id=2, fails_at_time = 15 s) switch1 = new Switch(id=1) switch2 = new Switch(id=2) + switch3 = new Switch(id=3) + switch4 = new Switch(id=4) - node1.out1 -> switch1.in1 after delay - switch1.out1 -> node1.in1 after delay + node1.out -> switch1.in1, switch3.in1 after delay + switch1.out1, switch3.out1 -> node1.in after delay switch1.out2 -> switch2.in2 after delay switch2.out2 -> switch1.in2 after delay - switch2.out1 -> node2.in1 after delay - node2.out1 -> switch2.in1 after delay - - switch3 = new Switch(id=3) - switch4 = new Switch(id=4) - - node1.out2 -> switch3.in1 after delay - switch3.out1 -> node1.in2 after delay + switch2.out1, switch4.out1 -> node2.in after delay + node2.out -> switch2.in1, switch4.in1 after delay switch3.out2 -> switch4.in2 after delay switch4.out2 -> switch3.in2 after delay - - switch4.out1 -> node2.in2 after delay - node2.out2 -> switch4.in1 after delay } From 358b7a20fb9ecb00da3bd7b810061573bed5fdba Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Mon, 15 Jan 2024 12:53:09 -0800 Subject: [PATCH 22/27] Avoid overwriting heartbeat with ping --- examples/C/src/leader-election/NRP_FD.lf | 66 +++++++++++++++++------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index ddc77a48..fddbb44f 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -20,6 +20,7 @@ */ target C { tracing: true, + logging: DEBUG, timeout: 20 s } @@ -52,6 +53,7 @@ preamble {= reactor Node( id: int = 0, heartbeat_period: time = 1 s, + routine_ping_offset: time = 1 ms, // Time after heartbeat to ping NRP. max_missed_heartbeats: int = 2, fails_at_time: time = 0, // For testing. 0 for no failure. ping_timeout: time = 500 ms, // Time until ping is deemed to have failed. @@ -66,8 +68,8 @@ reactor Node( state heartbeats_missed: int[2] = {0} state primary: int = 0 // The known primary node. - state ping_pending: bool = false - state ping_timeout_pending: bool = false + state ping_pending: bool = false // Ping has been issued and not responded to. + state ping_timeout_pending: bool = false // Ping timeout timer hasn't expired. state become_primary_on_ping_response: bool = false state NRP_network: int = {= INITIAL_NRP_NETWORK =} state NRP_switch_id: int = 0 // 0 means not known. @@ -96,7 +98,7 @@ reactor Node( self->primary = self->id; lf_set_mode(Primary); - lf_print(PRINTF_TIME ": Primary node %d received ping response on network %d. " + lf_print(PRINTF_TIME ": Initial primary node %d received ping response on network %d. " "Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, c, in[c]->value.source ); self->NRP_network = c; @@ -106,9 +108,18 @@ reactor Node( // Send new NRP message on all networks. for (int i = 0; i < out_width; i++) lf_set(out[i], message); } else if (in[c]->value.type == new_NRP) { - // Become backup. Source of the message is the primary. - self->primary = in[c]->value.source; - lf_set_mode(Backup); + if (in[c]->value.payload != self->NRP_switch_id) { + // Message is not redundant (new_NRP sent on both networks). + // Become backup. Source of the message is the primary. + lf_print(PRINTF_TIME ": Waiting node %d received new NRP %d on network %d. " + "Becoming backup.", lf_time_logical_elapsed(), self->id, in[c]->value.payload, + c, in[c]->value.source + ); + self->primary = in[c]->value.source; + self->NRP_switch_id = in[c]->value.payload; + self->NRP_network = c; + lf_set_mode(Backup); + } } } } @@ -117,6 +128,7 @@ reactor Node( mode Primary { timer heartbeat(0, heartbeat_period) + timer ping_NRP_timer(routine_ping_offset, heartbeat_period) reaction(reset) {= lf_print(PRINTF_TIME ": ---- Node %d becomes primary.", lf_time_logical_elapsed(), self->id); =} @@ -125,15 +137,20 @@ reactor Node( if(lf_time_logical_elapsed() > 0LL) lf_set_mode(Failed); =} - reaction(heartbeat) -> out, ping_timed_out {= + reaction(heartbeat) -> out {= lf_print(PRINTF_TIME ": Primary node %d sends heartbeat on both networks.", lf_time_logical_elapsed(), self->id ); message_t message = {heartbeat, self->id, 0, 0}; for (int i = 0; i < out_width; i++) lf_set(out[i], message); - + =} + + reaction(ping_NRP_timer) -> out, ping_timed_out {= // Ping the NRP if there is one and there isn't a ping timeout pending. if (self->NRP_switch_id != 0 && !self->ping_timeout_pending) { + lf_print(PRINTF_TIME ": Primary node %d pings NRP %d (routine).", + lf_time_logical_elapsed(), self->id, self->NRP_switch_id + ); message_t ping = {ping_NRP, self->id, self->NRP_switch_id, 0}; lf_set(out[self->NRP_network], ping); self->ping_pending = true; @@ -170,12 +187,12 @@ reactor Node( if (self->NRP_switch_id == 0) { // This is a new NRP. self->NRP_switch_id = in[c]->value.source; - // Notify the backup of the NRP. Destination 0 here means broadcast. - message_t message = {new_NRP, self->id, 0, in[c]->value.source}; - // Send new NRP message on all networks. - for (int i = 0; i < out_width; i++) lf_set(out[i], message); - lf_print(PRINTF_TIME ": Primary node %d notifies backup of new NRP %d.", - lf_time_logical_elapsed(), self->id, self->NRP_switch_id + self->NRP_network = c; + // Notify the backup of the NRP on the NRP's network. + message_t message = {new_NRP, self->id, 0, self->NRP_switch_id}; + lf_set(out[c], message); + lf_print(PRINTF_TIME ": Primary node %d notifies backup of new NRP %d on network %d.", + lf_time_logical_elapsed(), self->id, self->NRP_switch_id, c ); // NOTE: Should the primary get some confirmation from the backup? } @@ -209,6 +226,7 @@ reactor Node( message_t message = {ping_NRP, self->id, 0, 0}; lf_set(out[self->NRP_network], message); self->ping_pending = true; + self->ping_timeout_pending = true; lf_schedule(ping_timed_out, 0); } } @@ -241,6 +259,7 @@ reactor Node( lf_print(PRINTF_TIME ": Backup node %d received ping response on network %d from NRP on switch %d.", lf_time_logical_elapsed(), self->id, c, in[c]->value.source ); + self->NRP_switch_id = in[c]->value.source; // If there was a timeout on both networks that was not simultaneous, then // we tried pinging the NRP before becoming primary. if (self->become_primary_on_ping_response) { @@ -249,9 +268,12 @@ reactor Node( } self->ping_pending = false; } else if (in[c]->value.type == new_NRP) { - // NOTE: Should ping the new NRP and send confirmation back to primary. + // FIXME: Should ping the new NRP and send confirmation back to primary. + lf_print(PRINTF_TIME ": Backup node %d received new NRP %d on network %d.", + lf_time_logical_elapsed(), self->id, in[c]->value.payload, c + ); self->NRP_network = c; - self->NRP_switch_id = in[c]->value.source; + self->NRP_switch_id = in[c]->value.payload; } } } @@ -276,14 +298,20 @@ reactor Node( lf_time_logical_elapsed() ); lf_set_mode(Primary); - } else { + } else if (self->NRP_switch_id != 0) { // Ping the NRP because if we can't access it, we are on the wrong side of // a network partition and could end up with two primaries. message_t message = {ping_NRP, self->id, self->NRP_switch_id, 0}; lf_set(out[self->NRP_network], message); // Wait for a response before becoming primary. self->become_primary_on_ping_response = true; + self->ping_pending = true; + self->ping_timeout_pending = true; lf_schedule(ping_timed_out, 0); + } else { + lf_print_warning(PRINTF_TIME "**** Do not know which switch is the NRP! Cannot become primary.", + lf_time_logical_elapsed() + ); } self->heartbeats_missed[0] = 0; // Prevent detecting again immediately. self->heartbeats_missed[1] = 0; @@ -297,12 +325,13 @@ reactor Node( ); // Ping the NRP. message_t message = {ping_NRP, self->id, self->NRP_switch_id, 0}; - if (!self->ping_pending && self->NRP_switch_id != 0) { + if (!self->ping_pending && !self->ping_timeout_pending && self->NRP_switch_id != 0) { lf_set(out[self->NRP_network], message); lf_print(PRINTF_TIME ": Backup node %d pings NRP on network %d, switch %d", lf_time_logical_elapsed(), self->id, self->NRP_network, self->NRP_switch_id ); self->ping_pending = true; + self->ping_timeout_pending = true; lf_schedule(ping_timed_out, 0); } } @@ -313,7 +342,6 @@ reactor Node( =} reaction(ping_timed_out) -> out, new_NRP_request_timed_out, Failed {= - self->ping_timeout_pending = false; if (self->ping_pending) { // Ping timed out. lf_print(PRINTF_TIME ": Backup node %d gets no response from ping.", lf_time_logical_elapsed(), self->id); From a89e2815000d925c902b4076f1edceb9e5c8dc48 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Mon, 15 Jan 2024 14:50:29 -0800 Subject: [PATCH 23/27] Format --- examples/C/src/leader-election/NRP_FD.lf | 48 +++++++++++++----------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index fddbb44f..1de9b10f 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -1,10 +1,11 @@ /** - * This program implements a redundant fault-tolerant system where a primary node, if and when - * it fails, is replaced by a backup node. The protocol is described in this paper: + * This program implements a redundant fault-tolerant system where a primary node, if and when it + * fails, is replaced by a backup node. The protocol is described in this paper: * * Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Consistency Before - * Availability: Network Reference Point based Failure Detection for Controller Redundancy," Emerging - * Technologies and Factory Automation (ETFA), 12-15 September 2023, DOI: 10.1109/ETFA54631.2023.10275664 + * Availability: Network Reference Point based Failure Detection for Controller Redundancy," + * Emerging Technologies and Factory Automation (ETFA), 12-15 September 2023, DOI: + * 10.1109/ETFA54631.2023.10275664 * * The key idea in this protocol is that when a backup fails to detect the heartbeats of a primary * node, it becomes primary only if it has access to Network Reference Point (NRP), which is a point @@ -12,8 +13,8 @@ * of the partition that still has access to the NRP can become a primary. If a primary loses access * to the NRP, then it relinquishes its primary role because it is now on the wrong side of a * network partition. A backup on the right side of the partition will take over. - * - * This implementation omits some details in the paper. See NOTEs in the comments. + * + * This implementation omits some details in the paper. See NOTEs in the comments. * * @author Edward A. Lee * @author Marjan Sirjani @@ -28,12 +29,12 @@ preamble {= #ifndef NRF_FD #define NRF_FD #include "platform.h" // Defines PRINTF_TIME - + // Paper calls for manual intervention to set initial primary ID and NRP network. // Here, we just hardwire this choice using #define. #define INITIAL_PRIMARY_ID 1 #define INITIAL_NRP_NETWORK 0 - + enum message_type { heartbeat, ping_NRP, @@ -55,8 +56,8 @@ reactor Node( heartbeat_period: time = 1 s, routine_ping_offset: time = 1 ms, // Time after heartbeat to ping NRP. max_missed_heartbeats: int = 2, - fails_at_time: time = 0, // For testing. 0 for no failure. - ping_timeout: time = 500 ms, // Time until ping is deemed to have failed. + fails_at_time: time = 0, // For testing. 0 for no failure. + ping_timeout: time = 500 ms, // Time until ping is deemed to have failed. // Time until new NRP request is deemed to have failed. nrp_timeout: time = 500 ms) { // There are two network interfaces: @@ -67,13 +68,13 @@ reactor Node( timer node_fails(fails_at_time) state heartbeats_missed: int[2] = {0} - state primary: int = 0 // The known primary node. + state primary: int = 0 // The known primary node. state ping_pending: bool = false // Ping has been issued and not responded to. state ping_timeout_pending: bool = false // Ping timeout timer hasn't expired. state become_primary_on_ping_response: bool = false state NRP_network: int = {= INITIAL_NRP_NETWORK =} - state NRP_switch_id: int = 0 // 0 means not known. - + state NRP_switch_id: int = 0 // 0 means not known. + logical action ping_timed_out(ping_timeout) logical action new_NRP_request_timed_out(nrp_timeout) @@ -84,7 +85,7 @@ reactor Node( if (self->id == INITIAL_PRIMARY_ID) { message_t ping_message = {ping_NRP, self->id, 0, 0}; lf_set(out[INITIAL_NRP_NETWORK], ping_message); - // Instead of scheduling ping_timed_out, we just continue waiting until a ping response arrives. + // Instead of scheduling ping_timed_out, we just continue waiting until a ping response arrives. } =} @@ -97,7 +98,7 @@ reactor Node( // Become primary. self->primary = self->id; lf_set_mode(Primary); - + lf_print(PRINTF_TIME ": Initial primary node %d received ping response on network %d. " "Making switch %d the NRP.", lf_time_logical_elapsed(), self->id, c, in[c]->value.source ); @@ -124,8 +125,9 @@ reactor Node( } } =} - } // mode Waiting + } + // mode Waiting mode Primary { timer heartbeat(0, heartbeat_period) timer ping_NRP_timer(routine_ping_offset, heartbeat_period) @@ -144,7 +146,7 @@ reactor Node( message_t message = {heartbeat, self->id, 0, 0}; for (int i = 0; i < out_width; i++) lf_set(out[i], message); =} - + reaction(ping_NRP_timer) -> out, ping_timed_out {= // Ping the NRP if there is one and there isn't a ping timeout pending. if (self->NRP_switch_id != 0 && !self->ping_timeout_pending) { @@ -200,8 +202,8 @@ reactor Node( } } =} - - reaction(ping_timed_out) -> out, ping_timed_out, Failed {= + + reaction(ping_timed_out) -> out, ping_timed_out, reset(Failed) {= self->ping_timeout_pending = false; if (self->ping_pending) { // Ping timed out. @@ -230,9 +232,10 @@ reactor Node( lf_schedule(ping_timed_out, 0); } } - =} - } // mode Primary + =} + } + // mode Primary mode Backup { timer t(heartbeat_period, heartbeat_period) // NOTE: Paper says to SENDIMHERETOPRIMARY with "longer interval". @@ -341,7 +344,7 @@ reactor Node( self->heartbeats_missed[1]++; =} - reaction(ping_timed_out) -> out, new_NRP_request_timed_out, Failed {= + reaction(ping_timed_out) -> out, new_NRP_request_timed_out, reset(Failed) {= if (self->ping_pending) { // Ping timed out. lf_print(PRINTF_TIME ": Backup node %d gets no response from ping.", lf_time_logical_elapsed(), self->id); @@ -493,6 +496,7 @@ reactor Switch( =} } } + federated reactor(heartbeat_period: time = 1 s, delay: time = 1 ms) { node1 = new Node(heartbeat_period=heartbeat_period, id=1, fails_at_time = 10 s) switch1 = new Switch(id=1, fails_at_time = 3 s) From eccf08db550e1d8fdc480cd817f8aa67b668ed90 Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Mon, 15 Jan 2024 17:38:06 -0800 Subject: [PATCH 24/27] Tuned docs and formatting --- examples/C/src/leader-election/NRP_FD.lf | 5 +++-- examples/C/src/leader-election/NRP_FD_Partitioning.lf | 11 ++++++++--- examples/C/src/leader-election/NRP_FD_PrimaryFails.lf | 11 +++++++++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index 1de9b10f..ea172f35 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -16,12 +16,13 @@ * * This implementation omits some details in the paper. See NOTEs in the comments. * + * This version has switch1 failing at 3s, node1 failing at 10s, and node2 failing at 15s. + * * @author Edward A. Lee * @author Marjan Sirjani */ target C { tracing: true, - logging: DEBUG, timeout: 20 s } @@ -271,7 +272,7 @@ reactor Node( } self->ping_pending = false; } else if (in[c]->value.type == new_NRP) { - // FIXME: Should ping the new NRP and send confirmation back to primary. + // NOTE: Should ping the new NRP and send confirmation back to primary. lf_print(PRINTF_TIME ": Backup node %d received new NRP %d on network %d.", lf_time_logical_elapsed(), self->id, in[c]->value.payload, c ); diff --git a/examples/C/src/leader-election/NRP_FD_Partitioning.lf b/examples/C/src/leader-election/NRP_FD_Partitioning.lf index 62c01a95..3a046333 100644 --- a/examples/C/src/leader-election/NRP_FD_Partitioning.lf +++ b/examples/C/src/leader-election/NRP_FD_Partitioning.lf @@ -1,6 +1,11 @@ -// This version partitions the network and shows that the protocol -// prevents the backup from becoming primary, thereby preventing -// two primaries. +/** + * This version of NRP_FD partitions the network and shows that the protocol prevents the backup + * from becoming primary, thereby preventing two primaries. + * + * @author Edward A. Lee + * @author Marjan Sirjani + */ +// This version target C { tracing: true, timeout: 20 s diff --git a/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf b/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf index b0973995..c9ed2969 100644 --- a/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf +++ b/examples/C/src/leader-election/NRP_FD_PrimaryFails.lf @@ -1,5 +1,12 @@ -// This version simply has the primary failing after 5 seconds. -// Switch 1 remains the NRP. +/** + * This version of NRP_FD simply has the primary (node1) failing after 5 seconds and the backup + * (node2) failing at at 15s. The backup detects simultaneous loss of the heartbeat on both networks + * and hence assumes that the primary has failed rather than there being a network failure. Switch 1 + * remains the NRP. + * + * @author Edward A. Lee + * @author Marjan Sirjani + */ target C { tracing: true, timeout: 20 s From 676ce0770810c7444de123fbf363527423122fff Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Mon, 15 Jan 2024 17:56:09 -0800 Subject: [PATCH 25/27] Added READMEs --- examples/C/README.md | 3 +- examples/C/src/leader-election/README.md | 29 ++++++++++++++++++ examples/C/src/leader-election/img/NRP_FD.png | Bin 0 -> 26824 bytes .../img/NRP_FD_Partitioning.png | Bin 0 -> 27565 bytes .../img/NRP_FD_PrimaryFails.png | Bin 0 -> 27437 bytes 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 examples/C/src/leader-election/README.md create mode 100644 examples/C/src/leader-election/img/NRP_FD.png create mode 100644 examples/C/src/leader-election/img/NRP_FD_Partitioning.png create mode 100644 examples/C/src/leader-election/img/NRP_FD_PrimaryFails.png diff --git a/examples/C/README.md b/examples/C/README.md index e9fd4742..8955870f 100644 --- a/examples/C/README.md +++ b/examples/C/README.md @@ -10,4 +10,5 @@ * [Rhythm](src/rhythm/README.md): Sound generation and terminal user interface demos. * [SDV](src/sdv/README.md): Software defined vehicle sketch integrating user input, a web display, and sound. * [Train Door](src/train-door/README.md): Train door controller from a verification paper. -* [Distributed](src/distributed/README.md): Basic federated hello-world examples. \ No newline at end of file +* [Distributed](src/distributed/README.md): Basic federated hello-world examples. +* [Leader Election](src/leader-election/README.md): Federated fault-tolerant system with leader election. \ No newline at end of file diff --git a/examples/C/src/leader-election/README.md b/examples/C/src/leader-election/README.md new file mode 100644 index 00000000..8a8e3c69 --- /dev/null +++ b/examples/C/src/leader-election/README.md @@ -0,0 +1,29 @@ +# Leader Election + +These federated programs implements a redundant fault-tolerant system where a primary node, if and when it fails, is replaced by a backup node. The protocol is described in this paper: + +> Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Consistency Before Availability: Network Reference Point based Failure Detection for Controller Redundancy," Emerging Technologies and Factory Automation (ETFA), 12-15 September 2023, [DOI:10.1109/ETFA54631.2023.10275664](https://doi.org/10.1109/ETFA54631.2023.10275664) + + +The key idea in this protocol is that when a backup fails to detect the heartbeats of a primary node, it becomes primary only if it has access to Network Reference Point (NRP), which is a point in the network. This way, if the network becomes partitioned, only a backup that is on the side of the partition that still has access to the NRP can become a primary. If a primary loses access to the NRP, then it relinquishes its primary role because it is now on the wrong side of a network partition. A backup on the right side of the partition will take over. The "FD" in the names of the programs stands for "fault detection." + +## Prerequisite + +To run these programs, you are required to first [install the RTI](https://www.lf-lang.org/docs/handbook/distributed-execution?target=c#installation-of-the-rti) (the Run-Time Infrastructure), which handles the coordination. + +## Examples + + + + + + + + + + + + + + +
NRP_FD NRP_FD.lf : This version has switch1 failing at 3s, node1 failing at 10s, and node2 failing at 15s.
NRP_FD_PrimaryFails NRP_FD_PrimaryFails.lf : This version has the primary (node1) failing after 5 seconds and the backup (node2) failing at at 15s. The backup detects simultaneous loss of the heartbeat on both networks and hence assumes that the primary has failed rather than there being a network failure. Switch 1 remains the NRP.
NRP_FD_Partitioning NRP_FD_Partitioning.lf : This version partitions the network and shows that the protocol prevents the backup from becoming primary, thereby preventing two primaries.
diff --git a/examples/C/src/leader-election/img/NRP_FD.png b/examples/C/src/leader-election/img/NRP_FD.png new file mode 100644 index 0000000000000000000000000000000000000000..026cb60919b53b37d301ed1fb12984dcd3e794bb GIT binary patch literal 26824 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 Date: Wed, 24 Jan 2024 08:46:48 -0800 Subject: [PATCH 26/27] Timeout pending --- examples/C/src/leader-election/NRP_FD.lf | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/C/src/leader-election/NRP_FD.lf b/examples/C/src/leader-election/NRP_FD.lf index ea172f35..9fa28e55 100644 --- a/examples/C/src/leader-election/NRP_FD.lf +++ b/examples/C/src/leader-election/NRP_FD.lf @@ -346,6 +346,7 @@ reactor Node( =} reaction(ping_timed_out) -> out, new_NRP_request_timed_out, reset(Failed) {= + self->ping_timeout_pending = false; if (self->ping_pending) { // Ping timed out. lf_print(PRINTF_TIME ": Backup node %d gets no response from ping.", lf_time_logical_elapsed(), self->id); From 39b70cbb5c1ba2fdf1a5bfa3adbcb2fd53725e8a Mon Sep 17 00:00:00 2001 From: "Edward A. Lee" Date: Tue, 30 Jan 2024 09:47:43 -0800 Subject: [PATCH 27/27] Update to remove FIXMEs --- .../C/src/leader-election/HeartbeatBully.lf | 18 +----------------- examples/C/src/leader-election/README.md | 15 +++++++++++---- .../leader-election/img/HeartbeatBully.png | Bin 0 -> 418197 bytes 3 files changed, 12 insertions(+), 21 deletions(-) create mode 100644 examples/C/src/leader-election/img/HeartbeatBully.png diff --git a/examples/C/src/leader-election/HeartbeatBully.lf b/examples/C/src/leader-election/HeartbeatBully.lf index 31362d82..4e692d29 100644 --- a/examples/C/src/leader-election/HeartbeatBully.lf +++ b/examples/C/src/leader-election/HeartbeatBully.lf @@ -14,19 +14,7 @@ * is set so that each primary fails after sending three heartbeat messages. When all nodes have * failed, then the program exits. * - * This example is designed to be run as a federated program with decentralized coordination. - * However, as of this writing, bugs in the federated code generator cause the program to fail - * because all federates get the same bank_index == 0. This may be related to these bugs: - * - * - https://github.com/lf-lang/lingua-franca/issues/1961 - * - https://github.com/lf-lang/lingua-franca/issues/1962 - * - * When these bugs are fixed, then the federated version should operate exactly the same as the - * unfederated version except that it will become possible to kill the federates instead of having - * them fail on their own. The program should also be extended to include STP violation handlers to - * deal with the fundamental CAL theorem limitations, where unexpected network delays make it - * impossible to execute the program as designed. For example, if the network becomes partitioned, - * then it becomes possible to have two primary nodes simultaneously active. + * This example is designed to be run as a federated program. * * @author Edward A. Lee * @author Marjan Sirjani @@ -101,10 +89,6 @@ reactor Node( } } } - // FIXME - // =} STP (0) {= - // FIXME: What should we do here. - // lf_print_error("Node %d had an STP violation. Ignoring heartbeat as if it didn't arrive at all.", self->bank_index); =} reaction(t) -> reset(Prospect) {= diff --git a/examples/C/src/leader-election/README.md b/examples/C/src/leader-election/README.md index 8a8e3c69..d2cce2c9 100644 --- a/examples/C/src/leader-election/README.md +++ b/examples/C/src/leader-election/README.md @@ -1,11 +1,14 @@ # Leader Election -These federated programs implements a redundant fault-tolerant system where a primary node, if and when it fails, is replaced by a backup node. The protocol is described in this paper: +These federated programs implement redundant fault-tolerant systems where a primary node, if and when it fails, is replaced by a backup node. The HeartbeatBully example is described in this paper: -> Bjarne Johansson; Mats Rågberger; Alessandro V. Papadopoulos; Thomas Nolte, "Consistency Before Availability: Network Reference Point based Failure Detection for Controller Redundancy," Emerging Technologies and Factory Automation (ETFA), 12-15 September 2023, [DOI:10.1109/ETFA54631.2023.10275664](https://doi.org/10.1109/ETFA54631.2023.10275664) +> B. Johansson, M. Rågberger, A. V. Papadopoulos and T. Nolte, "Heartbeat Bully: Failure Detection and Redundancy Role Selection for Network-Centric Controller," IECON 2020 The 46th Annual Conference of the IEEE Industrial Electronics Society, Singapore, 2020, pp. 2126-2133, [DOI: 10.1109/IECON43393.2020.9254494](https://doi.org/10.1109/IECON43393.2020.9254494). +The NRP examples extend the algorithm to reduce the likelihood of getting multiple primaries when the network becomes partitioned. The NRP protocol is described in this paper: -The key idea in this protocol is that when a backup fails to detect the heartbeats of a primary node, it becomes primary only if it has access to Network Reference Point (NRP), which is a point in the network. This way, if the network becomes partitioned, only a backup that is on the side of the partition that still has access to the NRP can become a primary. If a primary loses access to the NRP, then it relinquishes its primary role because it is now on the wrong side of a network partition. A backup on the right side of the partition will take over. The "FD" in the names of the programs stands for "fault detection." +> B. Johansson, M. Rågberger, A. V. Papadopoulos, and T. Nolte, "Consistency Before Availability: Network Reference Point based Failure Detection for Controller Redundancy," Emerging Technologies and Factory Automation (ETFA), 12-15 September 2023, [DOI:10.1109/ETFA54631.2023.10275664](https://doi.org/10.1109/ETFA54631.2023.10275664) + +The key idea in the NRP protocol is that when a backup fails to detect the heartbeats of a primary node, it becomes primary only if it has access to Network Reference Point (NRP), which is a point in the network. This way, if the network becomes partitioned, only a backup that is on the side of the partition that still has access to the NRP can become a primary. If a primary loses access to the NRP, then it relinquishes its primary role because it is now on the wrong side of a network partition. A backup on the right side of the partition will take over. The "FD" in the names of the programs stands for "fault detection." ## Prerequisite @@ -15,8 +18,12 @@ To run these programs, you are required to first [install the RTI](https://www.l + + + + - + diff --git a/examples/C/src/leader-election/img/HeartbeatBully.png b/examples/C/src/leader-election/img/HeartbeatBully.png new file mode 100644 index 0000000000000000000000000000000000000000..54ed8551b5a4f9e44494a737fc34824d2a3ae644 GIT binary patch literal 418197 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

OVvB!fl2Eabi?i1Qor!}M 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

Heartbeat Bully HeartbeatBully.lf : Basic leader electrion protocol called "heartbeat bully".
NRP_FD NRP_FD.lf : This version has switch1 failing at 3s, node1 failing at 10s, and node2 failing at 15s. NRP_FD.lf : Extension using a network reference point (NRP) to help prevent multiple primaries. This version has switch1 failing at 3s, node1 failing at 10s, and node2 failing at 15s.
NRP_FD_PrimaryFails