From c550167d9ca9b5654c1b78e9635f7a4f7311cf92 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Thu, 10 Nov 2022 15:08:17 +0800 Subject: [PATCH] Add plugin for phpredis. (#29) --- .github/workflows/rust.yml | 5 +- Cargo.lock | 6 +- Cargo.toml | 1 + README.md | 2 +- .../service-agent/php-agent/Supported-list.md | 1 + src/component.rs | 1 + src/lib.rs | 1 + src/plugin/mod.rs | 2 + src/plugin/plugin_redis.rs | 372 ++++++++++++++++++ src/tag.rs | 25 ++ src/util.rs | 72 +++- tests/data/expected_context.yaml | 149 ++++++- tests/e2e.rs | 15 + tests/php/fpm/redis.fail.php | 30 ++ tests/php/fpm/redis.succ.php | 32 ++ 15 files changed, 707 insertions(+), 7 deletions(-) create mode 100644 src/plugin/plugin_redis.rs create mode 100644 src/tag.rs create mode 100644 tests/php/fpm/redis.fail.php create mode 100644 tests/php/fpm/redis.succ.php diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0430487..b407ec1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -85,8 +85,9 @@ jobs: tools: php-config, composer:v2 extensions: > bcmath, calendar, ctype, dom, exif, gettext, iconv, intl, json, mbstring, - mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline, memcached, - swoole-${{ matrix.version.swoole }}, xml, xmlreader, xmlwriter, yaml, zip + mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline, redis, + memcached, swoole-${{ matrix.version.swoole }}, xml, xmlreader, xmlwriter, + yaml, zip - name: Setup php-fpm for Linux if: matrix.os == 'ubuntu-20.04' diff --git a/Cargo.lock b/Cargo.lock index 1b08c5b..3000865 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1782,10 +1782,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1885,6 +1886,7 @@ dependencies = [ "phper", "prost", "reqwest", + "serde_json", "skywalking", "systemstat", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 79875cb..788c3e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ libc = "0.2.132" once_cell = "1.14.0" phper = "0.5.1" prost = "0.11.0" +serde_json = { version = "1.0.87", features = ["preserve_order"] } skywalking = "0.4.0" systemstat = "0.2.0" tempfile = "3.3.0" diff --git a/README.md b/README.md index cc757df..0d39aae 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ SkyWalking PHP Agent requires SkyWalking 8.4+ and PHP 7.2+ * [x] [PDO](https://www.php.net/manual/en/book.pdo.php) * [x] [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php) * [x] [Memcached](https://www.php.net/manual/en/book.memcached.php) - * [ ] [phpredis](https://github.com/phpredis/phpredis) + * [x] [phpredis](https://github.com/phpredis/phpredis) * [ ] [php-amqp](https://github.com/php-amqp/php-amqp) * [ ] [php-rdkafka](https://github.com/arnaud-lb/php-rdkafka) * [x] [predis](https://github.com/predis/predis) diff --git a/docs/en/setup/service-agent/php-agent/Supported-list.md b/docs/en/setup/service-agent/php-agent/Supported-list.md index d4227e7..4e11b15 100644 --- a/docs/en/setup/service-agent/php-agent/Supported-list.md +++ b/docs/en/setup/service-agent/php-agent/Supported-list.md @@ -13,6 +13,7 @@ The following plugins provide the distributed tracing capability. * [PDO](https://www.php.net/manual/en/book.pdo.php) * [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php) * [Memcached](https://www.php.net/manual/en/book.memcached.php) +* [phpredis](https://github.com/phpredis/phpredis) ## Support PHP library diff --git a/src/component.rs b/src/component.rs index cb6ab19..b6aaf08 100644 --- a/src/component.rs +++ b/src/component.rs @@ -23,3 +23,4 @@ pub const COMPONENT_PHP_PDO_ID: i32 = 8003; pub const COMPONENT_PHP_MYSQLI_ID: i32 = 8004; pub const COMPONENT_PHP_PREDIS_ID: i32 = 8006; pub const COMPONENT_PHP_MEMCACHED_ID: i32 = 20; +pub const COMPONENT_PHP_REDIS_ID: i32 = 7; diff --git a/src/lib.rs b/src/lib.rs index dd97960..6569149 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod execute; mod module; mod plugin; mod request; +mod tag; mod util; mod worker; diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 5b16556..070185c 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -18,6 +18,7 @@ mod plugin_memcached; mod plugin_mysqli; mod plugin_pdo; mod plugin_predis; +mod plugin_redis; mod plugin_swoole; use crate::execute::{AfterExecuteHook, BeforeExecuteHook}; @@ -33,6 +34,7 @@ static PLUGINS: Lazy>> = Lazy::new(|| { Box::new(plugin_swoole::SwooleHttpResponsePlugin::default()), Box::new(plugin_predis::PredisPlugin::default()), Box::new(plugin_memcached::MemcachedPlugin::default()), + Box::new(plugin_redis::RedisPlugin::default()), ] }); diff --git a/src/plugin/plugin_redis.rs b/src/plugin/plugin_redis.rs new file mode 100644 index 0000000..e376d07 --- /dev/null +++ b/src/plugin/plugin_redis.rs @@ -0,0 +1,372 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{any::Any, collections::HashSet}; + +use super::Plugin; +use crate::{ + component::COMPONENT_PHP_REDIS_ID, + context::RequestContext, + execute::{get_this_mut, AfterExecuteHook, BeforeExecuteHook, Noop}, + tag::{TAG_CACHE_CMD, TAG_CACHE_KEY, TAG_CACHE_OP, TAG_CACHE_TYPE}, +}; +use anyhow::Context; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use phper::{ + eg, + objects::ZObj, + sys, + values::{ExecuteData, ZVal}, +}; +use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span}; +use tracing::{debug, warn}; + +static PEER_MAP: Lazy> = Lazy::new(Default::default); + +static FREE_MAP: Lazy> = Lazy::new(Default::default); + +static REDIS_READ_COMMANDS: Lazy> = Lazy::new(|| { + [ + "blPop", + "brPop", + "get", + "getBit", + "getKeys", + "getMultiple", + "getRange", + "hExists", + "hGet", + "hGetAll", + "hKeys", + "hLen", + "hMGet", + "hScan", + "hStrLen", + "hVals", + "keys", + "lGet", + "lGetRange", + "lLen", + "lRange", + "lSize", + "mGet", + "sContains", + "sGetMembers", + "sIsMember", + "sMembers", + "sScan", + "sSize", + "strLen", + "zCount", + "zRange", + "zRangeByLex", + "zRangeByScore", + "zScan", + "zSize", + ] + .into_iter() + .map(str::to_ascii_lowercase) + .collect() +}); + +static REDIS_WRITE_COMMANDS: Lazy> = Lazy::new(|| { + [ + "append", + "bRPopLPush", + "decr", + "decrBy", + "del", + "delete", + "hDel", + "hIncrBy", + "hIncrByFloat", + "hMSet", + "hSet", + "hSetNx", + "incr", + "incrBy", + "incrByFloat", + "lInsert", + "lPush", + "lPushx", + "lRem", + "lRemove", + "lSet", + "lTrim", + "listTrim", + "mSet", + "mSetNX", + "pSetEx", + "rPopLPush", + "rPush", + "rPushX", + "randomKey", + "sAdd", + "sInter", + "sInterStore", + "sMove", + "sRandMember", + "sRem", + "sRemove", + "set", + "setBit", + "setEx", + "setNx", + "setRange", + "setTimeout", + "sort", + "unlink", + "zAdd", + "zDelete", + "zDeleteRangeByRank", + "zDeleteRangeByScore", + "zIncrBy", + "zRem", + "zRemRangeByRank", + "zRemRangeByScore", + "zRemove", + "zRemoveRangeByScore", + ] + .into_iter() + .map(str::to_ascii_lowercase) + .collect() +}); + +static REDIS_ALL_COMMANDS: Lazy> = Lazy::new(|| { + let mut commands = HashSet::new(); + commands.extend(REDIS_READ_COMMANDS.iter().map(Clone::clone)); + commands.extend(REDIS_WRITE_COMMANDS.iter().map(Clone::clone)); + commands +}); + +#[derive(Default, Clone)] +pub struct RedisPlugin; + +impl Plugin for RedisPlugin { + #[inline] + fn class_names(&self) -> Option<&'static [&'static str]> { + Some(&["Redis"]) + } + + #[inline] + fn function_name_prefix(&self) -> Option<&'static str> { + None + } + + fn hook( + &self, class_name: Option<&str>, function_name: &str, + ) -> Option<(Box, Box)> { + match (class_name, function_name) { + (Some("Redis"), "__construct") => Some(self.hook_redis_construct()), + (Some(class_name @ "Redis"), f) + if ["connect", "open", "pconnect", "popen"].contains(&f) => + { + Some(self.hook_redis_connect(class_name, function_name)) + } + (Some(class_name @ "Redis"), f) + if REDIS_ALL_COMMANDS.contains(&f.to_ascii_lowercase()) => + { + Some(self.hook_redis_methods(class_name, function_name)) + } + _ => None, + } + } +} + +impl RedisPlugin { + /// TODO Support first optional argument as config for phpredis 6.0+. + /// + fn hook_redis_construct(&self) -> (Box, Box) { + ( + Box::new(|_, execute_data| { + let this = get_this_mut(execute_data)?; + hack_free(this, Some(redis_dtor)); + + Ok(Box::new(())) + }), + Noop::noop(), + ) + } + + fn hook_redis_connect( + &self, class_name: &str, function_name: &str, + ) -> (Box, Box) { + let class_name = class_name.to_owned(); + let function_name = function_name.to_owned(); + ( + Box::new(move |request_id, execute_data| { + if execute_data.num_args() < 2 { + debug!("argument count less than 2, skipped."); + return Ok(Box::new(())); + } + + let host = { + let mut f = || { + Ok::<_, anyhow::Error>( + execute_data + .get_parameter(0) + .as_z_str() + .context("isn't string")? + .to_str()? + .to_owned(), + ) + }; + match f() { + Ok(host) => host, + Err(err) => { + warn!(?err, "parse first argument to host failed, skipped."); + return Ok(Box::new(())); + } + } + }; + let port = { + let mut f = || { + execute_data + .get_parameter(1) + .as_long() + .context("isn't long") + }; + match f() { + Ok(port) => port, + Err(err) => { + warn!(?err, "parse second argument to port failed, skipped."); + return Ok(Box::new(())); + } + } + }; + + let this = get_this_mut(execute_data)?; + let addr = format!("{}:{}", host, port); + debug!(addr, "Get redis peer"); + PEER_MAP.insert(this.handle(), Peer { addr: addr.clone() }); + + let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| { + Ok(ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &addr)) + })?; + + span.with_span_object_mut(|span| { + span.set_span_layer(SpanLayer::Cache); + span.component_id = COMPONENT_PHP_REDIS_ID; + span.add_tag(TAG_CACHE_TYPE, "redis"); + }); + + Ok(Box::new(span)) + }), + Box::new(after_hook), + ) + } + + fn hook_redis_methods( + &self, class_name: &str, function_name: &str, + ) -> (Box, Box) { + let class_name = class_name.to_owned(); + let function_name = function_name.to_owned(); + ( + Box::new(move |request_id, execute_data| { + let handle = get_this_mut(execute_data)?.handle(); + debug!(handle, function_name, "call redis method"); + let peer = PEER_MAP + .get(&handle) + .map(|r| r.value().addr.clone()) + .unwrap_or_default(); + + let key = execute_data + .get_parameter(0) + .as_z_str() + .and_then(|s| s.to_str().ok()); + let op = if REDIS_READ_COMMANDS.contains(&function_name.to_ascii_lowercase()) { + "read" + } else { + "write" + }; + + debug!(handle, function_name, key, op, "call redis command"); + + let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| { + Ok(ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &peer)) + })?; + + span.with_span_object_mut(|span| { + span.set_span_layer(SpanLayer::Cache); + span.component_id = COMPONENT_PHP_REDIS_ID; + span.add_tag(TAG_CACHE_TYPE, "redis"); + span.add_tag(TAG_CACHE_CMD, function_name); + span.add_tag(TAG_CACHE_OP, op); + if let Some(key) = key { + span.add_tag(TAG_CACHE_KEY, key) + } + }); + + Ok(Box::new(span)) + }), + Box::new(after_hook), + ) + } +} + +struct Peer { + addr: String, +} + +fn hack_free(this: &mut ZObj, new_free: sys::zend_object_free_obj_t) { + let handle = this.handle(); + + unsafe { + let ori_free = (*(*this.as_mut_ptr()).handlers).free_obj; + FREE_MAP.insert(handle, ori_free); + (*((*this.as_mut_ptr()).handlers as *mut sys::zend_object_handlers)).free_obj = new_free; + } +} + +unsafe extern "C" fn redis_dtor(object: *mut sys::zend_object) { + debug!("call Redis free"); + + let handle = ZObj::from_ptr(object).handle(); + + PEER_MAP.remove(&handle); + if let Some((_, Some(free))) = FREE_MAP.remove(&handle) { + free(object); + } +} + +fn after_hook( + _request_id: Option, span: Box, _execute_data: &mut ExecuteData, + _return_value: &mut ZVal, +) -> anyhow::Result<()> { + let mut span = span.downcast::().unwrap(); + + let ex = unsafe { ZObj::try_from_mut_ptr(eg!(exception)) }; + if let Some(ex) = ex { + span.with_span_object_mut(|span| { + span.is_error = true; + + let mut logs = Vec::new(); + if let Ok(class_name) = ex.get_class().get_name().to_str() { + logs.push(("Exception Class", class_name.to_owned())); + } + if let Some(message) = ex.get_property("message").as_z_str() { + if let Ok(message) = message.to_str() { + logs.push(("Exception Message", message.to_owned())); + } + } + if !logs.is_empty() { + span.add_log(logs); + } + }); + } + + Ok(()) +} diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..69ca800 --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,25 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License.. + +//! Tags +//! +//! Virtual Cache +//! +//! https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-cache/ + +pub const TAG_CACHE_TYPE: &str = "cache.type"; +pub const TAG_CACHE_OP: &str = "cache.op"; +pub const TAG_CACHE_CMD: &str = "cache.cmd"; +pub const TAG_CACHE_KEY: &str = "cache.key"; diff --git a/src/util.rs b/src/util.rs index 460c39f..1643780 100644 --- a/src/util.rs +++ b/src/util.rs @@ -16,7 +16,8 @@ use anyhow::bail; use chrono::Local; use once_cell::sync::Lazy; -use phper::{sys, values::ZVal}; +use phper::{arrays::IterKey, sys, values::ZVal}; +use serde_json::{json, Number, Value}; use std::{ ffi::CStr, panic::{catch_unwind, UnwindSafe}, @@ -112,3 +113,72 @@ pub fn catch_unwind_anyhow anyhow::Result + UnwindSafe, R>( pub fn get_sapi_module_name() -> &'static CStr { unsafe { CStr::from_ptr(sys::sapi_module.name) } } + +/// Use for later scene. +#[allow(dead_code)] +pub fn json_encode_values(values: &[ZVal]) -> serde_json::Result { + fn add(json_value: &mut Value, key: Option, item: Value) { + match key { + Some(key) => { + json_value.as_object_mut().unwrap().insert(key, item); + } + None => { + json_value.as_array_mut().unwrap().push(item); + } + } + } + + fn handle(json_value: &mut Value, key: Option, val: &ZVal) { + let type_info = val.get_type_info(); + + if type_info.is_null() { + add(json_value, key, Value::Null); + } else if type_info.is_true() { + add(json_value, key, Value::Bool(true)); + } else if type_info.is_false() { + add(json_value, key, Value::Bool(false)); + } else if type_info.is_long() { + let i = val.as_long().unwrap(); + add(json_value, key, Value::Number(i.into())); + } else if type_info.is_double() { + let d = val.as_double().unwrap(); + let n = match Number::from_f64(d) { + Some(n) => Value::Number(n), + None => Value::String("".to_owned()), + }; + add(json_value, key, n); + } else if type_info.is_string() { + let s = val + .as_z_str() + .unwrap() + .to_str() + .map(ToOwned::to_owned) + .unwrap_or_default(); + add(json_value, key, Value::String(s)); + } else if type_info.is_array() { + let arr = val.as_z_arr().unwrap(); + let is_arr = arr.iter().all(|(key, _)| matches!(key, IterKey::Index(_))); + let mut new_json_value = if is_arr { json!([]) } else { json!({}) }; + for (key, new_val) in arr.iter() { + if is_arr { + handle(&mut new_json_value, None, new_val); + } else { + let key = match key { + IterKey::Index(i) => i.to_string(), + IterKey::ZStr(s) => s.to_str().map(ToOwned::to_owned).unwrap_or_default(), + }; + handle(&mut new_json_value, Some(key), new_val); + } + } + add(json_value, key, new_json_value); + } else if type_info.is_object() { + add(json_value, key, Value::String("".to_owned())); + } + } + + let mut json_value = json!([]); + for val in values { + handle(&mut json_value, None, val); + } + serde_json::to_string(&json_value) +} diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml index f5ca94a..f366211 100644 --- a/tests/data/expected_context.yaml +++ b/tests/data/expected_context.yaml @@ -15,7 +15,7 @@ segmentItems: - serviceName: skywalking-agent-test-1 - segmentSize: 10 + segmentSize: 12 segments: - segmentId: "not null" spans: @@ -681,6 +681,153 @@ segmentItems: - { key: url, value: /memcached.php } - { key: http.method, value: GET } - { key: http.status_code, value: "200" } + - segmentId: "not null" + spans: + - operationName: Redis->connect + parentSpanId: 0 + spanId: 1 + spanLayer: Cache + startTime: gt 0 + endTime: gt 0 + componentId: 7 + isError: false + spanType: Exit + peer: "127.0.0.1:6379" + skipAnalysis: false + tags: + - key: cache.type + value: redis + - operationName: Redis->mset + parentSpanId: 0 + spanId: 2 + spanLayer: Cache + startTime: gt 0 + endTime: gt 0 + componentId: 7 + isError: false + spanType: Exit + peer: 127.0.0.1:6379 + skipAnalysis: false + tags: + - key: cache.type + value: redis + - key: cache.cmd + value: mset + - key: cache.op + value: write + - operationName: Redis->get + parentSpanId: 0 + spanId: 3 + spanLayer: Cache + startTime: gt 0 + endTime: gt 0 + componentId: 7 + isError: false + spanType: Exit + peer: 127.0.0.1:6379 + skipAnalysis: false + tags: + - key: cache.type + value: redis + - key: cache.cmd + value: get + - key: cache.op + value: read + - key: cache.key + value: key0 + - operationName: Redis->get + parentSpanId: 0 + spanId: 4 + spanLayer: Cache + startTime: gt 0 + endTime: gt 0 + componentId: 7 + isError: false + spanType: Exit + peer: 127.0.0.1:6379 + skipAnalysis: false + tags: + - key: cache.type + value: redis + - key: cache.cmd + value: get + - key: cache.op + value: read + - key: cache.key + value: key1 + - operationName: GET:/redis.succ.php + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 8001 + isError: false + spanType: Entry + peer: "" + skipAnalysis: false + tags: + - { key: url, value: /redis.succ.php } + - { key: http.method, value: GET } + - { key: http.status_code, value: "200" } + - segmentId: "not null" + spans: + - operationName: Redis->connect + parentSpanId: 0 + spanId: 1 + spanLayer: Cache + startTime: gt 0 + endTime: gt 0 + componentId: 7 + isError: false + spanType: Exit + peer: "127.0.0.1:6379" + skipAnalysis: false + tags: + - key: cache.type + value: redis + - operationName: Redis->set + parentSpanId: 0 + spanId: 2 + spanLayer: Cache + startTime: gt 0 + endTime: gt 0 + componentId: 7 + isError: true + spanType: Exit + peer: 127.0.0.1:6379 + skipAnalysis: false + tags: + - key: cache.type + value: redis + - key: cache.cmd + value: set + - key: cache.op + value: write + - key: cache.key + value: foo + logs: + - logEvent: + - { key: Exception Class, value: RedisException } + - { + key: Exception Message, + value: NOAUTH Authentication required., + } + - operationName: GET:/redis.fail.php + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 8001 + isError: false + spanType: Entry + peer: "" + skipAnalysis: false + tags: + - { key: url, value: /redis.fail.php } + - { key: http.method, value: GET } + - { key: http.status_code, value: "200" } - serviceName: skywalking-agent-test-2 segmentSize: 1 segments: diff --git a/tests/e2e.rs b/tests/e2e.rs index a15671b..bc3f876 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -52,6 +52,7 @@ async fn run_e2e() { request_fpm_predis().await; request_fpm_mysqli().await; request_fpm_memcached().await; + request_fpm_redis().await; request_swoole_curl().await; sleep(Duration::from_secs(3)).await; request_collector_validate().await; @@ -97,6 +98,20 @@ async fn request_fpm_memcached() { .await; } +async fn request_fpm_redis() { + request_common( + HTTP_CLIENT.get(format!("http://{}/redis.succ.php", PROXY_SERVER_1_ADDRESS)), + "ok", + ) + .await; + + request_common( + HTTP_CLIENT.get(format!("http://{}/redis.fail.php", PROXY_SERVER_1_ADDRESS)), + "ok", + ) + .await; +} + async fn request_swoole_curl() { request_common( HTTP_CLIENT.get(format!("http://{}/curl", SWOOLE_SERVER_1_ADDRESS)), diff --git a/tests/php/fpm/redis.fail.php b/tests/php/fpm/redis.fail.php new file mode 100644 index 0000000..be6610f --- /dev/null +++ b/tests/php/fpm/redis.fail.php @@ -0,0 +1,30 @@ +connect("127.0.0.1", 6379); + $client->set('foo', 'bar'); +} catch (RedisException $e) { +} + +echo "ok"; diff --git a/tests/php/fpm/redis.succ.php b/tests/php/fpm/redis.succ.php new file mode 100644 index 0000000..1abc743 --- /dev/null +++ b/tests/php/fpm/redis.succ.php @@ -0,0 +1,32 @@ +connect("127.0.0.1", 6379); + $client->auth('password'); + $client->mSet(['key0' => 'value0', 'key1' => 'value1']); + Assert::same($client->get('key0'), 'value0'); + Assert::same($client->get('key1'), 'value1'); +} + +echo "ok";