From c488171a016c74820fd40b9df105606ca3509e01 Mon Sep 17 00:00:00 2001 From: Tobias Bengtsson Date: Thu, 9 Nov 2023 20:58:14 +0100 Subject: [PATCH] Make APC updateGauge with set command lock-free in critical path When setting a value on a gauge, apcu_store is always called two times, taking a write lock each that can cause contention. - Replace apcu_store with compare-and-swap algorithm unless we store for the first time(s) - Eagerly fetch old value instead of apcu_exists, since we will anyway need it for the compare-and-swap algorithms that follows, saving one call to APCu - Make sure we never enter an infinite loop in the compare-and-swap section by falling back to a first-time insert if $old === false. A fix for the potential infinite loop below will be done in a separate commit. Signed-off-by: Tobias Bengtsson --- src/Prometheus/Storage/APC.php | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Prometheus/Storage/APC.php b/src/Prometheus/Storage/APC.php index c45f1940..c8a3933f 100644 --- a/src/Prometheus/Storage/APC.php +++ b/src/Prometheus/Storage/APC.php @@ -123,11 +123,32 @@ public function updateSummary(array $data): void public function updateGauge(array $data): void { $valueKey = $this->valueKey($data); + $old = apcu_fetch($valueKey); if ($data['command'] === Adapter::COMMAND_SET) { - apcu_store($valueKey, $this->toBinaryRepresentationAsInteger($data['value'])); - apcu_store($this->metaKey($data), json_encode($this->metaData($data))); + $new = $this->toBinaryRepresentationAsInteger($data['value']); + if ($old === false) { + apcu_store($valueKey, $new); + apcu_store($this->metaKey($data), json_encode($this->metaData($data))); + return; + } else { + // Taken from https://github.com/prometheus/client_golang/blob/66058aac3a83021948e5fb12f1f408ff556b9037/prometheus/value.go#L91 + while (true) { + if ($old !== false) { + if (apcu_cas($valueKey, $old, $new)) { + return; + } else { + $old = apcu_fetch($valueKey); + } + } else { + // Cache got evicted under our feet? Just consider it a fresh/new insert and move on. + apcu_store($valueKey, $new); + apcu_store($this->metaKey($data), json_encode($this->metaData($data))); + return; + } + } + } } else { - if (!apcu_exists($valueKey)) { + if ($old === false) { $new = apcu_add($valueKey, $this->toBinaryRepresentationAsInteger(0)); if ($new) { apcu_store($this->metaKey($data), json_encode($this->metaData($data)));