Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/asan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: ASAN (AddressSanitizer & LeakSanitizer)

# Memory error and leak detection using AddressSanitizer and LeakSanitizer
# This workflow builds memtier_benchmark with sanitizers enabled and runs
# the full test suite to detect memory leaks and address errors.

on: [push, pull_request]

jobs:
test-with-sanitizers:
runs-on: ubuntu-latest
name: Memory leak detection (ASAN/LSAN)
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install build dependencies
run: |
sudo apt-get -qq update
sudo apt-get install -y \
build-essential \
autoconf \
automake \
pkg-config \
libevent-dev \
zlib1g-dev \
libssl-dev

- name: Build with sanitizers
run: |
autoreconf -ivf
./configure --enable-sanitizers
make -j

- name: Verify ASAN is enabled
run: |
ldd ./memtier_benchmark | grep asan
echo "✓ AddressSanitizer is linked"

- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
architecture: x64

- name: Install Python test dependencies
run: pip install -r ./tests/test_requirements.txt

- name: Install Redis
run: |
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get -qq update
sudo apt-get install redis
sudo service redis-server stop

- name: Increase connection limit
run: |
sudo sysctl -w net.ipv4.tcp_fin_timeout=10
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
ulimit -n 40960

- name: Generate TLS test certificates
run: ./tests/gen-test-certs.sh

- name: Test OSS TCP with ASAN
timeout-minutes: 10
run: |
ASAN_OPTIONS=detect_leaks=1 ./tests/run_tests.sh

- name: Test OSS TCP TLS with ASAN
timeout-minutes: 10
run: |
ASAN_OPTIONS=detect_leaks=1 TLS=1 ./tests/run_tests.sh

- name: Test OSS TCP TLS v1.2 with ASAN
timeout-minutes: 10
run: |
ASAN_OPTIONS=detect_leaks=1 TLS_PROTOCOLS='TLSv1.2' TLS=1 ./tests/run_tests.sh

- name: Test OSS TCP TLS v1.3 with ASAN
timeout-minutes: 10
run: |
ASAN_OPTIONS=detect_leaks=1 TLS_PROTOCOLS='TLSv1.3' TLS=1 ./tests/run_tests.sh

- name: Test OSS-CLUSTER TCP with ASAN
timeout-minutes: 10
run: |
ASAN_OPTIONS=detect_leaks=1 OSS_STANDALONE=0 OSS_CLUSTER=1 ./tests/run_tests.sh

- name: Test OSS-CLUSTER TCP TLS with ASAN
timeout-minutes: 10
run: |
ASAN_OPTIONS=detect_leaks=1 OSS_STANDALONE=0 OSS_CLUSTER=1 TLS=1 ./tests/run_tests.sh

21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ To understand what test options are available simply run:

$ ./tests/run_tests.sh --help


**Memory leak detection with sanitizers**


memtier_benchmark supports building with AddressSanitizer (ASAN) and LeakSanitizer (LSAN) to detect memory errors and leaks during testing.

To build with sanitizers enabled:

$ ./configure --enable-sanitizers
$ make

To run tests with leak detection:

$ ASAN_OPTIONS=detect_leaks=1 ./tests/run_tests.sh

If memory leaks or errors are detected, tests will fail with detailed error messages showing the location of the issue.

To verify ASAN is enabled:

$ ldd ./memtier_benchmark | grep asan

## Using Docker

Use available images on Docker Hub:
Expand Down
10 changes: 10 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ AS_IF([test "x$enable_tls" != "xno"], [
AC_SUBST(LIBCRYPTO_CFLAGS) AC_SUBST(LIBCRYPTO_LIBS))
], [])

# Sanitizers support (ASAN/LSAN) is optional.
AC_ARG_ENABLE([sanitizers],
[AS_HELP_STRING([--enable-sanitizers],
[Enable AddressSanitizer and LeakSanitizer for memory error detection])])
AS_IF([test "x$enable_sanitizers" = "xyes"], [
AC_MSG_NOTICE([Enabling AddressSanitizer and LeakSanitizer])
CXXFLAGS="$CXXFLAGS -fsanitize=address -fsanitize=leak -fno-omit-frame-pointer -O1"
LDFLAGS="$LDFLAGS -fsanitize=address -fsanitize=leak"
], [])

# clock_gettime requires -lrt on old glibc only.
AC_SEARCH_LIBS([clock_gettime], [rt], , AC_MSG_ERROR([rt is required libevent.]))

Expand Down
10 changes: 10 additions & 0 deletions memtier_benchmark.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1956,6 +1956,16 @@ int main(int argc, char *argv[])
delete cfg.arbitrary_commands;
}

// Clean up dynamically allocated strings from URI parsing
if (cfg.uri) {
if (cfg.server) {
free((void*)cfg.server);
}
if (cfg.authenticate) {
free((void*)cfg.authenticate);
}
}

#ifdef USE_TLS
if(cfg.tls) {
if (cfg.openssl_ctx) {
Expand Down
88 changes: 88 additions & 0 deletions tests/mb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
Simple replacement for mbdirector package.
Contains only the Benchmark and RunConfig classes needed for tests.
"""
import os
import subprocess
import logging


class RunConfig(object):
"""Configuration for a benchmark run."""
next_id = 1

def __init__(self, base_results_dir, name, config, benchmark_config):
self.id = RunConfig.next_id
RunConfig.next_id += 1

self.redis_process_port = config.get('redis_process_port', 6379)

mbconfig = config.get('memtier_benchmark', {})
mbconfig.update(benchmark_config)
self.mb_binary = mbconfig.get('binary', 'memtier_benchmark')
self.mb_threads = mbconfig.get('threads')
self.mb_clients = mbconfig.get('clients')
self.mb_pipeline = mbconfig.get('pipeline')
self.mb_requests = mbconfig.get('requests')
self.mb_test_time = mbconfig.get('test_time')
self.explicit_connect_args = bool(
mbconfig.get('explicit_connect_args'))

self.results_dir = os.path.join(base_results_dir,
'{:04}_{}'.format(self.id, name))

def __repr__(self):
return '<RunConfig id={}>'.format(self.id)


class Benchmark(object):
"""Benchmark runner for memtier_benchmark."""

def __init__(self, config, **kwargs):
self.config = config
self.binary = self.config.mb_binary
self.name = kwargs['name']

# Configure
self.args = [self.binary]
if not self.config.explicit_connect_args:
self.args += ['--server', '127.0.0.1',
'--port', str(self.config.redis_process_port)
]
self.args += ['--out-file', os.path.join(config.results_dir,
'mb.stdout'),
'--json-out-file', os.path.join(config.results_dir,
'mb.json')]

if self.config.mb_threads is not None:
self.args += ['--threads', str(self.config.mb_threads)]
if self.config.mb_clients is not None:
self.args += ['--clients', str(self.config.mb_clients)]
if self.config.mb_pipeline is not None:
self.args += ['--pipeline', str(self.config.mb_pipeline)]
if self.config.mb_requests is not None:
self.args += ['--requests', str(self.config.mb_requests)]
if self.config.mb_test_time is not None:
self.args += ['--test-time', str(self.config.mb_test_time)]

self.args += kwargs['args']

@classmethod
def from_json(cls, config, json):
return cls(config, **json)

def write_file(self, name, data):
with open(os.path.join(self.config.results_dir, name), 'wb') as outfile:
outfile.write(data)

def run(self):
logging.debug(' Command: %s', ' '.join(self.args))
process = subprocess.Popen(
stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
executable=self.binary, args=self.args)
_stdout, _stderr = process.communicate()
if _stderr:
logging.debug(' >>> stderr <<<\n%s\n', _stderr)
self.write_file('mb.stderr', _stderr)
return process.wait() == 0

3 changes: 1 addition & 2 deletions tests/test_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
redis>=3.0.0
rltest==0.6.0
git+https://github.com/RedisLabs/mbdirector.git@master
rltest>=0.7.17
3 changes: 1 addition & 2 deletions tests/tests_oss_simple_flow.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import tempfile
import json
from include import *
from mbdirector.benchmark import Benchmark
from mbdirector.runner import RunConfig
from mb import Benchmark, RunConfig


def test_preload_and_set_get(env):
Expand Down
3 changes: 1 addition & 2 deletions tests/tests_oss_zipfian_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
agg_info_commandstats,
assert_minimum_memtier_outcomes
)
from mbdirector.benchmark import Benchmark
from mbdirector.runner import RunConfig
from mb import Benchmark, RunConfig


def correlation_coeficient(x: list[float], y: list[float]) -> float:
Expand Down
3 changes: 1 addition & 2 deletions tests/zipfian_benchmark_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
assert_minimum_memtier_outcomes,
get_expected_request_count,
)
from mbdirector.benchmark import Benchmark
from mbdirector.runner import RunConfig
from mb import Benchmark, RunConfig


class MonitorThread(threading.Thread):
Expand Down
Loading