|
26 | 26 | #include "Framework/WorkflowSpec.h" |
27 | 27 | #include <Monitoring/Monitoring.h> |
28 | 28 | #include <fairmq/TransportFactory.h> |
| 29 | +#include <fairmq/Channel.h> |
| 30 | +#include "Framework/FairMQDeviceProxy.h" |
| 31 | +#include "Framework/ExpirationHandler.h" |
| 32 | +#include "Framework/LifetimeHelpers.h" |
29 | 33 | #include <array> |
30 | 34 | #include <vector> |
31 | 35 | #include <uv.h> |
@@ -808,4 +812,159 @@ TEST_CASE("DataRelayer") |
808 | 812 | } |
809 | 813 | } |
810 | 814 | } |
| 815 | + |
| 816 | + SECTION("ProcessDanglingInputs") |
| 817 | + { |
| 818 | + InputSpec spec{"condition", "TST", "COND"}; |
| 819 | + std::vector<InputRoute> inputs = { |
| 820 | + InputRoute{spec, 0, "from_source_to_self", 0}}; |
| 821 | + |
| 822 | + std::vector<InputChannelInfo> infos{1}; |
| 823 | + TimesliceIndex index{1, infos}; |
| 824 | + ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index)); |
| 825 | + |
| 826 | + // Bind a fake input channel so FairMQDeviceProxy::getInputChannelIndex works |
| 827 | + FairMQDeviceProxy proxy; |
| 828 | + std::vector<fair::mq::Channel> channels{fair::mq::Channel("from_source_to_self")}; |
| 829 | + auto findChannel = [&channels](std::string const& name) -> fair::mq::Channel& { |
| 830 | + for (auto& ch : channels) { |
| 831 | + if (ch.GetName() == name) { |
| 832 | + return ch; |
| 833 | + } |
| 834 | + } |
| 835 | + throw std::runtime_error("Channel not found: " + name); |
| 836 | + }; |
| 837 | + proxy.bind({}, inputs, {}, findChannel, [] { return false; }); |
| 838 | + ref.registerService(ServiceRegistryHelpers::handleForService<FairMQDeviceProxy>(&proxy)); |
| 839 | + |
| 840 | + auto policy = CompletionPolicyHelpers::consumeWhenAny(); |
| 841 | + DataRelayer relayer(policy, inputs, index, {registry}, -1); |
| 842 | + relayer.setPipelineLength(4); |
| 843 | + |
| 844 | + auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq"); |
| 845 | + auto channelAlloc = o2::pmr::getTransportAllocator(transport.get()); |
| 846 | + |
| 847 | + DataHeader dh{"COND", "TST", 0}; |
| 848 | + dh.splitPayloadParts = 1; |
| 849 | + dh.splitPayloadIndex = 0; |
| 850 | + DataProcessingHeader dph{0, 1}; |
| 851 | + |
| 852 | + ExpirationHandler handler; |
| 853 | + handler.name = "test-condition"; |
| 854 | + handler.routeIndex = RouteIndex{0}; |
| 855 | + handler.lifetime = Lifetime::Condition; |
| 856 | + |
| 857 | + // Creator: claim an empty slot and assign timeslice 0 to it |
| 858 | + handler.creator = [](ServiceRegistryRef services, ChannelIndex channelIndex) -> TimesliceSlot { |
| 859 | + auto& index = services.get<TimesliceIndex>(); |
| 860 | + for (size_t si = 0; si < index.size(); si++) { |
| 861 | + TimesliceSlot slot{si}; |
| 862 | + if (!index.isValid(slot)) { |
| 863 | + index.associate(TimesliceId{0}, slot); |
| 864 | + index.setOldestPossibleInput({1}, channelIndex); |
| 865 | + return slot; |
| 866 | + } |
| 867 | + } |
| 868 | + return TimesliceSlot{TimesliceSlot::INVALID}; |
| 869 | + }; |
| 870 | + |
| 871 | + // Checker: always trigger expiration |
| 872 | + handler.checker = LifetimeHelpers::expireAlways(); |
| 873 | + |
| 874 | + // Handler: materialise a dummy header+payload into the PartRef |
| 875 | + handler.handler = [&transport, &channelAlloc, &dh, &dph](ServiceRegistryRef, PartRef& ref, data_matcher::VariableContext&) { |
| 876 | + ref.header = o2::pmr::getMessage(o2::header::Stack{channelAlloc, dh, dph}); |
| 877 | + ref.payload = transport->CreateMessage(4); |
| 878 | + }; |
| 879 | + |
| 880 | + std::vector<ExpirationHandler> handlers{handler}; |
| 881 | + auto activity = relayer.processDanglingInputs(handlers, {registry}, true); |
| 882 | + |
| 883 | + REQUIRE(activity.newSlots == 1); |
| 884 | + REQUIRE(activity.expiredSlots == 1); |
| 885 | + |
| 886 | + // The materialised data should now be ready to consume |
| 887 | + std::vector<RecordAction> ready; |
| 888 | + relayer.getReadyToProcess(ready); |
| 889 | + REQUIRE(ready.size() == 1); |
| 890 | + REQUIRE(ready[0].op == CompletionPolicy::CompletionOp::Consume); |
| 891 | + |
| 892 | + auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot); |
| 893 | + REQUIRE(result.size() == 1); |
| 894 | + REQUIRE((result.at(0).messages | count_parts{}) == 1); |
| 895 | + } |
| 896 | + |
| 897 | + SECTION("ProcessDanglingInputsSkipsWhenDataPresent") |
| 898 | + { |
| 899 | + // processDanglingInputs must not overwrite a slot that already has data. |
| 900 | + // This is guarded by the (part.messages | get_header{0}) != nullptr check. |
| 901 | + InputSpec spec{"condition", "TST", "COND"}; |
| 902 | + std::vector<InputRoute> inputs = { |
| 903 | + InputRoute{spec, 0, "from_source_to_self", 0}}; |
| 904 | + |
| 905 | + std::vector<InputChannelInfo> infos{1}; |
| 906 | + TimesliceIndex index{1, infos}; |
| 907 | + ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index)); |
| 908 | + |
| 909 | + FairMQDeviceProxy proxy; |
| 910 | + std::vector<fair::mq::Channel> channels{fair::mq::Channel("from_source_to_self")}; |
| 911 | + auto findChannel = [&channels](std::string const& name) -> fair::mq::Channel& { |
| 912 | + for (auto& ch : channels) { |
| 913 | + if (ch.GetName() == name) { |
| 914 | + return ch; |
| 915 | + } |
| 916 | + } |
| 917 | + throw std::runtime_error("Channel not found: " + name); |
| 918 | + }; |
| 919 | + proxy.bind({}, inputs, {}, findChannel, [] { return false; }); |
| 920 | + ref.registerService(ServiceRegistryHelpers::handleForService<FairMQDeviceProxy>(&proxy)); |
| 921 | + |
| 922 | + auto policy = CompletionPolicyHelpers::consumeWhenAny(); |
| 923 | + DataRelayer relayer(policy, inputs, index, {registry}, -1); |
| 924 | + relayer.setPipelineLength(4); |
| 925 | + |
| 926 | + auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq"); |
| 927 | + auto channelAlloc = o2::pmr::getTransportAllocator(transport.get()); |
| 928 | + |
| 929 | + DataHeader dh{"COND", "TST", 0}; |
| 930 | + dh.splitPayloadParts = 1; |
| 931 | + dh.splitPayloadIndex = 0; |
| 932 | + DataProcessingHeader dph{0, 1}; |
| 933 | + |
| 934 | + // Build an expiration handler that always tries to expire |
| 935 | + ExpirationHandler handler; |
| 936 | + handler.name = "test-condition"; |
| 937 | + handler.routeIndex = RouteIndex{0}; |
| 938 | + handler.lifetime = Lifetime::Condition; |
| 939 | + handler.creator = [](ServiceRegistryRef services, ChannelIndex channelIndex) -> TimesliceSlot { |
| 940 | + auto& index = services.get<TimesliceIndex>(); |
| 941 | + for (size_t si = 0; si < index.size(); si++) { |
| 942 | + TimesliceSlot slot{si}; |
| 943 | + if (!index.isValid(slot)) { |
| 944 | + index.associate(TimesliceId{0}, slot); |
| 945 | + index.setOldestPossibleInput({1}, channelIndex); |
| 946 | + return slot; |
| 947 | + } |
| 948 | + } |
| 949 | + return TimesliceSlot{TimesliceSlot::INVALID}; |
| 950 | + }; |
| 951 | + handler.checker = LifetimeHelpers::expireAlways(); |
| 952 | + int handlerCallCount = 0; |
| 953 | + handler.handler = [&transport, &channelAlloc, &dh, &dph, &handlerCallCount](ServiceRegistryRef, PartRef& ref, data_matcher::VariableContext&) { |
| 954 | + ref.header = o2::pmr::getMessage(o2::header::Stack{channelAlloc, dh, dph}); |
| 955 | + ref.payload = transport->CreateMessage(4); |
| 956 | + handlerCallCount++; |
| 957 | + }; |
| 958 | + std::vector<ExpirationHandler> handlers{handler}; |
| 959 | + |
| 960 | + // First call: slot is empty, so the handler fires and materialises data |
| 961 | + auto activity1 = relayer.processDanglingInputs(handlers, {registry}, true); |
| 962 | + REQUIRE(activity1.expiredSlots == 1); |
| 963 | + REQUIRE(handlerCallCount == 1); |
| 964 | + |
| 965 | + // Second call: slot already has data — the handler must NOT fire again |
| 966 | + auto activity2 = relayer.processDanglingInputs(handlers, {registry}, false); |
| 967 | + REQUIRE(activity2.expiredSlots == 0); |
| 968 | + REQUIRE(handlerCallCount == 1); // handler was not called a second time |
| 969 | + } |
811 | 970 | } |
0 commit comments