From 58b71ccaf1c5224ecb64d8645cfd26a3501615fd Mon Sep 17 00:00:00 2001
From: Max Hniebergall <137079448+maxhniebergall@users.noreply.github.com>
Date: Fri, 13 Sep 2024 10:57:51 -0400
Subject: [PATCH 01/27] [backport 8.16] Zero dimensional tensor (#2723)

* Change result writing to allow for a single value

* change to debug logging

* Wrap output in extra array to make 3D. Improve debug logging formatting

* Added unit test

* Add changelog

* formatting

* Fix reshaping of output tensor to only resahep zero-dimensional tensors

* Use std::move

* Backport fix by removing debug line; also remove std::move for reshape

* Update CHANGELOG.asciidoc
---
 bin/pytorch_inference/CResultWriter.cc         |  3 +++
 bin/pytorch_inference/CResultWriter.h          | 18 ++++++++++++++++++
 bin/pytorch_inference/Main.cc                  |  7 ++++++-
 .../unittest/CResultWriterTest.cc              | 12 ++++++++++++
 docs/CHANGELOG.asciidoc                        |  4 ++++
 5 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/bin/pytorch_inference/CResultWriter.cc b/bin/pytorch_inference/CResultWriter.cc
index b4ca0baeb0..34389dad44 100644
--- a/bin/pytorch_inference/CResultWriter.cc
+++ b/bin/pytorch_inference/CResultWriter.cc
@@ -136,6 +136,9 @@ std::string CResultWriter::createInnerResult(const ::torch::Tensor& results) {
             case 2:
                 this->writePrediction<2>(results, jsonWriter);
                 break;
+            case 1:
+                this->writePrediction<1>(results, jsonWriter);
+                break;
             default: {
                 std::ostringstream ss;
                 ss << "Cannot convert results tensor of size [" << sizes << ']';
diff --git a/bin/pytorch_inference/CResultWriter.h b/bin/pytorch_inference/CResultWriter.h
index 037a2769f5..8d809dc9df 100644
--- a/bin/pytorch_inference/CResultWriter.h
+++ b/bin/pytorch_inference/CResultWriter.h
@@ -191,6 +191,24 @@ class CResultWriter : public TStringBufWriter {
         jsonWriter.onObjectEnd();
     }
 
+    //! Write a 1D inference result
+    template<typename T>
+    void writeInferenceResults(const ::torch::TensorAccessor<T, 1UL>& accessor,
+                               TStringBufWriter& jsonWriter) {
+
+        jsonWriter.onKey(RESULT);
+        jsonWriter.onObjectBegin();
+        jsonWriter.onKey(INFERENCE);
+        // The Java side requires a 3D array, so wrap the 1D result in an
+        // extra outer array twice.
+        jsonWriter.onArrayBegin();
+        jsonWriter.onArrayBegin();
+        this->writeTensor(accessor, jsonWriter);
+        jsonWriter.onArrayEnd();
+        jsonWriter.onArrayEnd();
+        jsonWriter.onObjectEnd();
+    }
+
 private:
     core::CJsonOutputStreamWrapper m_WrappedOutputStream;
 };
diff --git a/bin/pytorch_inference/Main.cc b/bin/pytorch_inference/Main.cc
index 662810a48c..92db0aacec 100644
--- a/bin/pytorch_inference/Main.cc
+++ b/bin/pytorch_inference/Main.cc
@@ -73,7 +73,12 @@ torch::Tensor infer(torch::jit::script::Module& module_,
             // For transformers the result tensor is the first element in a tuple.
             all.push_back(output.toTuple()->elements()[0].toTensor());
         } else {
-            all.push_back(output.toTensor());
+            auto outputTensor = output.toTensor();
+            if (outputTensor.dim() == 0) { // If the output is a scaler, we need to reshape it into a 1D tensor
+                all.push_back(outputTensor.reshape({1, 1}));
+            } else {
+                all.push_back(std::move(outputTensor));
+            }
         }
 
         inputs.clear();
diff --git a/bin/pytorch_inference/unittest/CResultWriterTest.cc b/bin/pytorch_inference/unittest/CResultWriterTest.cc
index 99333db8c3..97b99038a2 100644
--- a/bin/pytorch_inference/unittest/CResultWriterTest.cc
+++ b/bin/pytorch_inference/unittest/CResultWriterTest.cc
@@ -80,6 +80,18 @@ BOOST_AUTO_TEST_CASE(testCreateInnerInferenceResult) {
     BOOST_REQUIRE_EQUAL(expected, innerPortion);
 }
 
+BOOST_AUTO_TEST_CASE(testCreateInnerInferenceResultFor1DimensionalResult) {
+    std::ostringstream output;
+    ml::torch::CResultWriter resultWriter{output};
+    ::torch::Tensor tensor{::torch::ones({1})};
+    std::string innerPortion{resultWriter.createInnerResult(tensor)};
+    std::string expected = "\"result\":{\"inference\":"
+                           "[[[1]]]}";
+    LOG_INFO(<< "expected: " << expected);
+    LOG_INFO(<< "actual: " << innerPortion);
+    BOOST_REQUIRE_EQUAL(expected, innerPortion);
+}
+
 BOOST_AUTO_TEST_CASE(testWrapAndWriteInferenceResult) {
     std::string innerPortion{
         "\"result\":{\"inference\":"
diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc
index 6f98ce1ff8..9c1d528166 100644
--- a/docs/CHANGELOG.asciidoc
+++ b/docs/CHANGELOG.asciidoc
@@ -35,6 +35,10 @@
 * Allow the user to force a detector to shift time series state by a specific amount.
   (See {ml-pull}2695[#2695].)
 
+=== Bug Fixes
+
+* Allow for pytorch_inference results to include zero-dimensional tensors.
+
 == {es} version 8.15.2
 
 === Enhancements

From fc5d248911a1a969b2e136098397fbad6efcd8e7 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Thu, 19 Sep 2024 15:39:44 +1200
Subject: [PATCH 02/27] [8.16][ML] Fix failing testFileIoIsGood test on mac x86
 (#2728) (#2732)

Bump a test thread timeout to increase the chance of successfully
reading from a test file on slower machines.

Prior to this change the `CIoManagerTest/testFileIoIsGood` test has been
failing approx 5 - 15 pct of the time. Testing the change with this
script

```
testFailures=0; for i in {1..1000}; do ../../../cmake-build-relwithdebinfo/test/lib/api/unittest/ml_test_api --run_test=CIoManagerTest/testFileIoGood; if [ $? != 0 ]; then ((++testFailures)); fi; done; echo $testFailures
```

echoes `0` upon completion.

Backports #2728
---
 lib/api/unittest/CIoManagerTest.cc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/api/unittest/CIoManagerTest.cc b/lib/api/unittest/CIoManagerTest.cc
index fcbdd1f763..1c99ebb46e 100644
--- a/lib/api/unittest/CIoManagerTest.cc
+++ b/lib/api/unittest/CIoManagerTest.cc
@@ -29,7 +29,7 @@ BOOST_AUTO_TEST_SUITE(CIoManagerTest)
 namespace {
 
 const std::uint32_t SLEEP_TIME_MS{100};
-const std::uint32_t PAUSE_TIME_MS{10};
+const std::uint32_t PAUSE_TIME_MS{40};
 const std::size_t MAX_ATTEMPTS{100};
 const std::size_t TEST_SIZE{10000};
 const char TEST_CHAR{'a'};

From fbf586214cf6a0476b81e0edd1e7725a672b6d70 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Thu, 19 Sep 2024 17:20:32 +1200
Subject: [PATCH 03/27] [8.16][ML] Correct home for macOS x86_64 test results
 (#2725) (#2735)

Ensure that macOS x86_64 unit test results are stored correctly in
Buildkite test analytics.

Backports #2725
---
 .buildkite/hooks/post-checkout | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.buildkite/hooks/post-checkout b/.buildkite/hooks/post-checkout
index 7ed9a76c55..2833da5326 100644
--- a/.buildkite/hooks/post-checkout
+++ b/.buildkite/hooks/post-checkout
@@ -21,6 +21,8 @@ if [[ "$BUILDKITE_PIPELINE_SLUG" == ml-cpp* ]]; then
     export BUILDKITE_ANALYTICS_TOKEN=$(vault read secret/ci/elastic-ml-cpp/buildkite/test_analytics/linux_aarch64 | awk '/^token/ {print $2;}')
   elif [[ "$BUILDKITE_STEP_KEY" == "build_test_macos-aarch64-RelWithDebInfo" ]]; then
     export BUILDKITE_ANALYTICS_TOKEN=$(vault read secret/ci/elastic-ml-cpp/buildkite/test_analytics/macos_aarch64 | awk '/^token/ {print $2;}')
+  elif [[ "$BUILDKITE_STEP_KEY" == "build_test_macos-x86_64-RelWithDebInfo" ]]; then
+    export BUILDKITE_ANALYTICS_TOKEN=$(vault read secret/ci/elastic-ml-cpp/buildkite/test_analytics/macos_x86_64 | awk '/^token/ {print $2;}')
   else [[ "$BUILDKITE_STEP_KEY" == "build_test_Windows-x86_64-RelWithDebInfo" ]]
     export BUILDKITE_ANALYTICS_TOKEN=$(vault read secret/ci/elastic-ml-cpp/buildkite/test_analytics/windows_x86_64 | awk '/^token/ {print $2;}')
   fi

From 0bc01ba0e12e9bc8c634d096e79f740df621bf80 Mon Sep 17 00:00:00 2001
From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
Date: Tue, 24 Sep 2024 14:01:24 +0200
Subject: [PATCH 04/27] [ML] Change time shift sign for intuitive configuration
 (#2737) (#2743)

Backport for #2737
---
 lib/model/CDetectionRule.cc              | 7 ++++++-
 lib/model/unittest/CDetectionRuleTest.cc | 8 ++++----
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/lib/model/CDetectionRule.cc b/lib/model/CDetectionRule.cc
index 0ad0c21d76..c6f8dd3531 100644
--- a/lib/model/CDetectionRule.cc
+++ b/lib/model/CDetectionRule.cc
@@ -87,7 +87,12 @@ void CDetectionRule::addTimeShift(core_t::TTime timeShift) {
             timeShiftApplied.end()) {
             // When the callback is executed, the model is already in the correct time
             // interval. Hence, we need to shift the time right away.
-            model.shiftTime(time, timeShift);
+            // IMPLEMENTATION DECISION: We apply the negative amount of time shift to the
+            // model. This is because the time shift is applied to the model's frame of reference
+            // and not the global time. This allows a more intuitive configuration from the user's
+            // perspective: in spring we move the clock forward, and the time shift is positive, in
+            // autumn we move the clock backward, and the time shift is negative.
+            model.shiftTime(time, -timeShift);
             timeShiftApplied.emplace_back(&model);
         }
     });
diff --git a/lib/model/unittest/CDetectionRuleTest.cc b/lib/model/unittest/CDetectionRuleTest.cc
index 09c408790e..ec41d0a85c 100644
--- a/lib/model/unittest/CDetectionRuleTest.cc
+++ b/lib/model/unittest/CDetectionRuleTest.cc
@@ -1020,8 +1020,8 @@ BOOST_FIXTURE_TEST_CASE(testRuleTimeShiftShouldShiftTimeSeriesModelState, CTestF
         rule.executeCallback(*model, timestamp);
 
         // the time series model should have been shifted by specified amount.
-        BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTime + timeShiftInSecs);
-        BOOST_TEST_REQUIRE(trendModel.timeShift() == timeShiftInSecs);
+        BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTime - timeShiftInSecs);
+        BOOST_TEST_REQUIRE(trendModel.timeShift() == -timeShiftInSecs);
 
         // and an annotation should have been added to the model
         BOOST_TEST_REQUIRE(annotations.size() == numAnnotationsBeforeShift + 1);
@@ -1091,8 +1091,8 @@ BOOST_FIXTURE_TEST_CASE(testTwoTimeShiftRuleShouldShiftTwice, CTestFixture) {
     // the values after the second time should be the sum of two rules.
     timestamp += timeShift1; // simulate the time has moved forward by the time shift
     rule2.executeCallback(*model, timestamp);
-    BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTimeAfterFirstShift + timeShift2);
-    BOOST_TEST_REQUIRE(trendModel.timeShift() == timeShift1 + timeShift2);
+    BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTimeAfterFirstShift - timeShift2);
+    BOOST_TEST_REQUIRE(trendModel.timeShift() == -(timeShift1 + timeShift2));
 }
 
 BOOST_AUTO_TEST_SUITE_END()

From f240a162613884c08fc89489a41548c962dc7ee6 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Wed, 25 Sep 2024 13:22:38 +1200
Subject: [PATCH 05/27] [8.16][ML] Fudge for CKMostCorrelatedTest/testScale
 (#2729) (#2744)

Add a bit of tolerance for a CKMostCorrelatedTest/testScale comparison.
This _should_ reduce the frequency of failures on macOS x86_64 CI builds
that are a bit slooooower than the rest.

Backports #2729
---
 lib/maths/common/unittest/CKMostCorrelatedTest.cc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/maths/common/unittest/CKMostCorrelatedTest.cc b/lib/maths/common/unittest/CKMostCorrelatedTest.cc
index 1cc23fbf90..5b8fd373d6 100644
--- a/lib/maths/common/unittest/CKMostCorrelatedTest.cc
+++ b/lib/maths/common/unittest/CKMostCorrelatedTest.cc
@@ -764,7 +764,7 @@ BOOST_AUTO_TEST_CASE(testScale) {
     double sdRatio = std::sqrt(maths::common::CBasicStatistics::variance(slope)) /
                      maths::common::CBasicStatistics::mean(slope);
     LOG_DEBUG(<< "sdRatio = " << sdRatio);
-    BOOST_TEST_REQUIRE(exponent < 2.0);
+    BOOST_TEST(exponent <= 2.0, boost::test_tools::tolerance(0.1));
     BOOST_TEST_REQUIRE(sdRatio < 0.75);
 }
 

From 393ff55b50e10d2d9e1097e9ff8c98e8bf7addc5 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Wed, 25 Sep 2024 13:24:43 +1200
Subject: [PATCH 06/27] [8.16][ML] Add CMake config to integrate gperftools
 (#2713) (#2750)

Add options to link in libprofiler and/or libtcmalloc to autodetact and
pytorch_inference. For Linux builds only. Not for production releases.

Backports #2713
---
 CMakeLists.txt                       | 28 ++++++++++++++++++++++++++++
 bin/autodetect/CMakeLists.txt        | 10 ++++++++++
 bin/pytorch_inference/CMakeLists.txt | 10 ++++++++++
 3 files changed, 48 insertions(+)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index ea9df4cec3..0aeb7a04fe 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -69,6 +69,26 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
   install(FILES ${CMAKE_BINARY_DIR}/Info.plist DESTINATION ${CMAKE_INSTALL_PREFIX})
 endif()
 
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+  if(NOT LINK_TCMALLOC)
+    set(LINK_TCMALLOC FALSE)
+  endif()
+  if(NOT LINK_PROFILER)
+    set(LINK_PROFILER FALSE)
+  endif()
+else()
+  if(LINK_TCMALLOC)
+    message(WARNING "Not linking libtcmalloc on ${CMAKE_SYSTEM_NAME}")
+    set(LINK_TCMALLOC FALSE)
+    unset(LINK_TCMALLOC CACHE)
+  endif()
+  if(LINK_PROFILER)
+    message(WARNING "Not linking libprofiler on ${CMAKE_SYSTEM_NAME}")
+    set(LINK_PROFILER FALSE)
+    unset(LINK_PROFILER CACHE)
+  endif()
+endif()
+
 message(STATUS "CMAKE_INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX}")
 
 include_directories(SYSTEM ${ML_SYSTEM_INCLUDE_DIRECTORIES})
@@ -87,3 +107,11 @@ add_subdirectory(devlib)
 # Add a target to build Doxygen generated documentation
 # if the doxygen executable can be found
 ml_doxygen(${CMAKE_SOURCE_DIR}/build/doxygen)
+
+if (LINK_TCMALLOC)
+  unset(LINK_TCMALLOC CACHE)
+endif()
+
+if (LINK_PROFILER)
+  unset(LINK_PROFILER CACHE)
+endif()
diff --git a/bin/autodetect/CMakeLists.txt b/bin/autodetect/CMakeLists.txt
index 970095c0b7..93af5e9201 100644
--- a/bin/autodetect/CMakeLists.txt
+++ b/bin/autodetect/CMakeLists.txt
@@ -22,6 +22,16 @@ set(ML_LINK_LIBRARIES
   MlVer
   )
 
+if (LINK_TCMALLOC)
+  message(AUTHOR_WARNING "Linking libtcmalloc. Build is not for production release.")
+  list(APPEND ML_LINK_LIBRARIES tcmalloc)
+endif ()
+
+if (LINK_PROFILER)
+  message(AUTHOR_WARNING "Linking libprofiler. Build is not for production release.")
+  list(APPEND ML_LINK_LIBRARIES profiler)
+endif ()
+
 ml_add_executable(autodetect
   CCmdLineParser.cc
   )
diff --git a/bin/pytorch_inference/CMakeLists.txt b/bin/pytorch_inference/CMakeLists.txt
index 62a4f3defd..5c6ff63528 100644
--- a/bin/pytorch_inference/CMakeLists.txt
+++ b/bin/pytorch_inference/CMakeLists.txt
@@ -21,6 +21,16 @@ set(ML_LINK_LIBRARIES
   ${C10_LIB}
   )
 
+if (LINK_TCMALLOC)
+  message(AUTHOR_WARNING "Linking libtcmalloc. Build is not for production release.")
+  list(APPEND ML_LINK_LIBRARIES tcmalloc)
+endif ()
+
+if (LINK_PROFILER)
+  message(AUTHOR_WARNING "Linking libprofiler. Build is not for production release.")
+  list(APPEND ML_LINK_LIBRARIES profiler)
+endif ()
+
 ml_add_executable(pytorch_inference
   CBufferedIStreamAdapter.cc
   CCmdLineParser.cc

From b9664f95a5d7d8e91238651ebb091d60b34c0c19 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Wed, 25 Sep 2024 13:44:41 +1200
Subject: [PATCH 07/27] [8.16][ML] Trigger a "staging" build on merge to branch
 (#2717) (#2752)

Ensure that code changes that are merged to active branches are
automatically incorporated into a build.

Backports #2717
---
 catalog-info.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/catalog-info.yaml b/catalog-info.yaml
index e1105881dd..cb3098d8a4 100644
--- a/catalog-info.yaml
+++ b/catalog-info.yaml
@@ -219,7 +219,7 @@ spec:
 
           '
         filter_enabled: true
-        trigger_mode: none
+        trigger_mode: code
       repository: elastic/ml-cpp
       skip_intermediate_builds: true
       teams:

From a12f4f7df89d5c6544f73fdb96b673eeb35fc1f6 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Wed, 25 Sep 2024 14:53:59 +1200
Subject: [PATCH 08/27] [8.16][ML] Fix a few address sanitizer issues (#2738)
 (#2745)

Fix a few issues picked up by the address sanitizer. These are mostly
related to unit tests, including one that leads to an occasional crash
on Windows - #2651, and also:

* Fix a heap-buffer-overflow in test case
   Address Sanitizer picked up a heap-buffer-overflow in `CBoostedTreeTest/testEdgeCases`. Rework the test slightly to avoid it.

* Optimize code when running the Address Sanitizer
   Add the `O3` flag to those used for compiling with the Address Sanitizer enabled. In most cases this is sufficient and cuts the run time significantly.

Backports #2738
---
 cmake/variables.cmake                               |  6 +++---
 lib/api/unittest/CJsonOutputWriterTest.cc           |  5 +++--
 lib/maths/analytics/unittest/CBoostedTreeTest.cc    |  4 ++--
 lib/maths/time_series/CTrendComponent.cc            | 13 +++++++++----
 .../time_series/unittest/CCalendarCyclicTestTest.cc |  2 +-
 5 files changed, 18 insertions(+), 12 deletions(-)

diff --git a/cmake/variables.cmake b/cmake/variables.cmake
index a442121ce5..a1f557dd67 100644
--- a/cmake/variables.cmake
+++ b/cmake/variables.cmake
@@ -160,7 +160,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
   set(CMAKE_CXX_FLAGS_RELEASE "/O2 /D NDEBUG /D EXCLUDE_TRACE_LOGGING /Qfast_transcendentals /Qvec-report:1")
   set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "/Zi /O2 /D NDEBUG /D EXCLUDE_TRACE_LOGGING /Qfast_transcendentals /Qvec-report:1")
   set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Od /RTC1")
-  set(CMAKE_CXX_FLAGS_SANITIZER "/fsanitize=address /Zi" CACHE STRING
+  set(CMAKE_CXX_FLAGS_SANITIZER "/fsanitize=address /O2 /Zi" CACHE STRING
           "Flags used by the C++ compiler during sanitizer builds."
           FORCE)
   set(CMAKE_EXE_LINKER_FLAGS_SANITIZER "")
@@ -173,7 +173,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
   set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -DEXCLUDE_TRACE_LOGGING -Wdisabled-optimization -D_FORTIFY_SOURCE=2")
   set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3 -DNDEBUG -DEXCLUDE_TRACE_LOGGING -Wdisabled-optimization -D_FORTIFY_SOURCE=2")
   set(CMAKE_CXX_FLAGS_DEBUG "-g")
-  set(CMAKE_CXX_FLAGS_SANITIZER "-fsanitize=address -g -fno-omit-frame-pointer" CACHE STRING
+  set(CMAKE_CXX_FLAGS_SANITIZER "-fsanitize=address -g -O3 -fno-omit-frame-pointer" CACHE STRING
           "Flags used by the C++ compiler during sanitizer builds."
           FORCE)
 endif()
@@ -182,7 +182,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
   set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -DEXCLUDE_TRACE_LOGGING")
   set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3 -DNDEBUG -DEXCLUDE_TRACE_LOGGING")
   set(CMAKE_CXX_FLAGS_DEBUG "-g")
-  set(CMAKE_CXX_FLAGS_SANITIZER "-fsanitize=address -g -fno-omit-frame-pointer" CACHE STRING
+  set(CMAKE_CXX_FLAGS_SANITIZER "-fsanitize=address -g -O3 -fno-omit-frame-pointer" CACHE STRING
           "Flags used by the C++ compiler during sanitizer builds."
           FORCE)
   mark_as_advanced(
diff --git a/lib/api/unittest/CJsonOutputWriterTest.cc b/lib/api/unittest/CJsonOutputWriterTest.cc
index a41d0dccf1..a88803e221 100644
--- a/lib/api/unittest/CJsonOutputWriterTest.cc
+++ b/lib/api/unittest/CJsonOutputWriterTest.cc
@@ -1010,6 +1010,7 @@ BOOST_AUTO_TEST_CASE(testGeoResultsWrite) {
     std::string functionDescription("lat_long(location)");
     ml::api::CHierarchicalResultsWriter::TOptionalStrOptionalStrPrDoublePrVec influences;
     std::string emptyString;
+    std::string mean_function("mean");
     // The output writer won't close the JSON structures until is is destroyed
     {
         std::ostringstream sstream;
@@ -1099,8 +1100,8 @@ BOOST_AUTO_TEST_CASE(testGeoResultsWrite) {
             ml::api::CHierarchicalResultsWriter::SResults result(
                 ml::api::CHierarchicalResultsWriter::E_Result, partitionFieldName,
                 partitionFieldValue, byFieldName, byFieldValue,
-                correlatedByFieldValue, 1, "mean", functionDescription, 2.24,
-                79, typical, actual, 10.0, 10.0, 0.5, 0.0, fieldName,
+                correlatedByFieldValue, 1, mean_function, functionDescription,
+                2.24, 79, typical, actual, 10.0, 10.0, 0.5, 0.0, fieldName,
                 influences, false, true, 1, 1, EMPTY_STRING_LIST, {});
             BOOST_TEST_REQUIRE(writer.acceptResult(result));
             BOOST_TEST_REQUIRE(writer.endOutputBatch(false, 1U));
diff --git a/lib/maths/analytics/unittest/CBoostedTreeTest.cc b/lib/maths/analytics/unittest/CBoostedTreeTest.cc
index 3161800b97..dbed0b6653 100644
--- a/lib/maths/analytics/unittest/CBoostedTreeTest.cc
+++ b/lib/maths/analytics/unittest/CBoostedTreeTest.cc
@@ -488,8 +488,8 @@ BOOST_AUTO_TEST_CASE(testEdgeCases) {
 
     auto frame = core::makeMainStorageDataFrame(cols).first;
 
-    fillDataFrame(5, 0, 2, {{1.0}, {1.0}, {1.0}, {1.0}, {1.0}},
-                  {0.0, 0.0, 0.0, 0.0, 0.0}, [](const TRowRef&) { return 1.0; }, *frame);
+    fillDataFrame(5, 0, 2, {{1.0, 1.0, 1.0, 1.0, 1.0}}, {0.0, 0.0, 0.0, 0.0, 0.0},
+                  [](const TRowRef&) { return 1.0; }, *frame);
 
     BOOST_REQUIRE_NO_THROW(maths::analytics::CBoostedTreeFactory::constructFromParameters(
                                1, std::make_unique<maths::analytics::boosted_tree::CMse>())
diff --git a/lib/maths/time_series/CTrendComponent.cc b/lib/maths/time_series/CTrendComponent.cc
index 78f9a27216..caaaa288bb 100644
--- a/lib/maths/time_series/CTrendComponent.cc
+++ b/lib/maths/time_series/CTrendComponent.cc
@@ -310,10 +310,15 @@ void CTrendComponent::shiftLevel(double shift,
     double magnitude{shifts[last] - shifts[next - 1]};
     if (m_TimeOfLastLevelChange != UNSET_TIME) {
         double dt{static_cast<double>(time - m_TimeOfLastLevelChange)};
-        double value{static_cast<double>(
-            common::CBasicStatistics::mean(values[segments[next] - 1]))};
-        m_ProbabilityOfLevelChangeModel.addTrainingDataPoint(LEVEL_CHANGE_LABEL,
-                                                             {{dt}, {value}});
+        if (values.size() > segments[next] - 1) {
+            double value{static_cast<double>(
+                common::CBasicStatistics::mean(values[segments[next] - 1]))};
+            m_ProbabilityOfLevelChangeModel.addTrainingDataPoint(LEVEL_CHANGE_LABEL,
+                                                                 {{dt}, {value}});
+        } else {
+            LOG_DEBUG(<< "Size mis-match reading from values. Length = "
+                      << values.size() << ", requested index = " << segments[next] - 1);
+        }
     }
     m_TimeOfLastLevelChange = time;
     for (std::size_t i = segments[last]; i < values.size(); ++i, time += bucketLength) {
diff --git a/lib/maths/time_series/unittest/CCalendarCyclicTestTest.cc b/lib/maths/time_series/unittest/CCalendarCyclicTestTest.cc
index 4609953e33..e6a791293b 100644
--- a/lib/maths/time_series/unittest/CCalendarCyclicTestTest.cc
+++ b/lib/maths/time_series/unittest/CCalendarCyclicTestTest.cc
@@ -528,7 +528,7 @@ BOOST_AUTO_TEST_CASE(testLongBuckets) {
         TDoubleVec error;
         for (core_t::TTime time = 0, i = 0; time <= end; time += DAY) {
             rng.generateNormalSamples(0.0, 9.0, 1, error);
-            if (time >= months[i] && time < months[i] + DAY) {
+            if (time >= months[i] && time < months[i] + DAY && i < months.size() - 1) {
                 error[0] += 20.0;
                 ++i;
             }

From e61db8cc456a9240d4f777b38bbc6ad8dba86e80 Mon Sep 17 00:00:00 2001
From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
Date: Wed, 25 Sep 2024 15:33:58 +0200
Subject: [PATCH 09/27] [ML] Persist callback detection rules in the detector
 model state (#2739) (#2754)

Backport for #2739
---
 include/core/CFlatPrefixTree.h           |  4 +
 include/core/CPatternSet.h               |  2 +
 include/model/CAnomalyDetectorModel.h    | 14 ++++
 include/model/CDetectionRule.h           |  9 +++
 include/model/CPopulationModel.h         |  2 -
 include/model/CRuleCondition.h           |  2 +
 include/model/CRuleScope.h               |  3 +
 lib/core/CFlatPrefixTree.cc              | 20 +++++
 lib/core/CPatternSet.cc                  | 12 +++
 lib/model/CAnomalyDetectorModel.cc       | 35 +++++++++
 lib/model/CCountingModel.cc              |  9 +++
 lib/model/CDetectionRule.cc              | 62 +++++++++++-----
 lib/model/CIndividualModel.cc            |  5 ++
 lib/model/CPopulationModel.cc            | 13 +++-
 lib/model/CRuleCondition.cc              |  9 +++
 lib/model/CRuleScope.cc                  | 12 +++
 lib/model/unittest/CCountingModelTest.cc | 42 +++++++++++
 lib/model/unittest/CDetectionRuleTest.cc | 95 ++++++++++++++++++++++++
 18 files changed, 329 insertions(+), 21 deletions(-)

diff --git a/include/core/CFlatPrefixTree.h b/include/core/CFlatPrefixTree.h
index 7addcfed58..fc065e0eda 100644
--- a/include/core/CFlatPrefixTree.h
+++ b/include/core/CFlatPrefixTree.h
@@ -63,6 +63,8 @@ class CORE_EXPORT CFlatPrefixTree {
 
         SNode(char c, char type, std::uint32_t next);
 
+        std::uint64_t checksum() const;
+
         bool operator<(char rhs) const;
         char s_Char;
         char s_Type;
@@ -120,6 +122,8 @@ class CORE_EXPORT CFlatPrefixTree {
     //! Pretty-prints the tree.
     std::string print() const;
 
+    std::uint64_t checksum() const;
+
 private:
     //! The recursive building helper.
     void buildRecursively(const TStrVec& prefixes,
diff --git a/include/core/CPatternSet.h b/include/core/CPatternSet.h
index 8b00d6564d..a3d1e0aa04 100644
--- a/include/core/CPatternSet.h
+++ b/include/core/CPatternSet.h
@@ -61,6 +61,8 @@ class CORE_EXPORT CPatternSet {
     //! Clears the set.
     void clear();
 
+    std::uint64_t checksum() const;
+
 private:
     void sortAndPruneDuplicates(TStrVec& keys);
 
diff --git a/include/model/CAnomalyDetectorModel.h b/include/model/CAnomalyDetectorModel.h
index 2e760151ed..cb6aa71a30 100644
--- a/include/model/CAnomalyDetectorModel.h
+++ b/include/model/CAnomalyDetectorModel.h
@@ -492,11 +492,19 @@ class MODEL_EXPORT CAnomalyDetectorModel {
     //! Apply time shift at the time \p time by \p shift amount of seconds.
     virtual void shiftTime(core_t::TTime time, core_t::TTime shift) = 0;
 
+    //! Check if the rule has been applied.
+    bool checkRuleApplied(const CDetectionRule& rule) const;
+
+    //! Mark the rule as applied.
+    void markRuleApplied(const CDetectionRule& rule);
+
 protected:
     using TStrCRef = std::reference_wrapper<const std::string>;
     using TSizeSize1VecUMap = boost::unordered_map<std::size_t, TSize1Vec>;
     using TFeatureSizeSize1VecUMapPr = std::pair<model_t::EFeature, TSizeSize1VecUMap>;
     using TFeatureSizeSize1VecUMapPrVec = std::vector<TFeatureSizeSize1VecUMapPr>;
+    using TUint64TTimePr = std::pair<std::uint64_t, core_t::TTime>;
+    using TUint64TTimePrVec = std::vector<TUint64TTimePr>;
 
     //! \brief The feature models.
     struct MODEL_EXPORT SFeatureModels {
@@ -710,6 +718,9 @@ class MODEL_EXPORT CAnomalyDetectorModel {
                                CAnnotation::EEvent type,
                                const std::string& annotation) = 0;
 
+    TUint64TTimePrVec& appliedRuleChecksums();
+    const TUint64TTimePrVec& appliedRuleChecksums() const;
+
 private:
     using TModelParamsCRef = std::reference_wrapper<const SModelParams>;
 
@@ -738,6 +749,9 @@ class MODEL_EXPORT CAnomalyDetectorModel {
     //! The influence calculators to use for each feature which is being
     //! modeled.
     TFeatureInfluenceCalculatorCPtrPrVecVec m_InfluenceCalculators;
+
+    //! Checksums of the rules that should be applied only once.
+    TUint64TTimePrVec m_AppliedRuleChecksums;
 };
 
 class CMemoryCircuitBreaker : public core::CMemoryCircuitBreaker {
diff --git a/include/model/CDetectionRule.h b/include/model/CDetectionRule.h
index f7dd4f0a8b..4bf3dd0165 100644
--- a/include/model/CDetectionRule.h
+++ b/include/model/CDetectionRule.h
@@ -65,6 +65,9 @@ class MODEL_EXPORT CDetectionRule {
     //! Add a condition.
     void addCondition(const CRuleCondition& condition);
 
+    //! Clear conditions.
+    void clearConditions();
+
     //! Set callback function to apply some action to a supplied time series model.
     void setCallback(TCallback cb);
 
@@ -88,6 +91,9 @@ class MODEL_EXPORT CDetectionRule {
     //! Pretty-print the rule.
     std::string print() const;
 
+    //! Checksum the rule.
+    std::uint64_t checksum() const;
+
 private:
     std::string printAction() const;
 
@@ -105,6 +111,9 @@ class MODEL_EXPORT CDetectionRule {
 
     //! Callback function to apply a change to a model based on the rule action.
     TCallback m_Callback;
+
+    //! The time shift to apply to the model.
+    core_t::TTime m_TimeShift{0};
 };
 }
 }
diff --git a/include/model/CPopulationModel.h b/include/model/CPopulationModel.h
index 347f5509bd..979fbc35e0 100644
--- a/include/model/CPopulationModel.h
+++ b/include/model/CPopulationModel.h
@@ -23,8 +23,6 @@
 #include <model/ImportExport.h>
 #include <model/ModelTypes.h>
 
-#include <map>
-#include <string>
 #include <utility>
 #include <vector>
 
diff --git a/include/model/CRuleCondition.h b/include/model/CRuleCondition.h
index db675760e1..d7b3937e4b 100644
--- a/include/model/CRuleCondition.h
+++ b/include/model/CRuleCondition.h
@@ -73,6 +73,8 @@ class MODEL_EXPORT CRuleCondition {
               std::size_t cid,
               core_t::TTime time) const;
 
+    std::uint64_t checksum() const;
+
 private:
     bool testValue(double value) const;
     std::string print(ERuleConditionAppliesTo appliesTo) const;
diff --git a/include/model/CRuleScope.h b/include/model/CRuleScope.h
index 8cf5444c81..0ea0a47506 100644
--- a/include/model/CRuleScope.h
+++ b/include/model/CRuleScope.h
@@ -16,6 +16,7 @@
 #include <core/CPatternSet.h>
 #include <core/CTriple.h>
 
+#include <cstdint>
 #include <string>
 #include <vector>
 
@@ -58,6 +59,8 @@ class MODEL_EXPORT CRuleScope {
     //! Pretty-print the scope.
     std::string print() const;
 
+    std::uint64_t checksum() const;
+
 private:
     //! A vector that holds the triple of the field, filter and its type.
     TStrPatternSetCRefFilterTypeTrVec m_Scope;
diff --git a/lib/core/CFlatPrefixTree.cc b/lib/core/CFlatPrefixTree.cc
index 56f6dbdcfc..f36489346e 100644
--- a/lib/core/CFlatPrefixTree.cc
+++ b/lib/core/CFlatPrefixTree.cc
@@ -11,6 +11,7 @@
 
 #include <core/CFlatPrefixTree.h>
 
+#include <core/CHashing.h>
 #include <core/CLogger.h>
 #include <core/CMemoryDef.h>
 #include <core/CStringUtils.h>
@@ -226,5 +227,24 @@ std::string CFlatPrefixTree::print() const {
     result += "]";
     return result;
 }
+
+std::uint64_t CFlatPrefixTree::SNode::checksum() const {
+    std::uint64_t result{0};
+    result = CHashing::hashCombine(result, static_cast<std::uint64_t>(s_Char));
+    result = CHashing::hashCombine(result, static_cast<std::uint64_t>(s_Type));
+    result = CHashing::hashCombine(result, static_cast<std::uint64_t>(s_Next));
+    return result;
+}
+
+std::uint64_t CFlatPrefixTree::checksum() const {
+    std::uint64_t result{0};
+
+    // Iterate over m_FlatTree
+    for (const auto& node : m_FlatTree) {
+        result = CHashing::hashCombine(result, node.checksum());
+    }
+
+    return result;
+}
 }
 }
diff --git a/lib/core/CPatternSet.cc b/lib/core/CPatternSet.cc
index 393d6796f8..c4cf3b6275 100644
--- a/lib/core/CPatternSet.cc
+++ b/lib/core/CPatternSet.cc
@@ -12,6 +12,7 @@
 #include <core/CPatternSet.h>
 
 #include <core/CBoostJsonParser.h>
+#include <core/CHashing.h>
 #include <core/CLogger.h>
 
 #include <algorithm>
@@ -153,5 +154,16 @@ void CPatternSet::clear() {
     m_SuffixPatterns.clear();
     m_ContainsPatterns.clear();
 }
+
+std::uint64_t CPatternSet::checksum() const {
+    std::uint64_t result{0};
+
+    result = CHashing::hashCombine(result, m_FullMatchPatterns.checksum());
+    result = CHashing::hashCombine(result, m_PrefixPatterns.checksum());
+    result = CHashing::hashCombine(result, m_SuffixPatterns.checksum());
+    result = CHashing::hashCombine(result, m_ContainsPatterns.checksum());
+
+    return result;
+}
 }
 }
diff --git a/lib/model/CAnomalyDetectorModel.cc b/lib/model/CAnomalyDetectorModel.cc
index c39339dc6b..90f6f95de9 100644
--- a/lib/model/CAnomalyDetectorModel.cc
+++ b/lib/model/CAnomalyDetectorModel.cc
@@ -16,6 +16,7 @@
 #include <core/CMemoryDef.h>
 #include <core/CStatePersistInserter.h>
 #include <core/CStateRestoreTraverser.h>
+#include <core/CTimeUtils.h>
 
 #include <maths/common/CChecksum.h>
 #include <maths/common/CMultivariatePrior.h>
@@ -48,6 +49,7 @@ const std::string EMPTY;
 
 const model_t::CResultType SKIP_SAMPLING_RESULT_TYPE;
 const double SKIP_SAMPLING_WEIGHT{0.005};
+const core_t::TTime APPLIED_DETECTION_RULE_EXPIRATION{31536000}; // 1 year
 
 const CAnomalyDetectorModel::TStr1Vec EMPTY_STRING_LIST;
 
@@ -325,6 +327,7 @@ std::uint64_t CAnomalyDetectorModel::checksum(bool /*includeCurrentBucketStats*/
             hash = maths::common::CChecksum::calculate(hash, m_PersonBucketCounts[pid]);
         }
     }
+    seed = maths::common::CChecksum::calculate(seed, m_AppliedRuleChecksums);
     LOG_TRACE(<< "seed = " << seed);
     LOG_TRACE(<< "checksums = " << hashes);
     return maths::common::CChecksum::calculate(seed, hashes);
@@ -336,6 +339,7 @@ void CAnomalyDetectorModel::debugMemoryUsage(const core::CMemoryUsage::TMemoryUs
     core::memory_debug::dynamicSize("m_Params", m_Params, mem);
     core::memory_debug::dynamicSize("m_PersonBucketCounts", m_PersonBucketCounts, mem);
     core::memory_debug::dynamicSize("m_InfluenceCalculators", m_InfluenceCalculators, mem);
+    core::memory_debug::dynamicSize("m_AppliedRuleChecksums", m_AppliedRuleChecksums, mem);
 }
 
 std::size_t CAnomalyDetectorModel::memoryUsage() const {
@@ -343,6 +347,7 @@ std::size_t CAnomalyDetectorModel::memoryUsage() const {
     mem += core::memory::dynamicSize(m_DataGatherer);
     mem += core::memory::dynamicSize(m_PersonBucketCounts);
     mem += core::memory::dynamicSize(m_InfluenceCalculators);
+    mem += core::memory::dynamicSize(m_AppliedRuleChecksums);
     return mem;
 }
 
@@ -657,5 +662,35 @@ void CAnomalyDetectorModel::CTimeSeriesCorrelateModelAllocator::prototypePrior(
 bool CMemoryCircuitBreaker::areAllocationsAllowed() const {
     return m_ResourceMonitor->areAllocationsAllowed();
 }
+
+CAnomalyDetectorModel::TUint64TTimePrVec& CAnomalyDetectorModel::appliedRuleChecksums() {
+    return m_AppliedRuleChecksums;
+}
+
+const CAnomalyDetectorModel::TUint64TTimePrVec&
+CAnomalyDetectorModel::appliedRuleChecksums() const {
+    return m_AppliedRuleChecksums;
+}
+
+bool CAnomalyDetectorModel::checkRuleApplied(const CDetectionRule& rule) const {
+    auto checksum = rule.checksum();
+    return std::find_if(m_AppliedRuleChecksums.begin(),
+                        m_AppliedRuleChecksums.end(), [checksum](const auto& pair) {
+                            return pair.first == checksum;
+                        }) != m_AppliedRuleChecksums.end();
+}
+
+void CAnomalyDetectorModel::markRuleApplied(const CDetectionRule& rule) {
+    auto currentTime = core::CTimeUtils::now();
+    m_AppliedRuleChecksums.emplace_back(rule.checksum(), currentTime);
+
+    // Remove all rules that are older than the expiration time
+    m_AppliedRuleChecksums.erase(
+        std::remove_if(m_AppliedRuleChecksums.begin(), m_AppliedRuleChecksums.end(),
+                       [currentTime](const auto& pair) {
+                           return currentTime - pair.second > APPLIED_DETECTION_RULE_EXPIRATION;
+                       }),
+        m_AppliedRuleChecksums.end());
+}
 }
 }
diff --git a/lib/model/CCountingModel.cc b/lib/model/CCountingModel.cc
index 06fb6a34ea..dc938e9f06 100644
--- a/lib/model/CCountingModel.cc
+++ b/lib/model/CCountingModel.cc
@@ -36,6 +36,7 @@ namespace {
 const std::string WINDOW_BUCKET_COUNT_TAG("a");
 const std::string PERSON_BUCKET_COUNT_TAG("b");
 const std::string MEAN_COUNT_TAG("c");
+const std::string APPLIED_DETECTION_RULE_CHECKSUMS_TAG("d");
 // Extra data tag deprecated at model version 34
 // TODO remove on next version bump
 //const std::string EXTRA_DATA_TAG("d");
@@ -79,6 +80,8 @@ void CCountingModel::acceptPersistInserter(core::CStatePersistInserter& inserter
     core::CPersistUtils::persist(PERSON_BUCKET_COUNT_TAG,
                                  this->personBucketCounts(), inserter);
     core::CPersistUtils::persist(MEAN_COUNT_TAG, m_MeanCounts, inserter);
+    core::CPersistUtils::persist(APPLIED_DETECTION_RULE_CHECKSUMS_TAG,
+                                 this->appliedRuleChecksums(), inserter);
 }
 
 bool CCountingModel::acceptRestoreTraverser(core::CStateRestoreTraverser& traverser) {
@@ -102,6 +105,12 @@ bool CCountingModel::acceptRestoreTraverser(core::CStateRestoreTraverser& traver
                 LOG_ERROR(<< "Invalid mean counts");
                 return false;
             }
+        } else if (name == APPLIED_DETECTION_RULE_CHECKSUMS_TAG) {
+            if (core::CPersistUtils::restore(name, this->appliedRuleChecksums(),
+                                             traverser) == false) {
+                LOG_ERROR(<< "Invalid applied detection rule checksums");
+                return false;
+            }
         }
     } while (traverser.next());
 
diff --git a/lib/model/CDetectionRule.cc b/lib/model/CDetectionRule.cc
index c6f8dd3531..ec1d8687e7 100644
--- a/lib/model/CDetectionRule.cc
+++ b/lib/model/CDetectionRule.cc
@@ -8,12 +8,16 @@
  * compliance with the Elastic License 2.0 and the foregoing additional
  * limitation.
  */
+#include <model/CDetectionRule.h>
 
 #include <core/CSmallVector.h>
 #include <core/CoreTypes.h>
 
+#include <maths/common/CChecksum.h>
+
 #include <model/CAnomalyDetectorModel.h>
-#include <model/CDetectionRule.h>
+
+#include <cstdint>
 
 namespace ml {
 namespace model {
@@ -38,6 +42,10 @@ void CDetectionRule::addCondition(const CRuleCondition& condition) {
     m_Conditions.push_back(condition);
 }
 
+void CDetectionRule::clearConditions() {
+    m_Conditions.clear();
+}
+
 void CDetectionRule::setCallback(TCallback cb) {
     m_Callback = std::move(cb);
 }
@@ -74,27 +82,30 @@ void CDetectionRule::executeCallback(CAnomalyDetectorModel& model, core_t::TTime
                 return;
             }
         }
+        if (model.checkRuleApplied(*this)) {
+            return;
+        }
         m_Callback(model, time);
+
+        // Time shift rules should be applied only once
+        if (m_Action & E_TimeShift) {
+            model.markRuleApplied(*this);
+        }
     }
 }
 
 void CDetectionRule::addTimeShift(core_t::TTime timeShift) {
-    using TAnomalyDetectorPtrVec = core::CSmallVector<CAnomalyDetectorModel*, 2>;
-    this->setCallback([
-        timeShift, timeShiftApplied = TAnomalyDetectorPtrVec()
-    ](CAnomalyDetectorModel & model, core_t::TTime time) mutable {
-        if (std::find(timeShiftApplied.begin(), timeShiftApplied.end(), &model) ==
-            timeShiftApplied.end()) {
-            // When the callback is executed, the model is already in the correct time
-            // interval. Hence, we need to shift the time right away.
-            // IMPLEMENTATION DECISION: We apply the negative amount of time shift to the
-            // model. This is because the time shift is applied to the model's frame of reference
-            // and not the global time. This allows a more intuitive configuration from the user's
-            // perspective: in spring we move the clock forward, and the time shift is positive, in
-            // autumn we move the clock backward, and the time shift is negative.
-            model.shiftTime(time, -timeShift);
-            timeShiftApplied.emplace_back(&model);
-        }
+    m_Action |= E_TimeShift;
+    m_TimeShift = timeShift;
+    this->setCallback([timeShift](CAnomalyDetectorModel& model, core_t::TTime time) {
+        // When the callback is executed, the model is already in the correct time
+        // interval. Hence, we need to shift the time right away.
+        // IMPLEMENTATION DECISION: We apply the negative amount of time shift to the
+        // model. This is because the time shift is applied to the model's frame of reference
+        // and not the global time. This allows a more intuitive configuration from the user's
+        // perspective: in spring we move the clock forward, and the time shift is positive, in
+        // autumn we move the clock backward, and the time shift is negative.
+        model.shiftTime(time, -timeShift);
     });
 }
 
@@ -134,5 +145,22 @@ std::string CDetectionRule::printAction() const {
     }
     return result;
 }
+
+std::uint64_t CDetectionRule::checksum() const {
+    std::uint64_t result = maths::common::CChecksum::calculate(0, m_Action);
+    result = maths::common::CChecksum::calculate(result, m_Scope);
+    result = maths::common::CChecksum::calculate(result, m_Conditions);
+
+    // Hash callback parameters if applicable
+    if (m_Action & E_TimeShift) {
+        // Hash m_TimeShift
+        result = maths::common::CChecksum::calculate(result, m_TimeShift);
+    }
+
+    // IMPLEMENTATION NOTE: If there are other parameters associated with the callback,
+    // they should be included in the checksum.
+
+    return result;
+}
 }
 }
diff --git a/lib/model/CIndividualModel.cc b/lib/model/CIndividualModel.cc
index 33d650402e..e5b8fbafc3 100644
--- a/lib/model/CIndividualModel.cc
+++ b/lib/model/CIndividualModel.cc
@@ -67,6 +67,7 @@ const std::string FEATURE_CORRELATE_MODELS_TAG("f");
 //const std::string INTERIM_BUCKET_CORRECTOR_TAG("h");
 const std::string MEMORY_ESTIMATOR_TAG("i");
 const std::string UPGRADING_PRE_7_5_STATE("j");
+const std::string APPLIED_DETECTION_RULE_CHECKSUMS_TAG("k");
 }
 
 CIndividualModel::CIndividualModel(const SModelParams& params,
@@ -357,6 +358,8 @@ void CIndividualModel::doAcceptPersistInserter(core::CStatePersistInserter& inse
     }
     core::CPersistUtils::persist(MEMORY_ESTIMATOR_TAG, m_MemoryEstimator, inserter);
     inserter.insertValue(UPGRADING_PRE_7_5_STATE, false);
+    core::CPersistUtils::persist(APPLIED_DETECTION_RULE_CHECKSUMS_TAG,
+                                 this->appliedRuleChecksums(), inserter);
 }
 
 bool CIndividualModel::doAcceptRestoreTraverser(core::CStateRestoreTraverser& traverser) {
@@ -387,6 +390,8 @@ bool CIndividualModel::doAcceptRestoreTraverser(core::CStateRestoreTraverser& tr
         RESTORE(MEMORY_ESTIMATOR_TAG,
                 core::CPersistUtils::restore(MEMORY_ESTIMATOR_TAG, m_MemoryEstimator, traverser))
         RESTORE_BUILT_IN(UPGRADING_PRE_7_5_STATE, upgradingPre7p5State)
+        RESTORE(APPLIED_DETECTION_RULE_CHECKSUMS_TAG,
+                core::CPersistUtils::restore(name, this->appliedRuleChecksums(), traverser));
     } while (traverser.next());
 
     if (traverser.haveBadState()) {
diff --git a/lib/model/CPopulationModel.cc b/lib/model/CPopulationModel.cc
index a803815b4e..28d6358359 100644
--- a/lib/model/CPopulationModel.cc
+++ b/lib/model/CPopulationModel.cc
@@ -91,6 +91,8 @@ const std::string ATTRIBUTE_FIRST_BUCKET_TIME_TAG("d");
 const std::string ATTRIBUTE_LAST_BUCKET_TIME_TAG("e");
 const std::string PERSON_ATTRIBUTE_BUCKET_COUNT_TAG("f");
 const std::string DISTINCT_PERSON_COUNT_TAG("g");
+const std::string APPLIED_DETECTION_RULE_CHECKSUMS_TAG("h");
+
 // Extra data tag deprecated at model version 34
 // TODO remove on next version bump
 //const std::string EXTRA_DATA_TAG("h");
@@ -294,6 +296,8 @@ void CPopulationModel::doAcceptPersistInserter(core::CStatePersistInserter& inse
             std::bind(&maths::common::CBjkstUniqueValues::acceptPersistInserter,
                       &m_DistinctPersonCounts[cid], std::placeholders::_1));
     }
+    core::CPersistUtils::persist(APPLIED_DETECTION_RULE_CHECKSUMS_TAG,
+                                 this->appliedRuleChecksums(), inserter);
 }
 
 bool CPopulationModel::doAcceptRestoreTraverser(core::CStateRestoreTraverser& traverser) {
@@ -316,12 +320,17 @@ bool CPopulationModel::doAcceptRestoreTraverser(core::CStateRestoreTraverser& tr
                 maths::time_series::CCountMinSketch(0, 0));
             m_PersonAttributeBucketCounts.back().swap(sketch);
             continue;
-        }
-        if (name == DISTINCT_PERSON_COUNT_TAG) {
+        } else if (name == DISTINCT_PERSON_COUNT_TAG) {
             maths::common::CBjkstUniqueValues sketch(traverser);
             m_DistinctPersonCounts.push_back(maths::common::CBjkstUniqueValues(0, 0));
             m_DistinctPersonCounts.back().swap(sketch);
             continue;
+        } else if (name == APPLIED_DETECTION_RULE_CHECKSUMS_TAG) {
+            if (core::CPersistUtils::restore(name, this->appliedRuleChecksums(),
+                                             traverser) == false) {
+                LOG_ERROR(<< "Invalid applied detection rule checksums");
+                return false;
+            }
         }
     } while (traverser.next());
 
diff --git a/lib/model/CRuleCondition.cc b/lib/model/CRuleCondition.cc
index 6f6ba6c475..8b9a35fb0e 100644
--- a/lib/model/CRuleCondition.cc
+++ b/lib/model/CRuleCondition.cc
@@ -12,6 +12,8 @@
 #include <core/CLogger.h>
 #include <core/CStringUtils.h>
 
+#include <maths/common/CChecksum.h>
+
 #include <model/CAnomalyDetectorModel.h>
 #include <model/CRuleCondition.h>
 
@@ -164,5 +166,12 @@ std::string CRuleCondition::print(ERuleConditionOperator op) const {
     }
     return std::string();
 }
+
+std::uint64_t CRuleCondition::checksum() const {
+    std::uint64_t result{maths::common::CChecksum::calculate(0, m_AppliesTo)};
+    result = maths::common::CChecksum::calculate(result, m_Operator);
+    result = maths::common::CChecksum::calculate(result, m_Value);
+    return result;
+}
 }
 }
diff --git a/lib/model/CRuleScope.cc b/lib/model/CRuleScope.cc
index 2cd541c5c7..e239491d7f 100644
--- a/lib/model/CRuleScope.cc
+++ b/lib/model/CRuleScope.cc
@@ -13,6 +13,8 @@
 
 #include <core/CPatternSet.h>
 
+#include <maths/common/CChecksum.h>
+
 #include <model/CAnomalyDetectorModel.h>
 #include <model/CDataGatherer.h>
 
@@ -70,5 +72,15 @@ std::string CRuleScope::print() const {
     }
     return result;
 }
+
+std::uint64_t CRuleScope::checksum() const {
+    std::uint64_t result{0};
+    for (const auto& triple : m_Scope) {
+        result = maths::common::CChecksum::calculate(result, triple.first);
+        result = maths::common::CChecksum::calculate(result, triple.second.get());
+        result = maths::common::CChecksum::calculate(result, triple.third);
+    }
+    return result;
+}
 }
 }
diff --git a/lib/model/unittest/CCountingModelTest.cc b/lib/model/unittest/CCountingModelTest.cc
index e4a3a5de65..facdbde9c1 100644
--- a/lib/model/unittest/CCountingModelTest.cc
+++ b/lib/model/unittest/CCountingModelTest.cc
@@ -10,6 +10,7 @@
  */
 
 #include <core/CContainerPrinter.h>
+#include <core/CJsonStatePersistInserter.h>
 #include <core/CoreTypes.h>
 
 #include <model/CCountingModel.h>
@@ -23,6 +24,7 @@
 #include <test/CRandomNumbers.h>
 
 #include "CModelTestFixtureBase.h"
+#include "core/CJsonStateRestoreTraverser.h"
 
 #include <boost/test/unit_test.hpp>
 
@@ -263,4 +265,44 @@ BOOST_FIXTURE_TEST_CASE(testInterimBucketCorrector, CTestFixture) {
     }
 }
 
+BOOST_FIXTURE_TEST_CASE(testAppliedRuleChecksumsPersistRestore, CTestFixture) {
+    // Check that applied rule checksums are persisted and restored correctly.
+
+    core_t::TTime time{200};
+    core_t::TTime bucketLength{600};
+
+    SModelParams params(bucketLength);
+    params.s_DecayRate = 0.001;
+
+    this->makeModel(params, {model_t::E_IndividualCountByBucketAndPerson}, time);
+    CCountingModel* model = dynamic_cast<CCountingModel*>(m_Model.get());
+    BOOST_TEST_REQUIRE(model);
+
+    // Create a time shift detection rule and apply it
+    CRuleCondition conditionGte;
+    conditionGte.appliesTo(CRuleCondition::E_Time);
+    conditionGte.op(CRuleCondition::E_GTE);
+    conditionGte.value(100.0);
+
+    CDetectionRule rule;
+    rule.addCondition(conditionGte);
+    rule.addTimeShift(100);
+    rule.executeCallback(*model, time);
+
+    // Persist the model with CCountingModel::acceptPersistInserter
+    std::ostringstream persistStream;
+    core::CJsonStatePersistInserter inserter(persistStream);
+    model->acceptPersistInserter(inserter);
+    std::string persist = persistStream.str();
+
+    // Restore the model with CCountingModel::acceptRestoreTraversal
+    std::istringstream restoreStream(persist);
+    core::CJsonStateRestoreTraverser traverser(restoreStream);
+    auto restoredModel = std::make_shared<CCountingModel>(
+        params, m_Gatherer, m_InterimBucketCorrector, traverser);
+
+    // Check that for the restored model the rule is marked as applied
+    BOOST_REQUIRE(model->checkRuleApplied(rule) == true);
+}
+
 BOOST_AUTO_TEST_SUITE_END()
diff --git a/lib/model/unittest/CDetectionRuleTest.cc b/lib/model/unittest/CDetectionRuleTest.cc
index ec41d0a85c..c4df2755f4 100644
--- a/lib/model/unittest/CDetectionRuleTest.cc
+++ b/lib/model/unittest/CDetectionRuleTest.cc
@@ -1095,4 +1095,99 @@ BOOST_FIXTURE_TEST_CASE(testTwoTimeShiftRuleShouldShiftTwice, CTestFixture) {
     BOOST_TEST_REQUIRE(trendModel.timeShift() == -(timeShift1 + timeShift2));
 }
 
+BOOST_FIXTURE_TEST_CASE(testChecksum, CTestFixture) {
+    // Create two identical rules
+    CDetectionRule rule1;
+    CDetectionRule rule2;
+
+    // Compute checksums
+    std::uint64_t checksum1 = rule1.checksum();
+    std::uint64_t checksum2 = rule2.checksum();
+
+    // Verify that identical rules have the same checksum
+    BOOST_REQUIRE_EQUAL(checksum1, checksum2);
+
+    // Test actions
+    // Modify the action of rule2
+    rule2.action(CDetectionRule::E_SkipModelUpdate);
+
+    // Verify that different actions result in different checksums
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_NE(checksum1, checksum2);
+
+    // Test conditions
+    // Reset rule2 to be identical to rule1
+    rule2 = rule1;
+
+    // Add a condition to rule2
+    CRuleCondition condition;
+    condition.appliesTo(CRuleCondition::E_Actual);
+    condition.op(CRuleCondition::E_GT);
+    condition.value(100.0);
+    rule2.addCondition(condition);
+
+    // Verify that adding a condition changes the checksum
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_NE(checksum1, checksum2);
+
+    // Add the same condition to rule1
+    rule1.addCondition(condition);
+
+    // Verify that identical conditions result in the same checksum
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_EQUAL(checksum1, checksum2);
+
+    // Modify the condition in rule2
+    condition.value(200.0);
+    rule2.clearConditions();
+    rule2.addCondition(condition);
+
+    // Verify that different condition values result in different checksums
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_NE(checksum1, checksum2);
+
+    // Test Scope
+    rule2 = rule1;
+
+    // Modify the scope of rule2
+    std::string fieldName = "user";
+    core::CPatternSet valueFilter;
+    valueFilter.initFromPatternList({"admin"});
+    rule2.includeScope(fieldName, valueFilter);
+
+    // Verify that different scopes result in different checksums
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_NE(checksum1, checksum2);
+
+    // Add the same scope to rule1
+    rule1.includeScope(fieldName, valueFilter);
+
+    // Verify that identical scopes result in the same checksum
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_EQUAL(checksum1, checksum2);
+
+    // Test Time Shift
+    // Modify the time shift in rule2
+    rule2.addTimeShift(3600);
+
+    // Verify that different time shifts result in different checksums
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_NE(checksum1, checksum2);
+
+    // Add the same time shift to rule1
+    rule1.addTimeShift(3600);
+
+    // Verify that identical time shifts result in the same checksum
+    checksum1 = rule1.checksum();
+    checksum2 = rule2.checksum();
+    BOOST_REQUIRE_EQUAL(checksum1, checksum2);
+}
+
 BOOST_AUTO_TEST_SUITE_END()

From 86faea9d791e3461e3e76f7b41b9caa8da9403f5 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Sat, 28 Sep 2024 08:53:26 +1200
Subject: [PATCH 10/27] [8.16][ML] QA tests on specific branch and version
 (#2742) (#2758)

Enable being able to specify a particular ES branch and stack version
when running QA tests on a PR. The syntax of the GitHub comment to do
this is, e.g.:

```
buildkite run_qa_tests for ES_BRANCH=8.x with STACK_VERSION=8.16.0
```

Backports #2742
---
 .buildkite/ml_pipeline/config.py         | 13 +++++++++++++
 .buildkite/pipelines/run_qa_tests.yml.sh | 14 +++++++++++++-
 .buildkite/pull-requests.json            |  2 +-
 3 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/.buildkite/ml_pipeline/config.py b/.buildkite/ml_pipeline/config.py
index 363d6826ef..4669ce8b58 100644
--- a/.buildkite/ml_pipeline/config.py
+++ b/.buildkite/ml_pipeline/config.py
@@ -40,6 +40,19 @@ def parse_comment(self):
             if self.run_pytorch_tests or self.run_qa_tests:
                 self.action = "build"
 
+        # If the ACTION is set to "run_qa_tests" then set some optional variables governing the ES branch to build, the
+        # stack version to set and the subset of QA tests to run, depending on whether appropriate variables are set in
+        # the environment.
+        if self.run_qa_tests:
+            if "GITHUB_PR_COMMENT_VAR_BRANCH" in os.environ:
+                os.environ["ES_BRANCH"] = os.environ["GITHUB_PR_COMMENT_VAR_BRANCH"]
+
+            if "GITHUB_PR_COMMENT_VAR_VERSION" in os.environ:
+                os.environ["STACK_VERSION"] = os.environ["GITHUB_PR_COMMENT_VAR_VERSION"]
+
+            if "GITHUB_PR_COMMENT_VAR_ARGS" in os.environ:
+                os.environ["QAF_TESTS_TO_RUN"] = os.environ["GITHUB_PR_COMMENT_VAR_ARGS"]
+
         # If the GITHUB_PR_COMMENT_VAR_ARCH environment variable is set then   attemot to parse it
         # into comma separated values. If the values are one or both of "aarch64" or "x86_64" then set the member
         # variables self.build_aarch64, self.build_x86_64 accordingly. These values will be used to restrict the build
diff --git a/.buildkite/pipelines/run_qa_tests.yml.sh b/.buildkite/pipelines/run_qa_tests.yml.sh
index 5534588ea3..5cff0aed85 100755
--- a/.buildkite/pipelines/run_qa_tests.yml.sh
+++ b/.buildkite/pipelines/run_qa_tests.yml.sh
@@ -24,5 +24,17 @@ steps:
     build:
       message: "${BUILDKITE_MESSAGE}"
       env:
-        QAF_TESTS_TO_RUN: "ml_cpp_pr"
+        QAF_TESTS_TO_RUN: "${QAF_TESTS_TO_RUN:-ml_cpp_pr}"
 EOL
+
+if [ "${ES_BRANCH}" != "" ]; then
+cat <<EOL
+        ES_BRANCH: "${ES_BRANCH}"
+EOL
+fi
+
+if [ "${STACK_VERSION}" != "" ]; then
+cat <<EOL
+        STACK_VERSION: "${STACK_VERSION}"
+EOL
+fi
diff --git a/.buildkite/pull-requests.json b/.buildkite/pull-requests.json
index 63198fb2b7..dcea827947 100644
--- a/.buildkite/pull-requests.json
+++ b/.buildkite/pull-requests.json
@@ -9,7 +9,7 @@
       "commit_status_context": "ml-cpp-ci",
       "build_on_commit": true,
       "build_on_comment": true,
-      "trigger_comment_regex": "^(?:(?:buildkite +)(?<action>build|debug|run_qa_tests|run_pytorch_tests) *(?: *on *(?<platform>(?:[ ,]*(?:windows|linux|mac(os)?))+))?) *(?<arch>(?:[ ,]*aarch64|x86_64)+)?$",
+      "trigger_comment_regex": "^(?:(?:buildkite +)(?<action>build|debug|run_qa_tests|run_pytorch_tests)(=(?<args>(?:[^ ]+)))? *(?: for ES_BRANCH=(?<branch>([.0-9a-zA-Z]+)))? *(?:with STACK_VERSION=(?<version>([.0-9]+)))? *(?: *on *(?<platform>(?:[ ,]*(?:windows|linux|mac(os)?))+))?) *(?<arch>(?:[, ]*aarch64|x86_64)+)?$",
       "always_trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))",
       "skip_ci_labels": ["skip-ci", "jenkins-ci", ">test-mute", ">docs"],
       "skip_target_branches": ["6.8", "7.11", "7.12"],

From 73bd200a64a8a8dbdcee97cbf6d94b871f7eb358 Mon Sep 17 00:00:00 2001
From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
Date: Fri, 11 Oct 2024 09:42:04 +0200
Subject: [PATCH 11/27] Revert "[ML] Change time shift sign for intuitive
 configuration (#2737) (#2743)" (#2761)

This reverts commit 0bc01ba0e12e9bc8c634d096e79f740df621bf80.
---
 lib/model/CDetectionRule.cc              | 7 +------
 lib/model/unittest/CDetectionRuleTest.cc | 8 ++++----
 2 files changed, 5 insertions(+), 10 deletions(-)

diff --git a/lib/model/CDetectionRule.cc b/lib/model/CDetectionRule.cc
index ec1d8687e7..0a4fa6de6c 100644
--- a/lib/model/CDetectionRule.cc
+++ b/lib/model/CDetectionRule.cc
@@ -100,12 +100,7 @@ void CDetectionRule::addTimeShift(core_t::TTime timeShift) {
     this->setCallback([timeShift](CAnomalyDetectorModel& model, core_t::TTime time) {
         // When the callback is executed, the model is already in the correct time
         // interval. Hence, we need to shift the time right away.
-        // IMPLEMENTATION DECISION: We apply the negative amount of time shift to the
-        // model. This is because the time shift is applied to the model's frame of reference
-        // and not the global time. This allows a more intuitive configuration from the user's
-        // perspective: in spring we move the clock forward, and the time shift is positive, in
-        // autumn we move the clock backward, and the time shift is negative.
-        model.shiftTime(time, -timeShift);
+        model.shiftTime(time, timeShift);
     });
 }
 
diff --git a/lib/model/unittest/CDetectionRuleTest.cc b/lib/model/unittest/CDetectionRuleTest.cc
index c4df2755f4..271ce6849e 100644
--- a/lib/model/unittest/CDetectionRuleTest.cc
+++ b/lib/model/unittest/CDetectionRuleTest.cc
@@ -1020,8 +1020,8 @@ BOOST_FIXTURE_TEST_CASE(testRuleTimeShiftShouldShiftTimeSeriesModelState, CTestF
         rule.executeCallback(*model, timestamp);
 
         // the time series model should have been shifted by specified amount.
-        BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTime - timeShiftInSecs);
-        BOOST_TEST_REQUIRE(trendModel.timeShift() == -timeShiftInSecs);
+        BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTime + timeShiftInSecs);
+        BOOST_TEST_REQUIRE(trendModel.timeShift() == timeShiftInSecs);
 
         // and an annotation should have been added to the model
         BOOST_TEST_REQUIRE(annotations.size() == numAnnotationsBeforeShift + 1);
@@ -1091,8 +1091,8 @@ BOOST_FIXTURE_TEST_CASE(testTwoTimeShiftRuleShouldShiftTwice, CTestFixture) {
     // the values after the second time should be the sum of two rules.
     timestamp += timeShift1; // simulate the time has moved forward by the time shift
     rule2.executeCallback(*model, timestamp);
-    BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTimeAfterFirstShift - timeShift2);
-    BOOST_TEST_REQUIRE(trendModel.timeShift() == -(timeShift1 + timeShift2));
+    BOOST_TEST_REQUIRE(trendModel.lastValueTime() == lastValueTimeAfterFirstShift + timeShift2);
+    BOOST_TEST_REQUIRE(trendModel.timeShift() == timeShift1 + timeShift2);
 }
 
 BOOST_FIXTURE_TEST_CASE(testChecksum, CTestFixture) {

From e8ba5ef98f9e7b0db450fae145becd14c6b97cae Mon Sep 17 00:00:00 2001
From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
Date: Tue, 15 Oct 2024 09:34:44 +0200
Subject: [PATCH 12/27] [ML] Fix forecasting parameters initialization for
 large models (#2759) (#2762)

We fix initialization of the decayRate parameter when models have to spill to disk
---
 docs/CHANGELOG.asciidoc                         | 6 ++++++
 lib/model/CForecastModelPersist.cc              | 6 +-----
 lib/model/unittest/CForecastModelPersistTest.cc | 6 ++++--
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc
index 9c1d528166..32af915f1e 100644
--- a/docs/CHANGELOG.asciidoc
+++ b/docs/CHANGELOG.asciidoc
@@ -39,6 +39,12 @@
 
 * Allow for pytorch_inference results to include zero-dimensional tensors.
 
+== {es} version 8.15.4
+
+=== Bug Fixes
+
+* Fix parameter initialization for large forecasting models. (See {ml-pull}2759[#2759].)
+
 == {es} version 8.15.2
 
 === Enhancements
diff --git a/lib/model/CForecastModelPersist.cc b/lib/model/CForecastModelPersist.cc
index 0f59a09a50..51d353c6ff 100644
--- a/lib/model/CForecastModelPersist.cc
+++ b/lib/model/CForecastModelPersist.cc
@@ -128,11 +128,7 @@ bool CForecastModelPersist::CRestore::nextModel(TMathsModelPtr& model,
                     m_ModelParams.s_MaximumTimeToTestForChange};
 
                 maths::common::SModelRestoreParams params{
-                    modelParams,
-                    maths::common::STimeSeriesDecompositionRestoreParams{
-                        m_ModelParams.s_DecayRate, m_ModelParams.s_BucketLength,
-                        m_ModelParams.s_ComponentSize,
-                        m_ModelParams.distributionRestoreParams(dataType)},
+                    modelParams, m_ModelParams.decompositionRestoreParams(dataType),
                     m_ModelParams.distributionRestoreParams(dataType)};
 
                 auto serialiserOperator =
diff --git a/lib/model/unittest/CForecastModelPersistTest.cc b/lib/model/unittest/CForecastModelPersistTest.cc
index b932b1423f..d0aa03af22 100644
--- a/lib/model/unittest/CForecastModelPersistTest.cc
+++ b/lib/model/unittest/CForecastModelPersistTest.cc
@@ -18,6 +18,7 @@
 #include <maths/time_series/CTimeSeriesDecomposition.h>
 #include <maths/time_series/CTimeSeriesModel.h>
 
+#include <model/CAnomalyDetectorModelConfig.h>
 #include <model/CForecastModelPersist.h>
 
 #include <test/BoostTestPointerOutput.h>
@@ -39,8 +40,9 @@ BOOST_AUTO_TEST_CASE(testPersistAndRestore) {
     params.s_DecayRate = 0.001;
     params.s_LearnRate = 1.0;
     params.s_MinimumTimeToDetectChange = 6 * core::constants::HOUR;
-    params.s_MaximumTimeToTestForChange = core::constants::DAY;
-    maths::time_series::CTimeSeriesDecomposition trend(params.s_DecayRate, bucketLength);
+    double trendDecayRate{CAnomalyDetectorModelConfig::trendDecayRate(
+        params.s_DecayRate, bucketLength)};
+    maths::time_series::CTimeSeriesDecomposition trend(trendDecayRate, bucketLength);
 
     maths::common::CNormalMeanPrecConjugate prior{
         maths::common::CNormalMeanPrecConjugate::nonInformativePrior(

From 148f2c42652bddc38224ef8bf50992647d38ca9a Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Wed, 16 Oct 2024 16:10:15 +0200
Subject: [PATCH 13/27] [ML] Preparing for 8.16 feature freeze (#2766)

---
 catalog-info.yaml | 14 +++++++++-----
 gradle.properties |  2 +-
 2 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/catalog-info.yaml b/catalog-info.yaml
index cb3098d8a4..195e681d24 100644
--- a/catalog-info.yaml
+++ b/catalog-info.yaml
@@ -156,7 +156,7 @@ spec:
         build_branches: true
         build_pull_request_forks: false
         cancel_deleted_branch_builds: true
-        filter_condition: build.branch == "main" || build.branch == "8.15" || build.branch == "7.17"
+        filter_condition: build.branch == "main" || build.branch == "8.x" || build.branch == "8.16" || build.branch == "7.17"
         filter_enabled: true
         publish_blocked_as_pending: true
         publish_commit_status: false
@@ -166,12 +166,16 @@ spec:
       schedules:
         Daily 7_17:
           branch: '7.17'
-          cronline: 30 02 * * *
+          cronline: 30 03 * * *
           message: Daily SNAPSHOT build for 7.17 
-        Daily 8_15:
-          branch: '8.15'
+        Daily 8_16:
+          branch: '8.16'
+          cronline: 30 02 * * *
+          message: Daily SNAPSHOT build for 8.16
+        Daily 8_x:
+          branch: '8.x'
           cronline: 30 01 * * *
-          message: Daily SNAPSHOT build for 8.15
+          message: Daily SNAPSHOT build for 8.x
         Daily main:
           branch: main
           cronline: 30 00 * * *
diff --git a/gradle.properties b/gradle.properties
index cf665d57c6..a72fdabb1b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.16.0
+elasticsearchVersion=8.17.0
 
 artifactName=ml-cpp
 

From ef1d04f0279dc65ee8d301ab9be476ef92ddb91a Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Thu, 12 Dec 2024 15:29:08 +0100
Subject: [PATCH 14/27] [ML] Bump version to 8.17.1

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index a72fdabb1b..9d6f9eb855 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.0
+elasticsearchVersion=8.17.1
 
 artifactName=ml-cpp
 

From 75c3c0ff7b2daa516153ad2d4bfabeba221a1d11 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Tue, 14 Jan 2025 14:14:46 +1300
Subject: [PATCH 15/27] [ML] Increase the upper limits for the Boost.JSON SAX
 parser (#2809) (#2812)

The maximum size limits for several features of the Boost.JSON SAX style parser are currently set to arbitrary "largeish" values. This PR increases those upper limits to be as large as possible.

Closes #2808
---
 docs/CHANGELOG.asciidoc           | 6 ++++++
 include/core/BoostJsonConstants.h | 9 +++++----
 2 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc
index 32af915f1e..1912af2970 100644
--- a/docs/CHANGELOG.asciidoc
+++ b/docs/CHANGELOG.asciidoc
@@ -28,6 +28,12 @@
 
 //=== Regressions
 
+== {es} version 8.16.4
+
+=== Bug Fixes
+
+* Increase the upper limits for the Boost.JSON SAX parser. (See {ml-pull}2809[#2809].)
+
 == {es} version 8.16.0
 
 === Enhancements
diff --git a/include/core/BoostJsonConstants.h b/include/core/BoostJsonConstants.h
index 3bc75f33d9..059f0aa499 100644
--- a/include/core/BoostJsonConstants.h
+++ b/include/core/BoostJsonConstants.h
@@ -13,6 +13,7 @@
 #define INCLUDED_ml_core_CBoostJsonConstants_h
 
 #include <cstddef>
+#include <limits>
 
 namespace ml {
 namespace core {
@@ -21,16 +22,16 @@ namespace boost_json_constants {
 // Constants that set upper limits for Boost.JSON SAX style parsing
 
 // The maximum number of elements allowed in an object
-constexpr std::size_t MAX_OBJECT_SIZE = 1'000'000;
+constexpr std::size_t MAX_OBJECT_SIZE = std::numeric_limits<std::size_t>::max();
 
 // The maximum number of elements allowed in an array
-constexpr std::size_t MAX_ARRAY_SIZE = 1'000'000;
+constexpr std::size_t MAX_ARRAY_SIZE = std::numeric_limits<std::size_t>::max();
 
 // The maximum number of characters allowed in a key
-constexpr std::size_t MAX_KEY_SIZE = 1 << 10;
+constexpr std::size_t MAX_KEY_SIZE = std::numeric_limits<std::size_t>::max();
 
 // The maximum number of characters allowed in a string
-constexpr std::size_t MAX_STRING_SIZE = 1 << 30;
+constexpr std::size_t MAX_STRING_SIZE = std::numeric_limits<std::size_t>::max();
 }
 }
 }

From 002b4653bd15a4505693e4e2ecc6c32556f240b9 Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 21 Jan 2025 09:10:52 +0100
Subject: [PATCH 16/27] [ML] Bump version to 8.17.2

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 9d6f9eb855..9214902cac 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.1
+elasticsearchVersion=8.17.2
 
 artifactName=ml-cpp
 

From 613f2dcc0c1dd365c9c99631406d977cb0ba05de Mon Sep 17 00:00:00 2001
From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
Date: Tue, 11 Feb 2025 09:38:37 +0100
Subject: [PATCH 17/27] [ML] Bump version to 8.17.3

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 9214902cac..e0f69976ae 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.2
+elasticsearchVersion=8.17.3
 
 artifactName=ml-cpp
 

From fb44037aca8d111cf2e14143096788831d38f7ed Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 4 Mar 2025 09:08:13 +0100
Subject: [PATCH 18/27] [ML] Bump version to 8.17.4

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index e0f69976ae..ad6c9d5d90 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.3
+elasticsearchVersion=8.17.4
 
 artifactName=ml-cpp
 

From 10e51f3fb65c2c00d2fba92ebf08cdd374354a51 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Fri, 7 Mar 2025 09:06:26 +1300
Subject: [PATCH 19/27] [8.17][ML] Correct handling of config updates (#2821)
 (#2825)

* Enable updating of custom rules
* Correctly update filters (scoped rules)

These changes fix a bug whereby memory corruption was caused when updating filters for an open job. The bug had previously been masked by code that reset the job's analysis config to its original state after every config update - the downside of this was that custom rules could not be updated on the fly, only by stopping and restarting the job.

Backports #2821
---
 build.gradle                                  |   6 +-
 docs/CHANGELOG.asciidoc                       |   6 +
 include/api/CAnomalyJob.h                     |   2 +
 include/api/CAnomalyJobConfig.h               |  27 ++-
 lib/api/CAnomalyJob.cc                        |   2 -
 lib/api/CAnomalyJobConfig.cc                  |  22 ---
 lib/api/CConfigUpdater.cc                     |  72 ++++---
 lib/api/unittest/CAnomalyJobConfigTest.cc     |  45 -----
 lib/api/unittest/CAnomalyJobTest.cc           | 184 ++++++++++++++++++
 lib/api/unittest/CTestAnomalyJob.cc           |  22 +++
 lib/api/unittest/CTestAnomalyJob.h            |   2 +
 .../testfiles/count_over_ip_config.json       |  44 +++++
 lib/api/unittest/testfiles/eventConfig.json   |   4 +
 lib/api/unittest/testfiles/filterConfig.json  |  11 ++
 14 files changed, 333 insertions(+), 116 deletions(-)
 create mode 100644 lib/api/unittest/testfiles/count_over_ip_config.json
 create mode 100644 lib/api/unittest/testfiles/eventConfig.json
 create mode 100644 lib/api/unittest/testfiles/filterConfig.json

diff --git a/build.gradle b/build.gradle
index f9ae0d56c8..fea0a7da31 100644
--- a/build.gradle
+++ b/build.gradle
@@ -435,7 +435,7 @@ task buildDependencyReport(type: Exec) {
 // This gives us the flexibility to build in different
 // ways and still use the same upload code.
 task upload(type: UploadS3Task) {
-  bucket 'prelert-artifacts'
+  bucket='prelert-artifacts'
   // Only upload the platform-specific artifacts in this task
   def zipFileDir = fileTree("${buildDir}/distributions").matching {
     include "*-aarch64.zip", "*-x86_64.zip"
@@ -447,7 +447,7 @@ task upload(type: UploadS3Task) {
 }
 
 task uploadAll(type: UploadS3Task) {
-  bucket 'prelert-artifacts'
+  bucket='prelert-artifacts'
   // Upload ALL artifacts (including the dependency report) in this task
   def fileDir = fileTree("${buildDir}/distributions").matching {
     include "ml-cpp-${project.version}*.zip", "dependencies-${version}.csv"
@@ -462,7 +462,7 @@ task uberUpload(type: UploadS3Task, dependsOn: [buildUberZipFromDownloads,
                                                 buildDependenciesZipFromDownloads,
                                                 buildNoDependenciesZipFromDownloads,
                                                 buildDependencyReport]) {
-  bucket 'prelert-artifacts'
+  bucket='prelert-artifacts'
   upload buildUberZipFromDownloads.outputs.files.singleFile, "maven/${artifactGroupPath}/${artifactName}/${project.version}/${buildUberZipFromDownloads.outputs.files.singleFile.name}"
   upload buildDependenciesZipFromDownloads.outputs.files.singleFile, "maven/${artifactGroupPath}/${artifactName}/${project.version}/${buildDependenciesZipFromDownloads.outputs.files.singleFile.name}"
   upload buildNoDependenciesZipFromDownloads.outputs.files.singleFile, "maven/${artifactGroupPath}/${artifactName}/${project.version}/${buildNoDependenciesZipFromDownloads.outputs.files.singleFile.name}"
diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc
index 1912af2970..2d41089a31 100644
--- a/docs/CHANGELOG.asciidoc
+++ b/docs/CHANGELOG.asciidoc
@@ -28,6 +28,12 @@
 
 //=== Regressions
 
+== {es} version 8.16.6
+
+=== Bug Fixes
+
+* Correct handling of config updates. (See {ml-pull}2821[#2821].)
+
 == {es} version 8.16.4
 
 === Bug Fixes
diff --git a/include/api/CAnomalyJob.h b/include/api/CAnomalyJob.h
index e4f1f452cb..fda2d59972 100644
--- a/include/api/CAnomalyJob.h
+++ b/include/api/CAnomalyJob.h
@@ -37,6 +37,7 @@
 
 namespace CAnomalyJobTest {
 struct testParsePersistControlMessageArgs;
+struct testConfigUpdate;
 struct testOutputBucketResultsUntilGivenIncompleteInitialBucket;
 }
 
@@ -521,6 +522,7 @@ class API_EXPORT CAnomalyJob : public CDataProcessor {
     core_t::TTime m_InitialLastFinalisedBucketEndTime{0};
 
     // Test case access
+    friend struct CAnomalyJobTest::testConfigUpdate;
     friend struct CAnomalyJobTest::testParsePersistControlMessageArgs;
 
     friend struct CAnomalyJobTest::testOutputBucketResultsUntilGivenIncompleteInitialBucket;
diff --git a/include/api/CAnomalyJobConfig.h b/include/api/CAnomalyJobConfig.h
index 3fe56a5402..7f9cae0edd 100644
--- a/include/api/CAnomalyJobConfig.h
+++ b/include/api/CAnomalyJobConfig.h
@@ -239,7 +239,19 @@ class API_EXPORT CAnomalyJobConfig {
         }
 
         void initRuleFilters(const CDetectionRulesJsonParser::TStrPatternSetUMap& ruleFilters) {
-            m_RuleFilters = ruleFilters;
+            // Update or insert values that are in the new map - we never delete filters at this level.
+            // Note that we can't simply assign "m_RuleFilters = ruleFilters", as that would result in
+            // the pattern set objects being destroyed and, as they are referenced by the anomaly detector models,
+            // this is a bad thing.
+            for (const auto& kv : ruleFilters) {
+                CDetectionRulesJsonParser::TStrPatternSetUMap::iterator itr =
+                    m_RuleFilters.find(kv.first);
+                if (itr != m_RuleFilters.end()) {
+                    itr->second = kv.second;
+                } else {
+                    m_RuleFilters.insert(kv);
+                }
+            }
         }
 
         void initScheduledEvents(const TStrDetectionRulePrVec& scheduledEvents) {
@@ -249,19 +261,6 @@ class API_EXPORT CAnomalyJobConfig {
         //! Parse a JSON value representing an entire analysis config object.
         void parse(const json::value& json);
 
-        //! Return a JSON string representing the analysis config
-        const std::string& getAnalysisConfig();
-
-        //! Reparse the detector configuration object from within a stored
-        //! string representing the analysis config object.
-        //! This is necessary to correctly reinitialise scoped rule objects
-        //! folowing an update of the fiter rules configuration.
-        bool reparseDetectorsFromStoredConfig(const std::string& analysisConfig);
-
-        void setConfig(const std::string& analysisConfigString) {
-            m_AnalysisConfigString = analysisConfigString;
-        }
-
         core_t::TTime bucketSpan() const { return m_BucketSpan; }
 
         //! Return the size of the model prune window expressed as a whole number of seconds.
diff --git a/lib/api/CAnomalyJob.cc b/lib/api/CAnomalyJob.cc
index 82757df65e..ad8242867a 100644
--- a/lib/api/CAnomalyJob.cc
+++ b/lib/api/CAnomalyJob.cc
@@ -474,8 +474,6 @@ void CAnomalyJob::updateConfig(const std::string& config) {
     if (configUpdater.update(config) == false) {
         LOG_ERROR(<< "Failed to update configuration");
     }
-    const std::string& analysisConfig = m_JobConfig.analysisConfig().getAnalysisConfig();
-    m_JobConfig.analysisConfig().reparseDetectorsFromStoredConfig(analysisConfig);
 }
 
 void CAnomalyJob::advanceTime(const std::string& time_) {
diff --git a/lib/api/CAnomalyJobConfig.cc b/lib/api/CAnomalyJobConfig.cc
index 9d05bc53c6..41cd8791d5 100644
--- a/lib/api/CAnomalyJobConfig.cc
+++ b/lib/api/CAnomalyJobConfig.cc
@@ -589,7 +589,6 @@ bool CAnomalyJobConfig::parse(const std::string& jsonStr) {
 
         auto analysisConfig = parameters[ANALYSIS_CONFIG].jsonObject();
         if (analysisConfig != nullptr) {
-            m_AnalysisConfig.setConfig(toString(*analysisConfig));
             m_AnalysisConfig.parse(*analysisConfig);
         }
 
@@ -724,27 +723,6 @@ void CAnomalyJobConfig::CAnalysisConfig::parseDetectorsConfig(const json::value&
     }
 }
 
-const std::string& CAnomalyJobConfig::CAnalysisConfig::getAnalysisConfig() {
-    return m_AnalysisConfigString;
-}
-
-bool CAnomalyJobConfig::CAnalysisConfig::reparseDetectorsFromStoredConfig(const std::string& analysisConfig) {
-    json::value doc;
-    bool ok = core::CBoostJsonParser::parse(analysisConfig, doc);
-    if (ok == false) {
-        LOG_ERROR(<< "An error occurred while parsing anomaly job config from JSON: \""
-                  << analysisConfig << "\"");
-        return false;
-    }
-
-    auto parameters = ANALYSIS_CONFIG_READER.read(doc);
-    auto detectorsConfig = parameters[DETECTORS].jsonObject();
-    if (detectorsConfig != nullptr) {
-        this->parseDetectorsConfig(*detectorsConfig);
-    }
-    return true;
-}
-
 void CAnomalyJobConfig::CAnalysisConfig::parse(const json::value& analysisConfig) {
     auto parameters = ANALYSIS_CONFIG_READER.read(analysisConfig);
     // We choose to ignore any errors here parsing the time duration string as
diff --git a/lib/api/CConfigUpdater.cc b/lib/api/CConfigUpdater.cc
index 17cb847572..dc3ae7e288 100644
--- a/lib/api/CConfigUpdater.cc
+++ b/lib/api/CConfigUpdater.cc
@@ -41,41 +41,53 @@ bool CConfigUpdater::update(const std::string& json) {
     }
 
     json::object obj = doc.as_object();
+    for (const auto& kv : obj) {
+        if (kv.key() == CAnomalyJobConfig::MODEL_PLOT_CONFIG) {
+            LOG_TRACE(<< "Updating model plot config");
 
-    if (obj.contains(CAnomalyJobConfig::MODEL_PLOT_CONFIG)) {
-        if (obj[CAnomalyJobConfig::MODEL_PLOT_CONFIG].is_object() == false) {
-            LOG_ERROR(<< "Input error: expected " << CAnomalyJobConfig::MODEL_PLOT_CONFIG
-                      << " to be JSON object but input was '" << json
-                      << "'. Please report this problem.");
-            return false;
-        }
-        const json::value& value = obj[CAnomalyJobConfig::MODEL_PLOT_CONFIG];
+            if (kv.value().is_object() == false) {
+                LOG_ERROR(<< "Input error: expected " << CAnomalyJobConfig::MODEL_PLOT_CONFIG
+                          << " to be JSON object but input was '" << json
+                          << "'. Please report this problem.");
+                return false;
+            }
 
-        m_JobConfig.modelPlotConfig().parse(value);
-        const ml::api::CAnomalyJobConfig::CModelPlotConfig& modelPlotConfig =
-            m_JobConfig.modelPlotConfig();
-        m_ModelConfig.configureModelPlot(modelPlotConfig.enabled(),
-                                         modelPlotConfig.annotationsEnabled(),
-                                         modelPlotConfig.terms());
-    } else if (obj.contains(CAnomalyJobConfig::FILTERS)) {
-        if (m_JobConfig.parseFilterConfig(json) == false) {
-            LOG_ERROR(<< "Failed to parse filter config update: " << json);
-            return false;
-        }
-        m_JobConfig.initRuleFilters();
-    } else if (obj.contains(CAnomalyJobConfig::EVENTS)) {
-        if (m_JobConfig.parseEventConfig(json) == false) {
-            LOG_ERROR(<< "Failed to parse events config update: " << json);
+            m_JobConfig.modelPlotConfig().parse(kv.value());
+            const ml::api::CAnomalyJobConfig::CModelPlotConfig& modelPlotConfig =
+                m_JobConfig.modelPlotConfig();
+            m_ModelConfig.configureModelPlot(modelPlotConfig.enabled(),
+                                             modelPlotConfig.annotationsEnabled(),
+                                             modelPlotConfig.terms());
+        } else if (kv.key() == CAnomalyJobConfig::FILTERS) {
+            LOG_TRACE(<< "Updating filters config");
+
+            if (m_JobConfig.parseFilterConfig(json) == false) {
+                LOG_ERROR(<< "Failed to parse filter config update: " << json);
+                return false;
+            }
+            LOG_TRACE(<< "Calling m_JobConfig.initRuleFilters");
+
+            m_JobConfig.initRuleFilters();
+
+            LOG_TRACE(<< "Done calling m_JobConfig.initRuleFilters");
+
+        } else if (kv.key() == CAnomalyJobConfig::EVENTS) {
+            LOG_TRACE(<< "Updating events config");
+
+            if (m_JobConfig.parseEventConfig(json) == false) {
+                LOG_ERROR(<< "Failed to parse events config update: " << json);
+                return false;
+            }
+            m_JobConfig.initScheduledEvents();
+        } else if (kv.key() == CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::DETECTOR_RULES) {
+            LOG_TRACE(<< "Updating detector rules config");
+            return m_JobConfig.analysisConfig().parseRulesUpdate(kv.value());
+        } else {
+            LOG_ERROR(<< "Unexpected JSON update message: " << json);
             return false;
         }
-        m_JobConfig.initScheduledEvents();
-    } else if (obj.contains(CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::DETECTOR_RULES)) {
-        return m_JobConfig.analysisConfig().parseRulesUpdate(
-            obj[CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::DETECTOR_RULES]);
-    } else {
-        LOG_ERROR(<< "Unexpected JSON update message: " << json);
-        return false;
     }
+
     return true;
 }
 }
diff --git a/lib/api/unittest/CAnomalyJobConfigTest.cc b/lib/api/unittest/CAnomalyJobConfigTest.cc
index 3699baa6f4..78b8fdcb07 100644
--- a/lib/api/unittest/CAnomalyJobConfigTest.cc
+++ b/lib/api/unittest/CAnomalyJobConfigTest.cc
@@ -57,51 +57,6 @@ BOOST_AUTO_TEST_CASE(testIntervalStagger) {
     BOOST_REQUIRE_EQUAL(job3Config.intervalStagger(), job1Config.intervalStagger());
 }
 
-BOOST_AUTO_TEST_CASE(testReparseDetectorsFromStoredConfig) {
-    const std::string validAnomalyJobConfigWithCustomRuleFilter{
-        "{\"job_id\":\"mean_bytes_by_clientip\",\"job_type\":\"anomaly_detector\",\"job_version\":\"8.0.0\",\"create_time\":1604671135245,\"description\":\"mean bytes by clientip\","
-        "\"analysis_config\":{\"bucket_span\":\"3h\",\"detectors\":[{\"detector_description\":\"mean(bytes) by clientip\",\"function\":\"mean\",\"field_name\":\"bytes\",\"by_field_name\":\"clientip\","
-        "\"custom_rules\":[{\"actions\":[\"skip_result\"],\"scope\":{\"clientip\":{\"filter_id\":\"safe_ips\",\"filter_type\":\"include\"}},\"conditions\":[{\"applies_to\":\"actual\",\"operator\":\"lt\",\"value\":10.0}]}],"
-        "\"detector_index\":0}],\"influencers\":[\"clientip\"]},\"analysis_limits\":{\"model_memory_limit\":\"42mb\",\"categorization_examples_limit\":4},"
-        "\"data_description\":{\"time_field\":\"timestamp\",\"time_format\":\"epoch_ms\"},\"model_plot_config\":{\"enabled\":false,\"annotations_enabled\":false},"
-        "\"model_snapshot_retention_days\":10,\"daily_model_snapshot_retention_after_days\":1,\"results_index_name\":\"shared\",\"allow_lazy_open\":false}"};
-
-    // Expect parsing to succeed if the filter referenced by the custom rule can be found in the filter map.
-    const std::string filterConfigJson{"{\"filters\":[{\"filter_id\":\"safe_ips\",\"items\":[]}]}"};
-    ml::api::CAnomalyJobConfig jobConfig;
-    BOOST_TEST_REQUIRE(jobConfig.parseFilterConfig(filterConfigJson));
-
-    const std::string validScheduledEventsConfigJson{"{\"events\":["
-                                                     "]}"};
-
-    BOOST_TEST_REQUIRE(jobConfig.parseEventConfig(validScheduledEventsConfigJson));
-
-    jobConfig.analysisConfig().init(jobConfig.ruleFilters(), jobConfig.scheduledEvents());
-
-    BOOST_REQUIRE_MESSAGE(jobConfig.parse(validAnomalyJobConfigWithCustomRuleFilter),
-                          "Cannot parse JSON job config!");
-    BOOST_TEST_REQUIRE(jobConfig.isInitialized());
-
-    // Expect parsing to fail if the analysis config JSON string is invalid
-    const std::string inValidAnalysisConfigString{"{\"bucket_span\":\"1h\""};
-    BOOST_TEST_REQUIRE(!jobConfig.analysisConfig().reparseDetectorsFromStoredConfig(
-        inValidAnalysisConfigString));
-
-    // Expect parsing to fail if the filter referenced by the custom rule cannot be found
-    const std::string validAnalysisConfigStringWithUnknownFilter{
-        "{\"bucket_span\":\"1h\",\"detectors\":[{\"detector_description\":\"count over ip\",\"function\":\"count\",\"over_field_name\":\"ip\",\"custom_rules\":[{\"actions\":[\"skip_result\"],\"scope\":{\"ip\":{\"filter_id\":\"unknown_filter\",\"filter_type\":\"include\"}}}],\"detector_index\":0}],\"influencers\":[],\"model_prune_window\":\"30d\"}"};
-    BOOST_REQUIRE_EXCEPTION(
-        jobConfig.analysisConfig().reparseDetectorsFromStoredConfig(validAnalysisConfigStringWithUnknownFilter),
-        ml::api::CAnomalyJobConfigReader::CParseError,
-        [](ml::api::CAnomalyJobConfigReader::CParseError const&) { return true; });
-
-    // Expect parsing to succeed if the filter referenced by the custom rule is registered.
-    const std::string validAnalysisConfigString{
-        "{\"bucket_span\":\"1h\",\"detectors\":[{\"detector_description\":\"count over ip\",\"function\":\"count\",\"over_field_name\":\"ip\",\"custom_rules\":[{\"actions\":[\"skip_result\"],\"scope\":{\"ip\":{\"filter_id\":\"safe_ips\",\"filter_type\":\"include\"}}}],\"detector_index\":0}],\"influencers\":[],\"model_prune_window\":\"30d\"}"};
-    BOOST_TEST_REQUIRE(jobConfig.analysisConfig().reparseDetectorsFromStoredConfig(
-        validAnalysisConfigString));
-}
-
 BOOST_AUTO_TEST_CASE(testParse) {
 
     using TAnalysisConfig = ml::api::CAnomalyJobConfig::CAnalysisConfig;
diff --git a/lib/api/unittest/CAnomalyJobTest.cc b/lib/api/unittest/CAnomalyJobTest.cc
index 02d5925483..0de490e0e5 100644
--- a/lib/api/unittest/CAnomalyJobTest.cc
+++ b/lib/api/unittest/CAnomalyJobTest.cc
@@ -36,6 +36,7 @@
 #include <cstdio>
 #include <fstream>
 #include <map>
+#include <random>
 #include <sstream>
 
 BOOST_TEST_DONT_PRINT_LOG_VALUE(json::array::const_iterator)
@@ -186,6 +187,9 @@ bool findLine(const std::string& regex, const ml::core::CRegex::TStrVec& lines)
 }
 
 const ml::core_t::TTime BUCKET_SIZE(3600);
+
+using TStrStrPr = std::pair<std::string, std::string>;
+using TStrStrPrVec = std::vector<TStrStrPr>;
 }
 
 using namespace ml;
@@ -851,6 +855,186 @@ BOOST_AUTO_TEST_CASE(testRestoreFailsWithEmptyStream) {
     BOOST_TEST_REQUIRE(job.restoreState(restoreSearcher, completeToTime) == false);
 }
 
+BOOST_AUTO_TEST_CASE(testConfigUpdate) {
+    // This, in part, is essentially replicating the DetectionRulesIT/testScope Java REST test.
+    // It proves useful to have the test here too, as it provides an entrypoint for investigating
+    // any issues related to filters, especially when updating them when already referenced by anomaly detector models.
+    // We simply want to see the job run to completion.
+    ml::api::CAnomalyJobConfig jobConfig;
+    BOOST_REQUIRE_EQUAL(true, jobConfig.initFromFiles("testfiles/count_over_ip_config.json",
+                                                      "testfiles/filterConfig.json",
+                                                      "testfiles/eventConfig.json"));
+
+    const ml::api::CAnomalyJobConfig::CAnalysisConfig& analysisConfig =
+        jobConfig.analysisConfig();
+
+    model::CLimits limits;
+
+    model::CAnomalyDetectorModelConfig modelConfig = analysisConfig.makeModelConfig();
+    std::stringstream outputStrm;
+    core::CJsonOutputStreamWrapper wrappedOutputStream(outputStrm);
+
+    CTestAnomalyJob job("job", limits, jobConfig, modelConfig, wrappedOutputStream);
+
+    auto generateRandomAlpha = [](int strLen) {
+        std::random_device rd;
+        std::mt19937 gen(rd());
+        std::uniform_int_distribution dis(0, 25);
+
+        std::string str;
+        for (int i = 0; i < strLen; ++i) {
+            str += char('a' + dis(gen));
+        }
+        return str;
+    };
+
+    long timestamp = 1509062400000L;
+    TStrStrPrVec data;
+
+    for (int bucket = 0; bucket < 20; bucket++) {
+        for (int i = 0; i < 5; i++) {
+            data.emplace_back(core::CStringUtils::typeToString(timestamp),
+                              generateRandomAlpha(10));
+        }
+        timestamp += 3600 * 1000;
+    }
+
+    // Now send anomalous counts for our filtered IPs plus 333.333.333.333
+    auto namedIps = std::vector{"111.111.111.111", "222.222.222.222", "333.333.333.333"};
+    for (int i = 0; i < 10; i++) {
+        for (auto& ip : namedIps) {
+            data.emplace_back(core::CStringUtils::typeToString(timestamp), ip);
+        }
+    }
+
+    for (int bucket = 0; bucket < 3; bucket++) {
+        for (int i = 0; i < 5; i++) {
+            data.emplace_back(core::CStringUtils::typeToString(timestamp),
+                              generateRandomAlpha(10));
+        }
+        timestamp += 3600 * 1000;
+    }
+
+    CTestAnomalyJob::TStrStrUMap dataRows;
+
+    for (const auto & [ time, ip ] : data) {
+        dataRows["time"] = time;
+        dataRows["ip"] = ip;
+        BOOST_TEST_REQUIRE(job.handleRecord(dataRows));
+    }
+
+    BOOST_REQUIRE_EQUAL(145, job.numRecordsHandled());
+
+    const std::string& detectorConfig1{R"(
+        {
+            "filters":[{"filter_id":"safe_ips", "items":["111.111.111.111","222.222.222.222"]}],
+            "events":[{"description":"event_1", "rules":[{"actions":["skip_result","skip_model_update"],"conditions":[{"applies_to":"time","operator":"gte","value": 1.0},{"applies_to":"time","operator":"lt","value": 2.0}]}]}],
+            "model_plot_config":{"enabled":true,"annotations_enabled":false},
+            "detector_rules":{"detector_index":0,"custom_rules":[{"actions":["skip_result"],"conditions":[{"applies_to":"actual","operator":"gte","value":15.0},{"applies_to":"actual","operator":"lte","value":30.0}]}]}
+        }
+    )"};
+
+    job.updateConfig(detectorConfig1);
+
+    BOOST_REQUIRE_EQUAL(1, jobConfig.analysisConfig().detectionRules().size());
+    auto itr = jobConfig.analysisConfig().detectionRules().find(0);
+    BOOST_REQUIRE_EQUAL(1, itr->second.size());
+    std::string rule{itr->second[0].print()};
+    BOOST_REQUIRE_EQUAL(
+        std::string("SKIP_RESULT IF ACTUAL >= 15.000000 AND ACTUAL <= 30.000000"), rule);
+
+    api::CAnomalyJobConfig::CModelPlotConfig& modelPlotConfig = jobConfig.modelPlotConfig();
+    BOOST_REQUIRE_EQUAL(false, modelPlotConfig.annotationsEnabled());
+    BOOST_REQUIRE_EQUAL(true, modelPlotConfig.enabled());
+
+    auto events = jobConfig.analysisConfig().scheduledEvents();
+    BOOST_REQUIRE_EQUAL(1, events.size());
+    BOOST_REQUIRE_EQUAL(std::string("event_1"), events[0].first);
+    BOOST_REQUIRE_EQUAL(std::string("SKIP_RESULT AND SKIP_MODEL_UPDATE IF TIME >= 1.000000 AND TIME < 2.000000"),
+                        events[0].second.print());
+
+    auto ruleFilters = jobConfig.ruleFilters();
+    BOOST_REQUIRE_EQUAL(1, ruleFilters.size());
+
+    BOOST_REQUIRE_EQUAL(true, ruleFilters["safe_ips"].contains("111.111.111.111"));
+    BOOST_REQUIRE_EQUAL(true, ruleFilters["safe_ips"].contains("222.222.222.222"));
+    BOOST_REQUIRE_EQUAL(false, ruleFilters["safe_ips"].contains("333.333.333.333"));
+
+    const std::string& detectorConfig2{R"(
+        {
+        "filters":[{"filter_id":"safe_ips", "items":["333.333.333.333"]}],
+        "events":[{"description":"event_1", "rules":[{"actions":["skip_result","skip_model_update"],"conditions":[{"applies_to":"time","operator":"gte","value": 2.0},{"applies_to":"time","operator":"lt","value": 3.0}]}]}],
+        "model_plot_config":{"enabled":false,"annotations_enabled":true},
+        "detector_rules":{"detector_index":0,"custom_rules":[{"actions":["skip_result"],"conditions":[{"applies_to":"typical","operator":"gte","value":10.0},{"applies_to":"typical","operator":"lte","value":50.0}]}]}
+        })"};
+
+    job.updateConfig(detectorConfig2);
+
+    data.clear();
+    // Send another anomalous bucket
+    for (int i = 0; i < 10; i++) {
+        for (auto& ip : namedIps) {
+            data.emplace_back(core::CStringUtils::typeToString(timestamp), ip);
+        }
+    }
+
+    // Some more normal buckets
+    for (int bucket = 0; bucket < 3; bucket++) {
+        for (int i = 0; i < 5; i++) {
+            data.emplace_back(core::CStringUtils::typeToString(timestamp),
+                              generateRandomAlpha(10));
+        }
+        timestamp += 3600 * 1000;
+    }
+
+    dataRows.clear();
+    for (const auto & [ time, ip ] : data) {
+        dataRows["time"] = time;
+        dataRows["ip"] = ip;
+        BOOST_TEST_REQUIRE(job.handleRecord(dataRows));
+    }
+
+    BOOST_REQUIRE_EQUAL(190, job.numRecordsHandled());
+
+    BOOST_REQUIRE_EQUAL(1, jobConfig.analysisConfig().detectionRules().size());
+    itr = jobConfig.analysisConfig().detectionRules().find(0);
+    BOOST_REQUIRE_EQUAL(1, itr->second.size());
+    rule = itr->second[0].print();
+    BOOST_REQUIRE_EQUAL(
+        std::string("SKIP_RESULT IF TYPICAL >= 10.000000 AND TYPICAL <= 50.000000"), rule);
+
+    modelPlotConfig = jobConfig.modelPlotConfig();
+    BOOST_REQUIRE_EQUAL(true, modelPlotConfig.annotationsEnabled());
+    BOOST_REQUIRE_EQUAL(false, modelPlotConfig.enabled());
+
+    events = jobConfig.analysisConfig().scheduledEvents();
+    BOOST_REQUIRE_EQUAL(1, events.size());
+    BOOST_REQUIRE_EQUAL(std::string("event_1"), events[0].first);
+    BOOST_REQUIRE_EQUAL(std::string("SKIP_RESULT AND SKIP_MODEL_UPDATE IF TIME >= 2.000000 AND TIME < 3.000000"),
+                        events[0].second.print());
+
+    ruleFilters = jobConfig.ruleFilters();
+    BOOST_REQUIRE_EQUAL(1, ruleFilters.size());
+
+    BOOST_REQUIRE_EQUAL(false, ruleFilters["safe_ips"].contains("111.111.111.111"));
+    BOOST_REQUIRE_EQUAL(false, ruleFilters["safe_ips"].contains("222.222.222.222"));
+    BOOST_REQUIRE_EQUAL(true, ruleFilters["safe_ips"].contains("333.333.333.333"));
+
+    job.finalise();
+    wrappedOutputStream.syncFlush();
+
+    std::string output = outputStrm.str();
+    LOG_TRACE(<< "Output has yielded: " << output);
+
+    // check that the quantile state has actually been persisted
+    core::CRegex regex;
+    regex.init("\n");
+    core::CRegex::TStrVec lines;
+    regex.split(output, lines);
+    BOOST_REQUIRE_EQUAL(
+        true, findLine("\"quantiles\":{\"job_id\":\"job\",\"quantile_state\".*", lines));
+}
+
 BOOST_AUTO_TEST_CASE(testParsePersistControlMessageArgs) {
     {
         const ml::core_t::TTime expectedSnapshotTimestamp{1283524206};
diff --git a/lib/api/unittest/CTestAnomalyJob.cc b/lib/api/unittest/CTestAnomalyJob.cc
index 89e139fc3e..5a3f678932 100644
--- a/lib/api/unittest/CTestAnomalyJob.cc
+++ b/lib/api/unittest/CTestAnomalyJob.cc
@@ -50,3 +50,25 @@ CTestAnomalyJob::makeSimpleJobConfig(const std::string& functionName,
                                            influencers, summaryCountFieldName);
     return jobConfig;
 }
+
+ml::api::CAnomalyJobConfig CTestAnomalyJob::makeJobConfig(const std::string& detectorsConfig) {
+    json::parser p;
+    boost::system::error_code ec;
+    p.write_some(detectorsConfig, ec);
+    if (ec) {
+        LOG_ERROR(<< "An error occurred while parsing JSON: " << ec.message());
+        return {};
+    }
+    json::value doc = p.release();
+    if (doc.is_object() == false) {
+        LOG_ERROR(<< "Input error: expected JSON object but input was '"
+                  << detectorsConfig << "'. Please report this problem.");
+        return {};
+    }
+
+    json::object obj = doc.as_object();
+
+    ml::api::CAnomalyJobConfig jobConfig;
+    jobConfig.analysisConfig().parseDetectorsConfig(obj);
+    return jobConfig;
+}
\ No newline at end of file
diff --git a/lib/api/unittest/CTestAnomalyJob.h b/lib/api/unittest/CTestAnomalyJob.h
index c794d7e17c..01187dfcdf 100644
--- a/lib/api/unittest/CTestAnomalyJob.h
+++ b/lib/api/unittest/CTestAnomalyJob.h
@@ -55,6 +55,8 @@ class CTestAnomalyJob : public ml::api::CAnomalyJob {
                         const std::string& partitionFieldName,
                         const TStrVec& influencers = {},
                         const std::string& summaryCountFieldName = "");
+
+    static ml::api::CAnomalyJobConfig makeJobConfig(const std::string& detectorsConfig);
 };
 
 #endif // INCLUDED_CTestAnomalyJob_h
diff --git a/lib/api/unittest/testfiles/count_over_ip_config.json b/lib/api/unittest/testfiles/count_over_ip_config.json
new file mode 100644
index 0000000000..170afb1925
--- /dev/null
+++ b/lib/api/unittest/testfiles/count_over_ip_config.json
@@ -0,0 +1,44 @@
+{
+  "job_id": "detection-rules-it-test-scope",
+  "job_type": "anomaly_detector",
+  "job_version": "12.0.0",
+  "create_time": 1739482196563,
+  "analysis_config": {
+    "bucket_span": "1h",
+    "detectors": [
+      {
+        "detector_description": "count over ip",
+        "function": "count",
+        "over_field_name": "ip",
+        "custom_rules": [
+          {
+            "actions": [
+              "skip_result"
+            ],
+            "scope": {
+              "ip": {
+                "filter_id": "safe_ips",
+                "filter_type": "include"
+              }
+            }
+          }
+        ],
+        "detector_index": 0
+      }
+    ],
+    "influencers": [],
+    "model_prune_window": "30d"
+  },
+  "analysis_limits": {
+    "model_memory_limit": "1024mb",
+    "categorization_examples_limit": 4
+  },
+  "data_description": {
+    "time_field": "time",
+    "time_format": "epoch_ms"
+  },
+  "model_snapshot_retention_days": 10,
+  "daily_model_snapshot_retention_after_days": 1,
+  "results_index_name": "shared",
+  "allow_lazy_open": false
+}
diff --git a/lib/api/unittest/testfiles/eventConfig.json b/lib/api/unittest/testfiles/eventConfig.json
new file mode 100644
index 0000000000..c14d27bb64
--- /dev/null
+++ b/lib/api/unittest/testfiles/eventConfig.json
@@ -0,0 +1,4 @@
+{
+  "events": [
+  ]
+}
diff --git a/lib/api/unittest/testfiles/filterConfig.json b/lib/api/unittest/testfiles/filterConfig.json
new file mode 100644
index 0000000000..682f3d451d
--- /dev/null
+++ b/lib/api/unittest/testfiles/filterConfig.json
@@ -0,0 +1,11 @@
+{
+  "filters": [
+    {
+      "filter_id": "safe_ips",
+      "items": [
+        "111.111.111.111",
+        "222.222.222.222"
+      ]
+    }
+  ]
+}

From 36ffe68b55b1ded1075e10ce6b54fcf3799cefd9 Mon Sep 17 00:00:00 2001
From: Ed Savage <ed.savage@elastic.co>
Date: Wed, 19 Mar 2025 16:42:30 +1300
Subject: [PATCH 20/27] [8.17][ML] Update location of boost libraries repo
 (#2829) (#2837)

The Boost repository is no longer hosted by jfrog. Update scripts, docs,
Dockerfiles etc to reference the new location.

Backports #2829
---
 build-setup/linux.md                                   | 2 +-
 build-setup/macos.md                                   | 2 +-
 build-setup/windows.md                                 | 2 +-
 dev-tools/docker/linux_aarch64_native_image/Dockerfile | 2 +-
 dev-tools/docker/linux_image/Dockerfile                | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/build-setup/linux.md b/build-setup/linux.md
index 1017c1eb94..78351dea97 100644
--- a/build-setup/linux.md
+++ b/build-setup/linux.md
@@ -173,7 +173,7 @@ to install.
 
 ### Boost 1.83.0
 
-Download version 1.83.0 of Boost from <https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2>. You must get this exact version, as the Machine Learning build system requires it.
+Download version 1.83.0 of Boost from <https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.bz2>. You must get this exact version, as the Machine Learning build system requires it.
 
 Assuming you chose the `.bz2` version, extract it to a temporary directory:
 
diff --git a/build-setup/macos.md b/build-setup/macos.md
index 9d3719384f..a0374d35ab 100644
--- a/build-setup/macos.md
+++ b/build-setup/macos.md
@@ -68,7 +68,7 @@ at the command prompt.
 
 ### Boost 1.83.0
 
-Download version 1.83.0 of Boost from <https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2>. You must get this exact version, as the Machine Learning build system requires it.
+Download version 1.83.0 of Boost from <https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.bz2>. You must get this exact version, as the Machine Learning build system requires it.
 
 Assuming you chose the `.bz2` version, extract it to a temporary directory:
 
diff --git a/build-setup/windows.md b/build-setup/windows.md
index 15ff2b6b92..a382ce10f7 100644
--- a/build-setup/windows.md
+++ b/build-setup/windows.md
@@ -121,7 +121,7 @@ nmake install
 
 ### Boost 1.83.0
 
-Download version 1.83.0 of Boost from <https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2> . You must get this exact version, as the Machine Learning build system requires it.
+Download version 1.83.0 of Boost from <https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.bz2> . You must get this exact version, as the Machine Learning build system requires it.
 
 Assuming you chose the `.bz2` version, extract it in a Git bash shell using the GNU tar that comes with Git for Windows, e.g.:
 
diff --git a/dev-tools/docker/linux_aarch64_native_image/Dockerfile b/dev-tools/docker/linux_aarch64_native_image/Dockerfile
index a8c75b5240..3909520ce8 100644
--- a/dev-tools/docker/linux_aarch64_native_image/Dockerfile
+++ b/dev-tools/docker/linux_aarch64_native_image/Dockerfile
@@ -78,7 +78,7 @@ RUN \
 # Build Boost
 RUN \
   cd ${build_dir} && \
-  wget --quiet -O - https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2 | tar jxf - && \
+  wget --quiet -O - https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.bz2 | tar jxf - && \
   cd boost_1_83_0 && \
   ./bootstrap.sh --without-libraries=context --without-libraries=coroutine --without-libraries=graph_parallel --without-libraries=mpi --without-libraries=python --without-icu && \
   sed -i -e 's|(13ul)(29ul)(53ul)(97ul)(193ul)(389ul)(769ul)(1543ul)(3079ul)(6151ul)( \\|(3ul)(13ul)(29ul)(53ul)(97ul)(193ul)(389ul)(769ul)(1543ul)(3079ul)(6151ul)(       \\|' boost/unordered/detail/prime_fmod.hpp && \
diff --git a/dev-tools/docker/linux_image/Dockerfile b/dev-tools/docker/linux_image/Dockerfile
index 99e957a363..d06789544e 100644
--- a/dev-tools/docker/linux_image/Dockerfile
+++ b/dev-tools/docker/linux_image/Dockerfile
@@ -78,7 +78,7 @@ RUN \
 # Build Boost
 RUN \
   cd ${build_dir} && \
-  wget --quiet -O - https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2 | tar jxf - && \
+  wget --quiet -O - https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.bz2 | tar jxf - && \
   cd boost_1_83_0 && \
   ./bootstrap.sh --without-libraries=context --without-libraries=coroutine --without-libraries=graph_parallel --without-libraries=mpi --without-libraries=python --without-icu && \
   sed -i -e 's|(13ul)(29ul)(53ul)(97ul)(193ul)(389ul)(769ul)(1543ul)(3079ul)(6151ul)( \\|(3ul)(13ul)(29ul)(53ul)(97ul)(193ul)(389ul)(769ul)(1543ul)(3079ul)(6151ul)(       \\|' boost/unordered/detail/prime_fmod.hpp && \

From 69ff21255c142d2402b6ebca67fc5c6076a42bb1 Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 25 Mar 2025 09:59:09 +0100
Subject: [PATCH 21/27] [ML] Bump version to 8.17.5

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index ad6c9d5d90..fe6fb9c001 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.4
+elasticsearchVersion=8.17.5
 
 artifactName=ml-cpp
 

From ed03b38c7a6debf0dccb827b80fa5af4322fe8c2 Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 15 Apr 2025 13:14:40 +0200
Subject: [PATCH 22/27] [ML] Bump version to 8.17.6

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index fe6fb9c001..9e6f74f4d6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.5
+elasticsearchVersion=8.17.6
 
 artifactName=ml-cpp
 

From cbd31400325b7500392c88669d1c12fcffac2369 Mon Sep 17 00:00:00 2001
From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com>
Date: Fri, 2 May 2025 09:46:37 +0200
Subject: [PATCH 23/27] [8.17] [ML] Restrict file system access for pytorch
 models (#2851) (#2855)

* [ML] Restrict file system access for pytorch models (#2851)

This PR ensures that the PyTorch models are not allowed to access the file system. It accomplishes the goal by inspecting the model's operations and prohibiting the loading of models with operations that read or write files.

(cherry picked from commit fff4062ad42ca40d9cb765505c83f580b72b106b)

* Update CHANGELOG.asciidoc

* rewrite for C++14 compatibility
---
 bin/pytorch_inference/Main.cc | 24 ++++++++++++++++++++++++
 docs/CHANGELOG.asciidoc       |  5 +++++
 2 files changed, 29 insertions(+)

diff --git a/bin/pytorch_inference/Main.cc b/bin/pytorch_inference/Main.cc
index 92db0aacec..98f303df4e 100644
--- a/bin/pytorch_inference/Main.cc
+++ b/bin/pytorch_inference/Main.cc
@@ -41,6 +41,29 @@
 #include <optional>
 #include <string>
 
+namespace {
+// Add more forbidden ops here if needed
+const std::unordered_set<std::string> FORBIDDEN_OPERATIONS = {"aten::from_file", "aten::save"};
+
+void verifySafeModel(const torch::jit::script::Module& module_) {
+    try {
+        const auto method = module_.get_method("forward");
+        const auto graph = method.graph();
+        for (const auto& node : graph->nodes()) {
+            const std::string opName = node->kind().toQualString();
+            if (FORBIDDEN_OPERATIONS.find(opName) != FORBIDDEN_OPERATIONS.end()) {
+                HANDLE_FATAL(<< "Loading the inference process failed because it contains forbidden operation: "
+                             << opName);
+            }
+        }
+    } catch (const c10::Error& e) {
+        LOG_FATAL(<< "Failed to get forward method: " << e.what());
+    }
+
+    LOG_DEBUG(<< "Model verified: no forbidden operations detected.");
+}
+}
+
 torch::Tensor infer(torch::jit::script::Module& module_,
                     ml::torch::CCommandParser::SRequest& request) {
 
@@ -280,6 +303,7 @@ int main(int argc, char** argv) {
             return EXIT_FAILURE;
         }
         module_ = torch::jit::load(std::move(readAdapter));
+        verifySafeModel(module_);
         module_.eval();
 
         LOG_DEBUG(<< "model loaded");
diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc
index 2d41089a31..5d27d41e33 100644
--- a/docs/CHANGELOG.asciidoc
+++ b/docs/CHANGELOG.asciidoc
@@ -28,6 +28,11 @@
 
 //=== Regressions
 
+== {es} version 8.17.7
+
+=== Enhancements
+* Restrict file system access for PyTorch models (See {ml-pull}2851[#2851].)
+
 == {es} version 8.16.6
 
 === Bug Fixes

From 25b74d3936f46fb3a6c6e29fbd8fea04a2132739 Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 6 May 2025 09:41:03 +0200
Subject: [PATCH 24/27]  [ML] Bump version to 8.17.7

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 9e6f74f4d6..27cf8c1e9b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.6
+elasticsearchVersion=8.17.7
 
 artifactName=ml-cpp
 

From 4c8cd67f897485d6db627e58a783403530783ba6 Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 3 Jun 2025 10:23:45 +0200
Subject: [PATCH 25/27] [ML] Bump version to 8.17.8

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 27cf8c1e9b..867b231927 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.7
+elasticsearchVersion=8.17.8
 
 artifactName=ml-cpp
 

From ea9c2e5d31d7238ee88b0430c11c871bd5283741 Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 24 Jun 2025 09:32:20 +0200
Subject: [PATCH 26/27] [ML] Bump version to 8.17.9

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 867b231927..5cbf03c133 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.8
+elasticsearchVersion=8.17.9
 
 artifactName=ml-cpp
 

From 2ade59577798074f27a652050a640966a60c8074 Mon Sep 17 00:00:00 2001
From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com>
Date: Tue, 22 Jul 2025 10:15:17 +0200
Subject: [PATCH 27/27]  [ML] Bump version to 8.17.10

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 5cbf03c133..2355738d30 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 org.gradle.daemon=false
 
-elasticsearchVersion=8.17.9
+elasticsearchVersion=8.17.10
 
 artifactName=ml-cpp
 
