diff --git a/core/src/main/java/org/lflang/AttributeUtils.java b/core/src/main/java/org/lflang/AttributeUtils.java index 095226d51d..9b94267fe0 100644 --- a/core/src/main/java/org/lflang/AttributeUtils.java +++ b/core/src/main/java/org/lflang/AttributeUtils.java @@ -187,6 +187,11 @@ public static boolean isSparse(EObject node) { return findAttributeByName(node, "sparse") != null; } + /** Return true if the node has an {@code @transient} attribute. */ + public static boolean isTransient(Instantiation node) { + return findAttributeByName(node, "transient") != null; + } + /** * Return true if the reactor is marked to be a federate. * diff --git a/core/src/main/java/org/lflang/federated/extensions/CExtension.java b/core/src/main/java/org/lflang/federated/extensions/CExtension.java index 124f540b8b..257ba1b323 100644 --- a/core/src/main/java/org/lflang/federated/extensions/CExtension.java +++ b/core/src/main/java/org/lflang/federated/extensions/CExtension.java @@ -467,7 +467,7 @@ public String generatePortAbsentReactionBody( + receivingPortID + ", " + connection.getDstFederate().id - + ", (long long) lf_time_logical_elapsed());", + + ", lf_time_logical_elapsed());", "if (" + sendRef + " == NULL || !" + sendRef + "->is_present) {", "LF_PRINT_LOG(\"The output port is NULL or it is not present.\");", " lf_send_port_absent_to_federate(" @@ -556,15 +556,33 @@ protected String makePreamble( // that handles incoming network messages destined to the specified // port. This will only be used if there are federates. int numOfNetworkActions = federate.networkMessageActions.size(); + int numZDCNetworkActions = federate.zeroDelayCycleNetworkMessageActions.size(); code.pr( """ interval_t _lf_action_delay_table[%1$s]; lf_action_base_t* _lf_action_table[%1$s]; size_t _lf_action_table_size = %1$s; - lf_action_base_t* _lf_zero_delay_cycle_action_table[%2$s]; - size_t _lf_zero_delay_cycle_action_table_size = %2$s; """ - .formatted(numOfNetworkActions, federate.zeroDelayCycleNetworkMessageActions.size())); + .formatted(numOfNetworkActions)); + if (numZDCNetworkActions > 0) { + code.pr( + """ + lf_action_base_t* _lf_zero_delay_cycle_action_table[%1$s]; + size_t _lf_zero_delay_cycle_action_table_size = %1$s; + uint16_t _lf_zero_delay_cycle_upstream_ids[%1$s]; + bool _lf_zero_delay_cycle_upstream_disconnected[%1$s] = { false }; + """ + .formatted(numZDCNetworkActions)); + } else { + // Make sure these symbols are defined, even though only size will be used. + code.pr( + """ + lf_action_base_t** _lf_zero_delay_cycle_action_table = NULL; + size_t _lf_zero_delay_cycle_action_table_size = 0; + uint16_t* _lf_zero_delay_cycle_upstream_ids = NULL; + bool* _lf_zero_delay_cycle_upstream_disconnected = NULL; + """); + } int numOfNetworkReactions = federate.networkReceiverReactions.size(); code.pr( @@ -732,6 +750,8 @@ else if (globalSTP instanceof CodeExprImpl) } // Set global variable identifying the federate. code.pr("_lf_my_fed_id = " + federate.id + ";"); + // Set indicator variable that specifies whether the federate is transient or not. + code.pr("_fed.is_transient = " + federate.isTransient + ";"); // We keep separate record for incoming and outgoing p2p connections to allow incoming traffic // to be processed in a separate diff --git a/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java b/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java index 87f2d59c90..977059ae6d 100644 --- a/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java +++ b/core/src/main/java/org/lflang/federated/extensions/CExtensionUtils.java @@ -60,7 +60,6 @@ public static String initializeTriggersForNetworkActions( CodeBuilder code = new CodeBuilder(); if (!federate.networkMessageActions.isEmpty()) { var actionTableCount = 0; - var zeroDelayActionTableCount = 0; for (int i = 0; i < federate.networkMessageActions.size(); ++i) { // Find the corresponding ActionInstance. Action action = federate.networkMessageActions.get(i); @@ -83,10 +82,17 @@ public static String initializeTriggersForNetworkActions( // Set the ID of the source federate. code.pr( trigger + ".source_id = " + federate.networkMessageSourceFederate.get(i).id + "; \\"); - if (federate.zeroDelayCycleNetworkMessageActions.contains(action)) { + int j = federate.zeroDelayCycleNetworkMessageActions.indexOf(action); + if (j >= 0) { + var upstream = federate.zeroDelayCycleNetworkUpstreamFeds.get(j); + code.pr("_lf_zero_delay_cycle_upstream_ids[" + j + "] = " + upstream.id + "; \\"); + if (upstream.isTransient) { + // Transient federates are assumed to be initially disconnected. + code.pr("_lf_zero_delay_cycle_upstream_disconnected[" + j + "] = true; \\"); + } code.pr( "_lf_zero_delay_cycle_action_table[" - + zeroDelayActionTableCount++ + + j + "] = (lf_action_base_t*)&" + trigger + "; \\"); @@ -152,7 +158,8 @@ public static String stpStructs(FederateInstance federate) { */ public static String createPortStatusFieldForInput(Input input) { StringBuilder builder = new StringBuilder(); - // If it is not a multiport, then we could re-use the port trigger, and nothing needs to be done + // If it is not a multiport, then we could re-use the port trigger, and nothing + // needs to be done if (ASTUtils.isMultiport(input)) { // If it is a multiport, then create an auxiliary list of port // triggers for each channel of @@ -241,12 +248,13 @@ static boolean clockSyncIsOn(FederateInstance federate, RtiConfig rtiConfig) { * *

Clock synchronization can be enabled using the clock-sync target property. * - * @see Documentation + * @see Documentation */ public static void initializeClockSynchronization( FederateInstance federate, RtiConfig rtiConfig, MessageReporter messageReporter) { - // Check if clock synchronization should be enabled for this federate in the first place + // Check if clock synchronization should be enabled for this federate in the + // first place if (clockSyncIsOn(federate, rtiConfig)) { messageReporter .nowhere() @@ -272,8 +280,8 @@ public static void initializeClockSynchronization( * *

Clock synchronization can be enabled using the clock-sync target property. * - * @see Documentation + * @see Documentation */ public static void addClockSyncCompileDefinitions(FederateInstance federate) { @@ -316,8 +324,15 @@ public static void generateCMakeInclude( "add_compile_definitions(LF_SOURCE_DIRECTORY=\"" + fileConfig.srcPath + "\")"); cmakeIncludeCode.pr( "add_compile_definitions(LF_PACKAGE_DIRECTORY=\"" + fileConfig.srcPkgPath + "\")"); + // After federates have been divided, their root package directory is different. cmakeIncludeCode.pr( - "add_compile_definitions(LF_SOURCE_GEN_DIRECTORY=\"" + fileConfig.getSrcGenPath() + "\")"); + "add_compile_definitions(LF_FED_PACKAGE_DIRECTORY=\"" + + fileConfig.srcPkgPath + + File.separator + + "fed-gen" + + File.separator + + fileConfig.name + + "\")"); cmakeIncludeCode.pr("add_compile_definitions(LF_FILE_SEPARATOR=\"" + File.separator + "\")"); try (var srcWriter = Files.newBufferedWriter(cmakeIncludePath)) { srcWriter.write(cmakeIncludeCode.getCode()); diff --git a/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java b/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java index 4eafe71ea0..6fdd6b20dd 100644 --- a/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java +++ b/core/src/main/java/org/lflang/federated/generator/FedASTUtils.java @@ -314,9 +314,10 @@ private static void addNetworkReceiverReactor( connection.dstFederate.networkMessageSourceFederate.add(connection.srcFederate); connection.dstFederate.networkMessageActionDelays.add(connection.getDefinition().getDelay()); if (connection.srcFederate.isInZeroDelayCycle() - && connection.getDefinition().getDelay() == null) + && connection.getDefinition().getDelay() == null) { connection.dstFederate.zeroDelayCycleNetworkMessageActions.add(networkAction); - + connection.dstFederate.zeroDelayCycleNetworkUpstreamFeds.add(connection.srcFederate); + } // Get the largest STAA for any reaction triggered by the destination port. TimeValue maxSTAA = findMaxSTAA(connection, coordination); diff --git a/core/src/main/java/org/lflang/federated/generator/FederateInstance.java b/core/src/main/java/org/lflang/federated/generator/FederateInstance.java index 27bd1ef974..8980df35a7 100644 --- a/core/src/main/java/org/lflang/federated/generator/FederateInstance.java +++ b/core/src/main/java/org/lflang/federated/generator/FederateInstance.java @@ -13,6 +13,7 @@ import java.util.TreeSet; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.lflang.AttributeUtils; import org.lflang.MessageReporter; import org.lflang.TimeValue; import org.lflang.ast.ASTUtils; @@ -73,6 +74,7 @@ public FederateInstance( this.bankWidth = bankWidth; this.messageReporter = messageReporter; this.targetConfig = targetConfig; + this.isTransient = AttributeUtils.isTransient(instantiation); // If the instantiation is in a bank, then we have to append // the bank index to the name. @@ -134,6 +136,9 @@ public Instantiation getInstantiation() { /** The integer ID of this federate. */ public int id; + /** Type of the federate: transient if true, and peristent if false . */ + public boolean isTransient = false; + /** * The name of this federate instance. This will be the instantiation name, possibly appended with * "__n", where n is the bank position of this instance if the instantiation is of a bank of @@ -165,6 +170,12 @@ public Instantiation getInstantiation() { */ public List zeroDelayCycleNetworkMessageActions = new ArrayList<>(); + /** + * List of upstream federates corresponding to actions in the zeroDelayCycleNetworkMessageActions + * list. + */ + public List zeroDelayCycleNetworkUpstreamFeds = new ArrayList<>(); + /** * A set of federates with which this federate has an inbound connection There will only be one * physical connection even if federate A has defined multiple physical connections to federate B. diff --git a/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java b/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java index f4d6dbc132..bb7c06df1e 100644 --- a/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java +++ b/core/src/main/java/org/lflang/federated/launcher/FedLauncherGenerator.java @@ -304,6 +304,11 @@ private String getSetupCode() { + " tmux: 2)\"", " echo \" -h, --help Show this help message\"", " echo \"\"", + " echo \"tmux: This launcher enables mouse mode (pane focus, resize, scroll).\"", + " echo \" To copy text to the clipboard, hold Shift or Function while dragging" + + " to select\"", + " echo \" (most terminals), or press Ctrl+b then [ to enter copy mode.\"", + " echo \"\"", " echo \"All other arguments are forwarded to each federate.\"", " echo \"For available federate parameters, run a federate binary with --help.\"", " exit 0", @@ -370,12 +375,20 @@ private String getRtiCommand( if (targetConfig.getOrDefault(TracingProperty.INSTANCE).isEnabled()) { commands.add(" -t \\"); } + // Identify the number of transient federates. + int transientFederatesNumber = 0; + for (FederateInstance federate : federates) { + if (federate.isTransient) { + transientFederatesNumber++; + } + } if (!targetConfig.getOrDefault(DNETProperty.INSTANCE)) { commands.add(" -d \\"); } commands.addAll( List.of( " -n " + federates.size() + " \\", + " -nt " + transientFederatesNumber + " \\", " -c " + targetConfig.getOrDefault(ClockSyncModeProperty.INSTANCE).toString() + " \\")); @@ -790,7 +803,7 @@ private String getTmuxLaunchCode(List federates, RtiConfig rti // Assign pane titles. The RTI title doubles as an instruction banner. lines.add( " tmux select-pane -t \"$RTI_PANE\" -T" - + " \"RTI — Ctrl+C to stop | Ctrl+B d to detach and kill\""); + + " \"RTI — Ctrl+C stop | C-b d detach | Shift or Fn+drag: copy\""); for (int i = 0; i < numFeds; i++) { lines.add( " tmux select-pane -t \"$FED_PANE_" + i + "\" -T \"" + federates.get(i).name + "\""); @@ -827,6 +840,14 @@ private String getTmuxLaunchCode(List federates, RtiConfig rti } lines.add(""); + // Mouse mode is enabled above so tmux handles the mouse; Shift+drag (or copy mode) is needed + // for system clipboard selection in most terminals. + lines.add(" echo \"\""); + lines.add( + " echo \"tmux: Hold Shift or Function while dragging to select text for the clipboard," + + " or Ctrl+b [ for copy mode.\""); + lines.add(""); + // Focus the RTI pane and attach to the session. lines.add(" tmux select-pane -t \"$RTI_PANE\""); lines.add(" tmux attach-session -t \"$SESSION_NAME\""); diff --git a/core/src/main/java/org/lflang/generator/c/CCompiler.java b/core/src/main/java/org/lflang/generator/c/CCompiler.java index 60ec7f1ad7..225990a2bd 100644 --- a/core/src/main/java/org/lflang/generator/c/CCompiler.java +++ b/core/src/main/java/org/lflang/generator/c/CCompiler.java @@ -87,9 +87,9 @@ public boolean runCCompiler(GeneratorBase generator, LFGeneratorContext context) // avoid any error residue that can occur in CMake from // a previous build. // FIXME: This is slow and only needed if an error - // has previously occurred. Deleting the build directory - // if no prior errors have occurred can prolong the compilation - // substantially. See #1416 for discussion. + // has previously occurred. Deleting the build directory + // if no prior errors have occurred can prolong the compilation + // substantially. See #1416 for discussion. FileUtil.deleteDirectory(buildPath); // Make sure the build directory exists Files.createDirectories(buildPath); @@ -218,14 +218,15 @@ private static List cmakeOptions(TargetConfig targetConfig, FileConfig f + FileUtil.toUnixString(fileConfig.getOutPath().relativize(fileConfig.binPath)), "-DLF_FILE_SEPARATOR='" + quote + separator + quote + "'")); // Add #define for source file directory. - // Do not do this for federated programs because for those, the definition is put - // into the cmake file (and fileConfig.srcPath is the wrong directory anyway). + // Do not do this for federated programs because for those, the definition is + // put into the cmake file (and fileConfig.srcPath is the wrong directory + // anyway). if (!fileConfig.srcPath.toString().contains("fed-gen")) { - // Do not convert to Unix path + // Do not convert to Unix path. Do not add quotes here; CMake defineString() adds them. arguments.add("-DLF_SOURCE_DIRECTORY=" + srcPath); arguments.add("-DLF_PACKAGE_DIRECTORY=" + rootPath); - arguments.add("-DLF_SOURCE_GEN_DIRECTORY=" + srcGenPath); } + arguments.add("-DLF_SOURCE_GEN_DIRECTORY=" + srcGenPath); // Append user-provided CMake configure definitions. These come after built-ins so they can // override defaults (e.g., CMAKE_BUILD_TYPE). diff --git a/core/src/main/java/org/lflang/validation/AttributeSpec.java b/core/src/main/java/org/lflang/validation/AttributeSpec.java index 26cca74cf3..b291644856 100644 --- a/core/src/main/java/org/lflang/validation/AttributeSpec.java +++ b/core/src/main/java/org/lflang/validation/AttributeSpec.java @@ -231,6 +231,8 @@ enum AttrParamType { new AttributeSpec(List.of(new AttrParamSpec(VALUE_ATTR, AttrParamType.TIME, false)))); // @sparse ATTRIBUTE_SPECS_BY_NAME.put("sparse", new AttributeSpec(null)); + // @transient + ATTRIBUTE_SPECS_BY_NAME.put("transient", new AttributeSpec(null)); // @icon("value") ATTRIBUTE_SPECS_BY_NAME.put( "icon", diff --git a/core/src/main/java/org/lflang/validation/LFValidator.java b/core/src/main/java/org/lflang/validation/LFValidator.java index 16986060e2..6769893377 100644 --- a/core/src/main/java/org/lflang/validation/LFValidator.java +++ b/core/src/main/java/org/lflang/validation/LFValidator.java @@ -1353,6 +1353,8 @@ public void checkAttributes(Attribute attr) { checkMaxWaitAttribute(attr); } else if (name.equals("absent_after")) { checkAbsentAfterAttribute(attr); + } else if (name.equals("transient")) { + checkTransientAttribute(attr); } if (GLOBAL_ATTRIBUTE_NAMES.contains(name)) { checkGlobalAttribute(attr); @@ -1417,6 +1419,25 @@ private void checkAbsentAfterAttribute(Attribute attr) { } } + /** @transient marks a federate instantiation as transient (join/leave during execution). */ + private void checkTransientAttribute(Attribute attr) { + EObject container = attr.eContainer(); + if (!(container instanceof Instantiation)) { + error( + "The @transient attribute can only be applied to a federate instantiation.", + attr, + Literals.ATTRIBUTE__ATTR_NAME); + return; + } + EObject parent = container.eContainer(); + if (!(parent instanceof Reactor reactor) || !reactor.isFederated()) { + error( + "The @transient attribute can only be applied inside a federated reactor.", + attr, + Literals.ATTRIBUTE__ATTR_NAME); + } + } + @Check(CheckType.FAST) public void checkWidthSpec(WidthSpec widthSpec) { if (!this.target.supportsMultiports()) { diff --git a/core/src/main/resources/lib/c/reactor-c b/core/src/main/resources/lib/c/reactor-c index 51034b92f1..a144507760 160000 --- a/core/src/main/resources/lib/c/reactor-c +++ b/core/src/main/resources/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit 51034b92f1da8558bd54e9ac8d610de3a8012bfc +Subproject commit a144507760a67634e4b16a34e8cb1192afbcbd5c diff --git a/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java b/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java index 4f1603a532..2fd91c33d4 100644 --- a/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java +++ b/core/src/test/java/org/lflang/tests/compiler/LinguaFrancaValidationTest.java @@ -1080,6 +1080,57 @@ public void testFederationSupport() throws Exception { } } + @Test + public void testTransientAttributeOnFederateInstantiation() throws Exception { + Model model = + parseWithoutError( + """ + target C + reactor Foo {} + federated reactor { + @transient + foo = new Foo() + bar = new Foo() + } + """); + validator.assertNoIssues(model); + } + + @Test + public void testTransientAttributeRejectedOnMainReactor() throws Exception { + Model model = + parseWithoutError( + """ + target C + @transient + main reactor {} + """); + validator.assertError( + model, + LfPackage.eINSTANCE.getAttribute(), + null, + "The @transient attribute can only be applied to a federate instantiation."); + } + + @Test + public void testTransientAttributeRejectedOnInput() throws Exception { + Model model = + parseWithoutError( + """ + target C + reactor R { + @transient + input x: int + } + main reactor {} + """); + validator.assertError( + model, + LfPackage.eINSTANCE.getAttribute(), + null, + "The @transient attribute can only be applied to a federate instantiation."); + } + /** Tests for state and parameter declarations, including native lists. */ @Test public void stateAndParameterDeclarationsInC() throws Exception { diff --git a/test/C/Makefile b/test/C/Makefile new file mode 100644 index 0000000000..9f48f8bc9b --- /dev/null +++ b/test/C/Makefile @@ -0,0 +1,50 @@ +# This file lets you format the code-base with a single command. +FILES := $(shell find . -name '*.lf' -not -path "*/fed-gen/*") + +.PHONY: help +help: + @echo "Available commands:" + @echo " make clean - Clean up build and documentation" + @echo " make format - Format all Lingua Franca files using lff" + @echo " make format-dev - Format all Lingua Franca files using lff-dev" + @echo " make build-all - Build all Lingua Franca files using lfc" + @echo " make build-all-dev - Build all Lingua Franca files using lfc-dev" + +.PHONY: format +format: + @for file in $(FILES); do \ + echo "========== Formatting $$file =========="; \ + lff $$file; \ + done + +.PHONY: format-dev +format-dev: + @for file in $(FILES); do \ + echo "========== Formatting $$file =========="; \ + lff-dev $$file; \ + done + +.PHONY: build-all +build-all: + @for file in $(FILES); do \ + echo "================================================"; \ + echo "Building $$file"; \ + echo "================================================"; \ + lfc $$file; \ + done + +.PHONY: build-all-dev +build-all-dev: + @for file in $(FILES); do \ + echo "================================================"; \ + echo "Building $$file"; \ + echo "================================================"; \ + lfc-dev $$file; \ + done + +.PHONY: clean +clean: + rm -rf *.lft *.csv *.log src-gen fed-gen include bin + +# Set help as the default target +.DEFAULT_GOAL := help diff --git a/test/C/src/federated/transient/TransientDownstreamWithTimer.lf b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf new file mode 100644 index 0000000000..9aeed757ab --- /dev/null +++ b/test/C/src/federated/transient/TransientDownstreamWithTimer.lf @@ -0,0 +1,158 @@ +/** + * This LF program tests if a transient federate correctly leaves then joins the federation. It also + * tests whether the transient's downstream executes as expected, that is it receives correct TAGs, + * regardless of the transient being absent or present. In this test: + * - the transient federate spontaneously leaves the federation after 2 reactions to input port + * `in`, + * - the downstream of the transient federate has only one transient as upstream. + */ +target C { + timeout: 3 s +} + +preamble {= + #include + #include +=} + +/** Persistent federate that is responsible for lauching the transient federate */ +reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") { + timer t(launch_time, 0) + + reaction(t) {= + // Construct the command to launch the transient federate + char mid_launch_cmd[512]; + sprintf(mid_launch_cmd, + "%s/bin/federate__%s -i %s", + LF_FED_PACKAGE_DIRECTORY, + self->fed_instance_name, + lf_get_federation_id() + ); + + lf_print("Launching federate federate__%s at physical time " PRINTF_TIME ".", + self->fed_instance_name, lf_time_physical()); + lf_print("**** Launch command: %s", mid_launch_cmd); + + int status = system(mid_launch_cmd); + + // Exit if error + if (status == 0) { + lf_print("Successfully launched federate__%s.", self->fed_instance_name); + } else { + lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name); + } + =} +} + +/** + * Persistent federate, upstream of the transient. It reacts to its timer by sending increments to + * output port out. + */ +reactor Up(period: time = 500 ms) { + output out: int + timer t(0, period) + state count: int = 0 + + reaction(t) -> out {= + lf_set(out, self->count); + self->count++; + =} +} + +/** + * Transient federate that forwards whatever it receives from `Up` to `Down`. It reacts twice to + * input port `in`, then stops. It will execute twice during the lifetime of the federation. The + * second launch is done by `TransientExec` at logical time 1 s. Each time `Middle` joins, it + * notifies `Down`. + */ +reactor Middle { + input in: int + output out: int + output join: int + state count: int = 0 + + // Middle notifies its downstream that he joined, but make sure first that the effective start + // tag is correct + reaction(startup) -> join {= + tag_t t = lf_tag_start_effective(); + if(t.time < lf_time_start()) { + lf_print_error_and_exit("Fatal error: the transient's effective start time is less than the federation start time"); + } + + lf_set(join, 0); + =} + + // Pass the input value to the output port and stop spontaneously after two reactions to in + reaction(in) -> out {= + self->count++; + lf_set(out, in->value); + + if (self->count == 2) { + lf_stop(); + } + =} +} + +/** + * Persistent federate, which is downstream of the transient. It has to keep reacting to its + * internal timer and also to inputs from the tansient, if any. + */ +reactor Down { + timer t(0, 500 ms) + + input in: int + input join: int + + state count_timer: int = 0 + state count_join: int = 0 + state count_in_mid_reactions: int = 0 + + reaction(t) {= + self->count_timer++; + =} + + reaction(join) {= + self->count_join++; + =} + + reaction(in) {= + self->count_in_mid_reactions++; + =} + + reaction(shutdown) {= + // Check that the TAG has been successfully issued to Down + if (self->count_timer < 5) { + lf_print_error_and_exit("Down federate's timer reacted %d times, while it had to react more than %d times.", + self->count_timer, 5); + } + + // Check that `Middle` have joined 2 times + if (self->count_join != 2) { + lf_print_error_and_exit("Transient federate did not join twice, but %d times!", self->count_join); + } + + // Check that `Middle` have reacted correctly + if (self->count_in_mid_reactions < 4) { + lf_print_error_and_exit("Transient federate Mid did not execute and pass values from up corretly! Expected >= 4, but had: %d.", + self->count_in_mid_reactions); + } + =} +} + +federated reactor { + // Persistent federate that is responsible for lauching the transient once, after 1s + midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid") + + // Persistent downstream and upstream federates of the transient + up = new Up() + down = new Down() + + // Transient federate + @transient + mid = new Middle() + + // Connections + up.out -> mid.in + mid.join -> down.join + mid.out -> down.in +} diff --git a/test/C/src/federated/transient/TransientDownstreamWithTwoUpstream.lf b/test/C/src/federated/transient/TransientDownstreamWithTwoUpstream.lf new file mode 100644 index 0000000000..e0692f4c22 --- /dev/null +++ b/test/C/src/federated/transient/TransientDownstreamWithTwoUpstream.lf @@ -0,0 +1,128 @@ +/** + * This LF program tests whether a transient federate corretly leaves then joins the federation. It + * also tests whether the transient's downstream executes as expected, that is it received correct + * TAGs, regardless of the transient being absent or present. In this test: + * - the transient federate spontaneously leaves the federation after 2 reactions to input port in, + * - the downstream of the transient federate has one persistent and one transient upstreams. + * + * In addition, the program tests if authentication works in case of a federation with transients, + * by adding `auth` target property. + */ +target C { + timeout: 3 s, + auth: true +} + +import Up from "TransientDownstreamWithTimer.lf" +import Middle from "TransientDownstreamWithTimer.lf" + +preamble {= + #include + #include +=} + +/** Persistent federate that is responsible for lauching the transient federate */ +reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") { + timer t(launch_time, 0) + + reaction(t) {= + // Construct the command to launch the transient federate + char mid_launch_cmd[512]; + sprintf(mid_launch_cmd, + "%s/bin/federate__%s -i %s", + LF_FED_PACKAGE_DIRECTORY, + self->fed_instance_name, + lf_get_federation_id() + ); + + lf_print("Launching federate federate__%s at physical time " PRINTF_TIME ".", + self->fed_instance_name, lf_time_physical()); + + int status = system(mid_launch_cmd); + + // Exit if error + if (status == 0) { + lf_print("Successfully launched federate__%s.", self->fed_instance_name); + } else { + lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name); + } + =} +} + +/** + * Persistent federate, which is downstream of the transient. It has to keep reacting to its + * internal timer and also to inputs from the tansient, if any. + */ +reactor Down { + timer t(0, 500 ms) + + input in_mid: int + input in_up: int + input join: int + + state count_timer: int = 0 + state count_join: int = 0 + state count_in_mid_reactions: int = 0 + state count_in_up_reactions: int = 0 + + reaction(t) {= + self->count_timer++; + =} + + reaction(join) {= + self->count_join++; + =} + + reaction(in_mid) {= + self->count_in_mid_reactions++; + =} + + reaction(in_up) {= + self->count_in_up_reactions++; + =} + + reaction(shutdown) {= + // Check that the TAG have been successfully issued to Down + if (self->count_timer < 5) { + lf_print_error_and_exit("Federate's timer reacted %d times, while it had to react more than %d times.", + self->count_timer, + 5); + } + if (self->count_in_up_reactions < 7) { + lf_print_error_and_exit("Federate's timer reacted %d times, while it had to react more than %d times.", + self->count_in_up_reactions, + 7); + } + + // Check that Middle have joined 2 times + if (self->count_join != 2) { + lf_print_error_and_exit("Transient federate did not join twice, but %d times!", self->count_join); + } + + // Check that Middle have reacted correctly + if (self->count_in_mid_reactions < 4) { + lf_print_error_and_exit("Transient federate Mid did not execute and pass values from up corretly! Expected >= 4, but had: %d.", + self->count_in_mid_reactions); + } + =} +} + +federated reactor { + // Persistent federate that is responsible for lauching the transient once, after 1s + midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid") + + // Persistent downstream and upstream federates of the transient + up1 = new Up() + up2 = new Up(period = 300 msec) + down = new Down() + + // Transient federate + @transient + mid = new Middle() + + // Connections + up1.out -> mid.in + mid.join -> down.join + mid.out -> down.in_mid + up2.out -> down.in_up +} diff --git a/test/C/src/federated/transient/TransientHotSwap.lf b/test/C/src/federated/transient/TransientHotSwap.lf new file mode 100644 index 0000000000..683b6debd5 --- /dev/null +++ b/test/C/src/federated/transient/TransientHotSwap.lf @@ -0,0 +1,97 @@ +/** + * This LF program is a variant of TransientDownstreamWithTimer that tests the Hot Swap mechanism. + * For this, it tests whether the transient's downstream executes as expected and if `mid` is + * stopped and the second instance joins as expected. In this test: + * - the transient federate DOES NOT spontaneously leave the federation. + * - the downstream of the transient federate has only one transient as upstream. + * - A persistent federate `TransientExec` launches `mid` after 1s to activate the hot mechanism + * swap. + */ +target C { + timeout: 3 s, + auth: true +} + +import Up from "TransientDownstreamWithTimer.lf" +import Down from "TransientDownstreamWithTimer.lf" + +preamble {= + #include + #include +=} + +/** Persistent federate that is responsible for lauching the transient federate */ +reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") { + timer t(launch_time, 0) + + reaction(t) {= + // Construct the command to launch the transient federate + char mid_launch_cmd[512]; + sprintf(mid_launch_cmd, + "%s/bin/federate__%s -i %s", + LF_FED_PACKAGE_DIRECTORY, + self->fed_instance_name, + lf_get_federation_id() + ); + + lf_print("Launching federate federate__%s at physical time " PRINTF_TIME ".", + self->fed_instance_name, lf_time_physical()); + + int status = system(mid_launch_cmd); + + // Exit if error + if (status == 0) { + lf_print("Successfully launched federate__%s.", self->fed_instance_name); + } else { + lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name); + } + =} +} + +/** + * Transient federate that forwards whatever it receives from `Up` to `Down`. It reacts twice to + * input port `in`, then stops. It will execute twice during the lifetime of the federation. The + * second launch is done by `TransientExec` at logical time 1 s. Each time `Middle` joins, it + * notifies `Down`. + */ +reactor Middle { + input in: int + output out: int + output join: int + state count: int = 0 + + // Middle notifies its downstream that he joined, but make sure first that the effective start + // tag is correct + reaction(startup) -> join {= + tag_t t = lf_tag_start_effective(); + if(t.time < lf_time_start()) { + lf_print_error_and_exit("Fatal error: the transient's effective start time is less than the federation start time"); + } + + lf_set(join, 0); + =} + + // Pass the input value to the output port + reaction(in) -> out {= + self->count++; + lf_set(out, in->value); + =} +} + +federated reactor { + // Persistent federate that is responsible for lauching the transient once, after 1s + midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid") + + // Persistent downstream and upstream federates of the transient + up = new Up() + down = new Down() + + // Transient federate + @transient + mid = new Middle() + + // Connections + up.out -> mid.in + mid.join -> down.join + mid.out -> down.in +} diff --git a/test/C/src/federated/transient/TransientStatePersistence.lf b/test/C/src/federated/transient/TransientStatePersistence.lf new file mode 100644 index 0000000000..bb5c0eff68 --- /dev/null +++ b/test/C/src/federated/transient/TransientStatePersistence.lf @@ -0,0 +1,194 @@ +/** + * This LF program showcases and tests the persistance of the internal state of a transient federate + * across executions. Using the hot swap mechanism, the transient federate `Middle` leaves and then + * joins. Whenever the state (of type `federate_state_t`) changes, it notifies `Persistence`. + * `Middle` notifies `Persistence` also when it joins. When `Middle` joins the second time or after, + * it receives the saved state and sets it. In this, the order of the reactions is important. + */ +target C { + timeout: 2900 ms +} + +preamble {= + #include + #include + // The internal federate state to be persistent across executions + typedef struct federate_state_t { + char state_char; + int state_count; + } federate_state_t; +=} + +/** Persistent federate that is responsible for lauching the transient federate */ +reactor TransientExec(launch_time: time = 0, fed_instance_name: char* = "instance") { + timer t(launch_time, 0) + + reaction(t) {= + // Construct the command to launch the transient federate + char mid_launch_cmd[512]; + sprintf(mid_launch_cmd, + "%s/bin/federate__%s -i %s", + LF_FED_PACKAGE_DIRECTORY, + self->fed_instance_name, + lf_get_federation_id() + ); + + lf_print("Launching federate %s at physical time " PRINTF_TIME ".", + self->fed_instance_name, lf_time_physical()); + + int status = system(mid_launch_cmd); + + // Exit if error + if (status == 0) { + lf_print("Successfully launched federate__%s.", self->fed_instance_name); + } else { + lf_print_error_and_exit("Unable to launch federate__%s. Abort!", self->fed_instance_name); + } + =} +} + +reactor Persistence { + state middle_state: federate_state_t = {'A', 0} + state middle_first_join: bool = true + + input in_from_middle: federate_state_t + input in_middle_join: bool + output out_to_middle: federate_state_t + + // Only send the previous state if it not the first time Middle joins + reaction(in_middle_join) -> out_to_middle {= + if (!self->middle_first_join) { + lf_set(out_to_middle, self->middle_state); + lf_print("Notifying Mid of the latest state: {%c,%d}", self->middle_state.state_char, + self->middle_state.state_count); + } + self->middle_first_join = false; + =} + + reaction(in_from_middle) {= + self->middle_state.state_char = in_from_middle->value.state_char; + self->middle_state.state_count = in_from_middle->value.state_count; + lf_print("Latest received state: {%c,%d}", self->middle_state.state_char, + self->middle_state.state_count); + =} +} + +/** + * Persistent federate, upstream of the transient. It reacts to its timer by sending increments to + * out output port. + */ +reactor Up(period: time = 500 ms) { + output out: int + timer t(0, period) + state count: int = 0 + + reaction(t) -> out {= + lf_set(out, self->count); + self->count++; + lf_print("Up timer sent %d", self->count); + =} +} + +/** + * Transient federate that forwards whatever it receives from Up to down. It reacts twice to in + * input ports, then stops. It will execute twice during the lifetime of the federation. The second + * launch is done by TransientExec at logical time 1 s. Each time Middle joins, it notifies Down. + */ +reactor Middle { + input in: int + output out: int + output join: bool + state middle_state: federate_state_t = {'A', 0} + + output out_to_persistence: federate_state_t // State Persistence + input in_from_persistence: federate_state_t + + // Middle notifies its downstream that he joined + reaction(startup) -> join {= + lf_set(join, true); + =} + + reaction(in_from_persistence) {= + self->middle_state = in_from_persistence->value; + lf_print("Received the latest state of: {%c,%d} at " PRINTF_TIME ".", + self->middle_state.state_char, + self->middle_state.state_count, + lf_time_logical_elapsed()); + =} + + // When an input is received, the internal state is updated, and then sent to + // Persistance. + reaction(in) -> out, out_to_persistence {= + self->middle_state.state_char++; + self->middle_state.state_count += 2; + lf_set(out, self->middle_state.state_count); + lf_set(out_to_persistence, self->middle_state); + lf_print("Mid state is: {count='%c', count=%d}", + self->middle_state.state_char, + self->middle_state.state_count); + + if (self->middle_state.state_count == 4) { + lf_stop(); + } + =} +} + +/** + * Persistent federate, which is downstream of the transient. It has to keep reacting to its + * internal timer and also to inputs from the tansient, if any. + */ +reactor Down { + timer t(0, 500 ms) + + input in: int + input join: bool + + state count_timer: int = 0 + state count_join: int = 0 + state count_in_mid_reactions: int = 0 + + reaction(t) {= + self->count_timer++; + lf_print("Down timer count %d", self->count_timer); + =} + + reaction(join) {= + self->count_join++; + lf_print("Down count join %d", self->count_join); + =} + + reaction(in) {= + self->count_in_mid_reactions++; + lf_print("Down in %d", self->count_in_mid_reactions); + =} + + reaction(shutdown) {= + if(self->count_join == 2 && self->count_in_mid_reactions < 4) { + lf_print_error_and_exit("Mid Joined twice, but the state did not persist \ + across executions! state_count is %d, while is should be > then %d.", + self->count_in_mid_reactions, + 4); + } + =} +} + +federated reactor { + // Persistent downstream and upstream federates of the transient + up = new Up() + down = new Down() + persistence = new Persistence() + // Persistent federate that is responsible for lauching the transient once, after 1s + midExec = new TransientExec(launch_time = 1 s, fed_instance_name="mid") + + // Transient federate + @transient + mid = new Middle() + + // Connections + up.out -> mid.in + mid.join -> down.join + mid.join -> persistence.in_middle_join + mid.out -> down.in + persistence.out_to_middle -> mid.in_from_persistence + mid.out_to_persistence -> persistence.in_from_middle +}