Repository: elastic/apm-agent-php (php)

Issue:
PHP CLI process hangs on the PeriodicTaskExecutor destructor's thread_.join() call
**Describe the bug**
In our setup a main PHP CLI process triggered by cron is creating sub PHP processes to do work parallelly. We have observed that after some time, there are multiple child processes that are stuck. Their unit of work is completed, but they are not terminating. Upon further investigating these processes, we have seen that they are waiting for a lock to be released. After reproducing the issue locally we have seen in the traces that the PHP CLI process hangs on the [PeriodicTaskExecutor class destructor's thread_.join() call](https://github.com/elastic/apm-agent-php/blob/main/agent/native/libcommon/code/PeriodicTaskExecutor.h#L35).

- strace outpout of the stuck process

    ```
    strace: Process 488 attached
    futex(0x7f66c8bff910, FUTEX_WAIT_BITSET|FUTEX_CLOCK_REALTIME, 492, NULL, FUTEX_BITSET_MATCH_ANY
    ```
- gbd backtrace

    ```
    (gdb) backtrace
    #0  __futex_abstimed_wait_common64 (private=128, cancel=true, abstime=0x0, op=265, expected=492, futex_word=0x7f66c8bff910) at ./nptl/futex-internal.c:57
    #1  __futex_abstimed_wait_common (cancel=true, private=128, abstime=0x0, clockid=0, expected=492, futex_word=0x7f66c8bff910) at ./nptl/futex-internal.c:87
    #2  __GI___futex_abstimed_wait_cancelable64 (futex_word=futex_word@entry=0x7f66c8bff910, expected=492, clockid=clockid@entry=0, abstime=abstime@entry=0x0, private=private@entry=128) at ./nptl/futex-internal.c:139
    #3  0x00007f66d0636624 in __pthread_clockjoin_ex (threadid=140079431415360, thread_return=0x0, clockid=0, abstime=0x0, block=<optimized out>) at ./nptl/pthread_join_common.c:105
    #4  0x00007f66c93d9a63 in __gthread_join (__value_ptr=0x0, __threadid=<optimized out>) at /home/build/prerequisities/gcc-12.2.0/x86_64-linux-gnu/libstdc++-v3/include/x86_64-linux-gnu/bits/gthr-default.h:669
    #5  std::thread::join (this=0x5653fcdf7688) at ../../../.././libstdc++-v3/src/c++11/thread.cc:112
    #6  0x00007f66c9328f1a in elasticapm::php::PeriodicTaskExecutor::~PeriodicTaskExecutor (this=0x5653fcdf7640, __in_chrg=<optimized out>) at /source/agent/native/libcommon/code/./PeriodicTaskExecutor.h:35
    #7  elasticapm::php::PeriodicTaskExecutor::~PeriodicTaskExecutor (this=0x5653fcdf7640, __in_chrg=<optimized out>) at /source/agent/native/libcommon/code/./PeriodicTaskExecutor.h:37
    #8  std::default_delete<elasticapm::php::PeriodicTaskExecutor>::operator() (__ptr=0x5653fcdf7640, this=<optimized out>) at /opt/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:95
    #9  std::unique_ptr<elasticapm::php::PeriodicTaskExecutor, std::default_delete<elasticapm::php::PeriodicTaskExecutor> >::~unique_ptr (this=0x5653fce212c8, __in_chrg=<optimized out>) at /opt/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:396
    #10 elasticapm::php::AgentGlobals::~AgentGlobals (this=0x5653fce21290, __in_chrg=<optimized out>) at /source/agent/native/libcommon/code/./AgentGlobals.h:12
    #11 zm_globals_dtor_elastic_apm (elastic_apm_globals=0x7f66c947dd80 <elastic_apm_globals>) at /source/agent/native/ext/elastic_apm.cpp:299
    #12 0x00005653fafc4758 in module_destructor (module=0x7f66c947d8a0 <elastic_apm_module_entry>) at ./Zend/zend_API.c:3075
    #13 0x00005653fafd1015 in _zend_hash_del_el_ex (prev=<optimized out>, p=0x5653fca2df00, idx=59, ht=0x5653fb2458a0 <module_registry>) at ./Zend/zend_hash.c:1425
    #14 _zend_hash_del_el (p=0x5653fca2df00, idx=59, ht=0x5653fb2458a0 <module_registry>) at ./Zend/zend_hash.c:1452
    #15 zend_hash_graceful_reverse_destroy (ht=0x5653fb2458a0 <module_registry>) at ./Zend/zend_hash.c:1977
    #16 0x00005653fafc2e96 in zend_destroy_modules () at ./Zend/zend_API.c:2384
    #17 0x00005653fafbded7 in zend_shutdown () at ./Zend/zend.c:1099
    #18 0x00005653faf575ba in php_module_shutdown () at ./main/main.c:2453
    #19 php_module_shutdown () at ./main/main.c:2430
    #20 0x00005653fadecbf2 in main (argc=5, argv=0x5653fc9e96e0) at ./sapi/cli/php_cli.c:1346
    ```
  
   ![image](https://github.com/user-attachments/assets/7c142549-de1c-438c-980f-95597192a110)

**To Reproduce**
Running the following setup reproduces the situation consistently for me:
- Files:
  - ./Dockerfile
    ```
    FROM ubuntu:22.04
    ARG DEBIAN_FRONTEND=noninteractive
    ARG PHP_VERSION=8.2
    
    RUN apt -y update
    RUN apt-get -y install software-properties-common
    RUN apt-get -y install tar gdb strace
    
    RUN add-apt-repository -c main -c main/debug ppa:ondrej/php
    RUN apt -y update
    
    RUN apt-get -y install php${PHP_VERSION} php${PHP_VERSION}-cli-dbgsym
    RUN update-alternatives --set php /usr/bin/php${PHP_VERSION}
    
    RUN apt-get -y install php${PHP_VERSION}-dom php${PHP_VERSION}-mysql php${PHP_VERSION}-soap
    
    ADD --chmod=0755 https://github.com/elastic/apm-agent-php/releases/download/v1.13.0/apm-agent-php_1.13.0_amd64.deb /root
    ADD --chmod=0755 https://github.com/elastic/apm-agent-php/releases/download/v1.13.0/apm-agent-php-debugsymbols-linux-x86-64.tar /root
    
    RUN dpkg -i /root/apm-agent-php_1.13.0_amd64.deb
    RUN tar xvf /root/apm-agent-php-debugsymbols-linux-x86-64.tar -C /usr/lib/debug
    
    RUN touch /var/log/apm_process.log
    
    COPY ./src /root/src
    WORKDIR /root/src
    ```
  - ./docker-compose.yml
    ```
    name: apm_process
    services:
        apm_process:
            build:
                context: "."
                args:
                    PHP_VERSION: ${PHP_VERSION-8.2}
            volumes:
                - "./src:/root/src"
                - "./99-elastic-apm-custom.ini:/etc/php/${PHP_VERSION-8.2}/cli/conf.d/99-elastic-apm-custom.ini"
            cap_add:
                -   SYS_PTRACE
            tty: true
            stdin_open: true
      ```
  - ./99-elastic-apm-custom.ini
    ```
    [elastic]
    elastic_apm.enabled = true
    elastic_apm.secret_token = "{{REPLACE_ME}}"
    elastic_apm.server_url = "{{REPLACE_ME}}"
    elastic_apm.environment = "local"
    elastic_apm.log_level = "DEBUG"
    elastic_apm.log_level_stderr = "DEBUG"
    elastic_apm.server_timeout = "30s"
    elastic_apm.service_name = "apm_process"
    elastic_apm.service_version = "v1"
    elastic_apm.transaction_max_spans = 500
    elastic_apm.transaction_sample_rate = 1.0
    elastic_apm.verify_server_cert = true
    elastic_apm.dev_internal_backend_comm_log_verbose = true
    elastic_apm.profiling_inferred_spans_enabled = true
    ```
  - ./src/test.php
    ```
    <?php
    
    use Elastic\Apm\ElasticApm;
    
    chdir(__DIR__);
    $transaction = ElasticApm::getCurrentTransaction();
    $transaction->setName('PARENT');
    
    echo 'PARENT transaction id:' . $transaction->getId() . "\n";
    
    for ($i = 0; $i < 10000; $i++) {
        $param = random_int(0, 60);
    
        $command = "nohup /usr/bin/php -f run.php " . $param . " " . getmypid() . '-' . $i . " 2>>/var/log/apm_process.log 1>&2 &";
        echo "Running: " . $command . "\n";
    
        $span = $transaction->beginChildSpan('child span ' . $i, 'process', 'run.php', 'execute');
        shell_exec($command);
        $span->end();
    
        $sleepBetweenChildren = random_int(0, 1000);
        echo "Sleeping for " . $sleepBetweenChildren . "(us)\n";
        usleep($sleepBetweenChildren);
    }
    ```
  - ./src/run.php
    ```
    <?php
    
    use Elastic\Apm\ElasticApm;
    
    $transaction = ElasticApm::getCurrentTransaction();
    $transaction->setName('CHILD');
    echo getmypid() . ' Child transaction id:' . $transaction->getId() . "\n";
    
    $span = $transaction->beginChildSpan('wait', 'custom');
    $sleepTime = $argv[1];
    echo getmypid() . ' Child sleep for ' . $sleepTime . "(ms)\n";
    usleep(1000 * $sleepTime);
    $span->end();
    ```

- Steps:
  1. `docker compose up`
  2. `docker compose exec apm_process /bin/bash`
  3. `php test.php 2>>/var/log/apm_process.log 1>&2`
  4. After the process finished, check with `top` command the list of processes and there should be at least one, but possibly multiple like this `/usr/bin/php -f run.php {{delay_time}} {{main_process_id}}-{{counter_of_child_processes}}` stuck forever.
  5. with `strace -p {{process_id}}` we should see that the process is stuck on a `futex()` call like above.

- Notes:
  - The issue is possibly a race condition, so it might take a few runs to reproduce, but for me the above setup consistently ended in multiple stuck processes. 
  - I have originally started with our production setup and dependencies to reproduce the issue, but managed to strip down to this relative minimalistic example. The 3 extensions (dom, mysql, soap) seems to be needed to reproduce the issue. If any of them is taken out of the example, the problem did not occur for me.

**Expected behavior**
PHP process should not stuck upon terminating when APM extension is enabled.


Pull request description:
Fixed PeriodicTaskExecutor fork handler (#1209)
Fixes #1209 

Patch:
diff --git a/.ci/Makefile b/.ci/Makefile
index 588ede8e..acec7e96 100644
--- a/.ci/Makefile
+++ b/.ci/Makefile
@@ -1,11 +1,21 @@
 ifeq ($(DOCKERFILE),)
 	ifeq ($(BUILD_ARCHITECTURE), linuxmusl-x86-64)
 		DOCKERFILE := Dockerfile.alpine
+	else ifeq ($(BUILD_ARCHITECTURE), linuxmusl-arm64)
+		DOCKERFILE := Dockerfile.alpine
 	else
 		DOCKERFILE := Dockerfile
 	endif
 endif
 
+DOCKER_PLATFORM := linux/x86_64
+ifeq ($(BUILD_ARCHITECTURE), linux-arm64)
+	DOCKER_PLATFORM := linux/arm64
+else ifeq ($(BUILD_ARCHITECTURE), linuxmusl-arm64)
+	DOCKER_PLATFORM := linux/arm64
+endif
+
+
 SHELL=/bin/bash -o pipefail
 MAKEFLAGS += --no-print-directory
 IMAGE := test-php
@@ -45,6 +55,7 @@ prepare:  ## Build docker image for building and testing the project
 	docker build \
 		--build-arg PHP_VERSION=${PHP_VERSION} --build-arg SEL_DISTRO=${SEL_DISTRO}\
 		--tag $(IMAGE):${PHP_VERSION}$(SUFFIX) \
+		--platform $(DOCKER_PLATFORM) \
 		-f ${DOCKERFILE} .
 	@echo "::endgroup::"
 
@@ -59,8 +70,8 @@ build:
 		-w /source/agent/native \
 		-e CONAN_USER_HOME=$(CONAN_USER_HOME) \
 		elasticobservability/apm-agent-php-dev:native-build-gcc-12.2.0-$(BUILD_ARCHITECTURE)-0.0.2 \
-		sh -c "cmake --preset $(BUILD_ARCHITECTURE)-release \
-				&& cmake --build --preset $(BUILD_ARCHITECTURE)-release -j$(nproc)\
+		sh -c "cmake --preset $(BUILD_ARCHITECTURE)-release  \
+				&& cmake --build --preset $(BUILD_ARCHITECTURE)-release \
 				&& ctest --preset $(BUILD_ARCHITECTURE)-release --verbose"
 	@echo "::endgroup::"
 
diff --git a/.github/workflows/build-arch-matrix-generator.yml b/.github/workflows/build-arch-matrix-generator.yml
new file mode 100644
index 00000000..c44d8419
--- /dev/null
+++ b/.github/workflows/build-arch-matrix-generator.yml
@@ -0,0 +1,44 @@
+---
+
+# Runs the build based on the provided files in test.yml
+name: build
+
+on:
+  workflow_call:
+    inputs:
+      build_arch:
+        required: true
+        type: string
+        default: 'x86_64'
+    outputs:
+      matrix-combinations:
+        description: "Matrix of architectures to build for"
+        value: ${{ jobs.setup-build-matrix.outputs.matrix-combinations }}
+
+jobs:
+  setup-build-matrix:
+    name: setup-build-matrix
+    runs-on: ubuntu-latest
+    env:
+      SELECTED_ARCH: ${{ inputs.build_arch }}
+    outputs:
+      matrix-combinations: ${{ steps.setup-matrix-combinations.outputs.matrix-combinations }}
+    steps:
+      - name: Create build matrix
+        id: setup-matrix-combinations
+        run: |
+          MATRIX=''
+          if [ "${SELECTED_ARCH}" == "x86_64" ] ||  [ "${SELECTED_ARCH}" == "all" ]; then
+            echo "${SELECTED_ARCH} selected. Adding x86_64"
+            MATRIX+='{"arch": "linux-x86-64"}, {"arch": "linuxmusl-x86-64"}'
+          fi
+          if [ "${SELECTED_ARCH}" == "arm64" ] ||  [ "${SELECTED_ARCH}" == "all" ]; then
+            echo "${SELECTED_ARCH} selected. Adding arm64"
+            if [ ! -z "${MATRIX}" ]; then
+              MATRIX+=','
+            fi
+            MATRIX+='{"arch": "linux-arm64", "run_qemu": 1},{"arch": "linuxmusl-arm64", "run_qemu": 1}'
+          fi
+          echo "matrix-combinations={\"include\":[$MATRIX]}"
+
+          echo "matrix-combinations={\"include\":[$MATRIX]}" >> $GITHUB_OUTPUT
diff --git a/.github/workflows/build-packages.yml b/.github/workflows/build-packages.yml
index 25e55cbd..0715e1a0 100644
--- a/.github/workflows/build-packages.yml
+++ b/.github/workflows/build-packages.yml
@@ -13,24 +13,28 @@ jobs:
     runs-on: ubuntu-latest
     permissions:
       packages: read
+    strategy:
+      fail-fast: false
+      matrix:
+        BUILD_ARCH: ["x86-64", "arm64"]
+    env:
+      BUILD_ARCH: ${{ matrix.BUILD_ARCH }}
     steps:
       - uses: actions/checkout@v4
       - uses: actions/download-artifact@v3
         with:
-          name: package-parts-linux-x86-64
-          path: agent/native/_build/linux-x86-64-release/
+          name: package-parts-linux-${{ matrix.BUILD_ARCH }}
+          path: agent/native/_build/linux-${{ matrix.BUILD_ARCH }}-release/
       - uses: actions/download-artifact@v3
         with:
-          name: package-parts-linuxmusl-x86-64
-          path: agent/native/_build/linuxmusl-x86-64-release/
-
+          name: package-parts-linuxmusl-${{ matrix.BUILD_ARCH }}
+          path: agent/native/_build/linuxmusl-${{ matrix.BUILD_ARCH }}-release/
       - name: Log in to the Container registry
         uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
-
       - name: package
         run: make -C packaging package
       - name: package info
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 06559847..329816e7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,28 +4,54 @@
 name: build
 
 on:
-  workflow_call: ~
+  workflow_call:
+    inputs:
+      build_arch:
+        required: false
+        type: string
+        default: 'x86_64'
+  workflow_dispatch:
+    inputs:
+      build_arch:
+        type: choice
+        description: Build architecture
+        default: 'x86_64'
+        options:
+          - all
+          - x86_64
+          - arm64
 
 permissions:
   contents: read
 
 jobs:
+  setup-build-matrix:
+    uses: ./.github/workflows/build-arch-matrix-generator.yml
+    with:
+      build_arch: ${{ inputs.build_arch }}
+
   build:
     name: build-agent-library
     runs-on: ubuntu-latest
-    timeout-minutes: 30
+    needs: setup-build-matrix
+    timeout-minutes: 300
     strategy:
       fail-fast: false
-      matrix:
-        arch:
-          - "linux-x86-64"
-          - "linuxmusl-x86-64"
+      matrix: ${{ fromJson(needs.setup-build-matrix.outputs.matrix-combinations) }}
     env:
       BUILD_ARCHITECTURE: ${{ matrix.arch }}
     steps:
       - uses: actions/checkout@v4
+      - if: ${{ matrix.run_qemu }}
+        uses: docker/setup-qemu-action@v3
+        with:
+          platforms: arm64
       - name: Build
-        run: make -f .ci/Makefile build
+        run: |
+          uname -a
+          echo "Detected CPUs: $(nproc)"
+          make -f .ci/Makefile build
+
       - uses: actions/upload-artifact@v3
         with:
           name: package-parts-${{ matrix.arch }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3506e381..1c331193 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,6 +5,7 @@ permissions:
   contents: read
 
 on:
+  workflow_dispatch: ~
   push:
     tags: [ "v[0-9]+*" ]
     branches:
@@ -28,6 +29,8 @@ jobs:
 
   build:
     uses: ./.github/workflows/build.yml
+    with:
+      build_arch: all
 
   build-packages:
     permissions:
diff --git a/agent/native/ext/constants.h b/agent/native/ext/constants.h
index 48067fd6..fb00a8f3 100644
--- a/agent/native/ext/constants.h
+++ b/agent/native/ext/constants.h
@@ -19,6 +19,7 @@
 
 #pragma once
 
+
 #include "basic_macros.h"
 #include "elastic_apm_assert.h"
 
diff --git a/agent/native/libcommon/code/PeriodicTaskExecutor.h b/agent/native/libcommon/code/PeriodicTaskExecutor.h
index aa8e32e9..4a4463b2 100644
--- a/agent/native/libcommon/code/PeriodicTaskExecutor.h
+++ b/agent/native/libcommon/code/PeriodicTaskExecutor.h
@@ -11,7 +11,6 @@
 
 namespace elasticapm::php {
 
-
 class PeriodicTaskExecutor : public ForkableInterface {
 private:
     auto getThreadWorkerFunction() {
@@ -30,7 +29,6 @@ public:
 
     ~PeriodicTaskExecutor() {
         shutdown();
-
         if (thread_.joinable()) {
             thread_.join();
         }
@@ -62,12 +60,17 @@ public:
 
     void prefork() final {
         shutdown();
-        thread_.join();
+        if (thread_.joinable()) {
+            thread_.join();
+        }
     }
 
     void postfork([[maybe_unused]] bool child) final {
         working_ = true;
+
+        mutex_.lock();
         thread_ = std::thread(getThreadWorkerFunction());
+        mutex_.unlock();
         pauseCondition_.notify_all();
     }
 
@@ -107,8 +110,8 @@ private:
     std::chrono::milliseconds sleepInterval_ = std::chrono::milliseconds(20);
     std::vector<task_t> periodicTasks_;
     worker_init_t workerInit_;
-    std::thread thread_;
     std::mutex mutex_;
+    std::thread thread_;
     std::condition_variable pauseCondition_;
     bool working_ = true;
     bool resumed_ = false;
diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc
index 07902e8b..fb4d2702 100644
--- a/docs/setup.asciidoc
+++ b/docs/setup.asciidoc
@@ -11,12 +11,14 @@ endif::[]
 === Prerequisites
 
 [discrete]
-==== Operating system
-The agent is currently only available for the Linux operating system.
+==== Operating system and architecture
+We officially support Linux systems (glibc, deb and rpm packages) and Alpine Linux (musl libc - apk packages) for x86_64 (AMD64) processors.
+
+NOTE: Experimentally, we also provide packages for the ARM64 architecture - please note that these packages have not been fully tested.
 
 [discrete]
 ==== PHP
-The agent supports PHP versions 7.2-8.2.
+The agent supports PHP versions 7.2-8.3.
 
 [discrete]
 ==== curl
diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc
index 20a4c54a..c5bb0906 100644
--- a/docs/supported-technologies.asciidoc
+++ b/docs/supported-technologies.asciidoc
@@ -14,7 +14,8 @@ We will use the results to add support for the most requested technologies.
 [[supported-os]]
 === Operating systems
 
-The agent supports Linux operating system.
+We officially support Linux systems (glibc, deb and rpm packages) and Alpine Linux (musl libc - apk packages) for x86_64 (AMD64) processors.
+Experimentally, we also provide packages for the ARM64 architecture - please note that these packages have not been fully tested.
 
 [float]
 [[supported-php-versions]]
diff --git a/packaging/Makefile b/packaging/Makefile
index 993775e5..687b7864 100644
--- a/packaging/Makefile
+++ b/packaging/Makefile
@@ -11,6 +11,8 @@ SEL_DISTRO?=buster
 GIT_SHA?=$(shell git rev-parse HEAD || echo "unknown")
 RELEASE_VERSION?=
 GITHUB_RELEASES_URL=https://github.com/elastic/apm-agent-php/releases/download
+BUILD_ARCH := $(if $(BUILD_ARCH),$(BUILD_ARCH),"x86-64")
+
 export FPM_FLAGS=
 
 ifneq ($(PHP_VERSION), 7.2)
@@ -46,7 +48,7 @@ create-%: prepare  ## Create the specific package
 		-e OUTPUT=$(OUTPUT) \
 		-e FPM_FLAGS=${FPM_FLAGS} \
 		-e PHP_AGENT_DIR=${PHP_AGENT_DIR} \
-		-e BUILD_ARCH=x86-64 \
+		-e BUILD_ARCH=$(BUILD_ARCH) \
 		-w /app $(IMAGE)
 	@echo "::endgroup::"
 
@@ -76,33 +78,57 @@ info: apk-info deb-info rpm-info tar-info  ## Show the package metadata for all
 .PHONY: apk-info
 apk-info:  ## Show the apk package metadata
 	cd $(PWD) ;\
-	BINARY=$$(ls -1 $(OUTPUT)/*.apk) ;\
+	if [ "$(BUILD_ARCH)" = "arm64" ]; then \
+		BINARY=$$(find $(OUTPUT)/ -type f -name "*aarch64*.apk" -o -name "*arm64*.apk" | head -n 1); \
+	elif [ "$(BUILD_ARCH)" = "x86-64" ]; then \
+		BINARY=$$(find $(OUTPUT)/ -type f -name "*x86_64*.apk" -o -name "*amd64*.apk" | head -n 1); \
+	else \
+		echo "Unsupported BUILD_ARCH: $(BUILD_ARCH)"; \
+		exit 1; \
+	fi; \
+	echo $$BINARY; \
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /sbin/apk $(IMAGE) manifest $$BINARY
 
 .PHONY: deb-info
 deb-info:  ## Show the deb package metadata
 	cd $(PWD) ;\
-	BINARY=$$(ls -1 $(OUTPUT)/*.deb) ;\
+	if [ "$(BUILD_ARCH)" = "arm64" ]; then \
+		BINARY=$$(find $(OUTPUT)/ -type f -name "*aarch64*.deb" -o -name "*arm64*.deb" | head -n 1); \
+	elif [ "$(BUILD_ARCH)" = "x86-64" ]; then \
+		BINARY=$$(find $(OUTPUT)/ -type f -name "*x86_64*.deb" -o -name "*amd64*.deb" | head -n 1); \
+	else \
+		echo "Unsupported BUILD_ARCH: $(BUILD_ARCH)"; \
+		exit 1; \
+	fi; \
+	echo $$BINARY; \
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/dpkg $(IMAGE) --info $$BINARY ;\
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/dpkg $(IMAGE) -c $$BINARY
 
 .PHONY: rpm-info
 rpm-info:  ## Show the rpm package metadata
 	cd $(PWD) ;\
-	BINARY=$$(ls -1 $(OUTPUT)/*.rpm) ;\
+	if [ "$(BUILD_ARCH)" = "arm64" ]; then \
+		BINARY=$$(find $(OUTPUT)/ -type f -name "*aarch64*.rpm" -o -name "*arm64*.rpm" | head -n 1); \
+	elif [ "$(BUILD_ARCH)" = "x86-64" ]; then \
+		BINARY=$$(find $(OUTPUT)/ -type f -name "*x86_64*.rpm" -o -name "*amd64*.rpm" | head -n 1); \
+	else \
+		echo "Unsupported BUILD_ARCH: $(BUILD_ARCH)"; \
+		exit 1; \
+	fi; \
+	echo $$BINARY; \
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/rpm $(IMAGE) -qip $$BINARY ;\
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/rpm $(IMAGE) -qlp $$BINARY
 
 .PHONY: tar-info
 tar-info:  ## Show the tar package metadata
 	cd $(PWD) ;\
-	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-linux-x86-64*.tar) ;\
+	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-linux-$(BUILD_ARCH)*.tar) ;\
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/tar $(IMAGE) -tvf $$BINARY ;\
-	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-debugsymbols-linux-x86-64.tar) ;\
+	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-debugsymbols-linux-$(BUILD_ARCH).tar) ;\
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/tar $(IMAGE) -tvf $$BINARY ;\
-	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-linuxmusl-x86-64*.tar) ;\
+	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-linuxmusl-$(BUILD_ARCH)*.tar) ;\
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/tar $(IMAGE) -tvf $$BINARY ;\
-	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-debugsymbols-linuxmusl-x86-64.tar) ;\
+	BINARY=$$(ls -1 $(OUTPUT)/$(NAME)-debugsymbols-linuxmusl-$(BUILD_ARCH).tar) ;\
 	docker run --rm -v $(PWD):/app -w /app --entrypoint /usr/bin/tar $(IMAGE) -tvf $$BINARY
 
 .PHONY: prepare-apk
diff --git a/packaging/create-package.sh b/packaging/create-package.sh
index 9207a718..01bb4d89 100755
--- a/packaging/create-package.sh
+++ b/packaging/create-package.sh
@@ -117,25 +117,23 @@ rm "${OUTPUT}"/*.bck
 }
 
 
-
-
 # create second tar for musl
 if [ "${TYPE}" = 'tar' ] ; then
 	NAME_BACKUP=${NAME}
-	NAME="${NAME_BACKUP}-linux-x86-64"
-	BUILD_EXT_DIR=agent/native/_build/linux-x86-64-release/ext/
-	BUILD_LOADER_DIR=agent/native/_build/linux-x86-64-release/loader/code/
+	NAME="${NAME_BACKUP}-linux-${BUILD_ARCH}"
+	BUILD_EXT_DIR=agent/native/_build/linux-${BUILD_ARCH}-release/ext/
+	BUILD_LOADER_DIR=agent/native/_build/linux-${BUILD_ARCH}-release/loader/code/
 	createPackage
 
-	NAME="${NAME_BACKUP}-debugsymbols-linux-x86-64"
+	NAME="${NAME_BACKUP}-debugsymbols-linux-${BUILD_ARCH}"
 	createDebugPackage
 
-	NAME="${NAME_BACKUP}-linuxmusl-x86-64"
-	BUILD_EXT_DIR=agent/native/_build/linuxmusl-x86-64-release/ext/
-	BUILD_LOADER_DIR=agent/native/_build/linuxmusl-x86-64-release/loader/code/
+	NAME="${NAME_BACKUP}-linuxmusl-${BUILD_ARCH}"
+	BUILD_EXT_DIR=agent/native/_build/linuxmusl-${BUILD_ARCH}-release/ext/
+	BUILD_LOADER_DIR=agent/native/_build/linuxmusl-${BUILD_ARCH}-release/loader/code/
 	createPackage
 
-	NAME="${NAME_BACKUP}-debugsymbols-linuxmusl-x86-64"
+	NAME="${NAME_BACKUP}-debugsymbols-linuxmusl-${BUILD_ARCH}"
 	createDebugPackage
 else
 	createPackage


Test patch:
diff --git a/.github/workflows/test-packages.yml b/.github/workflows/test-packages.yml
index 070c2862..4392822d 100644
--- a/.github/workflows/test-packages.yml
+++ b/.github/workflows/test-packages.yml
@@ -73,6 +73,11 @@ jobs:
           name: package-parts-linuxmusl-x86-64
           path: agent/native/_build/linuxmusl-x86-64-release
 
+      - name: Remove ARM64 artifacts
+        run: |
+          rm ${BUILD_PACKAGES}/apm-agent-php*arm64*
+          rm ${BUILD_PACKAGES}/apm-agent-php*aarch64*
+
       - if: ${{ env.TESTING_TYPE == 'lifecycle' }}
         name: lifecycle test
         run: |
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3e35dcff..d4d9d465 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,6 +1,12 @@
 name: test
 
 on:
+  workflow_call:
+    inputs:
+      build_arch:
+        required: true
+        type: string
+        default: 'all'
   pull_request:
     paths-ignore:
       - "**/*.asciidoc"
@@ -26,13 +32,21 @@ concurrency:
 jobs:
   build:
     uses: ./.github/workflows/build.yml
+    with:
+      build_arch: all
+
+  setup-build-matrix:
+    uses: ./.github/workflows/build-arch-matrix-generator.yml
+    with:
+      build_arch: all
 
   phpt-tests:
     name: phpt-tests
     runs-on: ubuntu-latest
     needs:
       - build
-    timeout-minutes: 10
+      - setup-build-matrix
+    timeout-minutes: 120
     strategy:
       fail-fast: false
       matrix:
@@ -44,25 +58,27 @@ jobs:
           - "8.1"
           - "8.2"
           - "8.3"
-        arch:
-          - "linux-x86-64"
-          - "linuxmusl-x86-64"
+        data: ${{ fromJson(needs.setup-build-matrix.outputs.matrix-combinations).include }}
     env:
       PHP_VERSION: ${{ matrix.php-version }}
-      BUILD_ARCHITECTURE: ${{ matrix.arch }}
+      BUILD_ARCHITECTURE: ${{ matrix.data.arch }}
     steps:
       - uses: actions/checkout@v4
-      - name: Prepare
-        run: make -f .ci/Makefile prepare
       - uses: actions/download-artifact@v3
         with:
-          name: package-parts-${{ matrix.arch }}
-          path: agent/native/_build/${{ matrix.arch }}-release/
-      - name: Display structure of downloaded files
-        run: ls -R
+          name: package-parts-${{ matrix.data.arch }}
+          path: agent/native/_build/${{ matrix.data.arch }}-release/
+      - if: ${{ matrix.data.run_qemu }}
+        uses: docker/setup-qemu-action@v3
+        with:
+          platforms: arm64
+      - name: Prepare
+        run: make -f .ci/Makefile prepare
       - name: phpt-unit-tests
-        run: make -f .ci/Makefile run-phpt-tests
-
+        run: |
+          tree agent/native/_build/
+          uname -a
+          make -f .ci/Makefile run-phpt-tests
   static-checks-unit-tests:
     name: static-checks-unit-tests
     runs-on: ubuntu-latest
diff --git a/agent/native/libcommon/test/PeriodicTaskExecutorTest.cpp b/agent/native/libcommon/test/PeriodicTaskExecutorTest.cpp
index 0e77b926..b196df54 100644
--- a/agent/native/libcommon/test/PeriodicTaskExecutorTest.cpp
+++ b/agent/native/libcommon/test/PeriodicTaskExecutorTest.cpp
@@ -47,7 +47,13 @@ TEST(PeriodicTaskExecutorTest, resumeAfterFork) {
 
     periodicTaskExecutor_.setInterval(20ms);
 
-    pthread_atfork(fh_prepare, fh_parent, fh_child);
+
+    static bool pthread_atfork_called = false;
+
+    if (!pthread_atfork_called) {
+        pthread_atfork(fh_prepare, fh_parent, fh_child);
+        pthread_atfork_called = true;
+    }
 
     periodicTaskExecutor_.resumePeriodicTasks();
     std::this_thread::sleep_for(100ms);
@@ -60,7 +66,7 @@ TEST(PeriodicTaskExecutorTest, resumeAfterFork) {
 
     ASSERT_GE(counter.load(), 13); // should be 15 in ideal world
     if (pid == 0) {
-        exit(0);
+        exit(testing::Test::HasFailure());
     }
 }