Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grcooper/non block buffers #1017

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dfdd8f1
example adding support for arbitrary meta flags and for getting them …
danmayer Oct 16, 2024
23d36b9
fix rubocop
danmayer Oct 16, 2024
9929eb7
fix specs for ruby / rack head
danmayer Oct 16, 2024
45aaa77
additional tests on meta get with flags
danmayer Oct 16, 2024
a1840e1
add example benchmark showing Dalli doesn't optimally handle large st…
danmayer Oct 16, 2024
707a361
fix for Ruby head tests
danmayer Oct 17, 2024
d545443
Merge pull request #5 from Shopify/add_initial_benchmark
danmayer Oct 21, 2024
dcbc756
optimize multi_set for faster command and single node ring
danmayer Oct 22, 2024
6271dfa
alternative approach to multi commands that doesn't require extra MN …
danmayer Oct 24, 2024
c780b56
Merge branch 'main' into example_meta_get_with_meta_flags
danmayer Oct 24, 2024
1cf3ad2
additional benchmarks
danmayer Oct 24, 2024
45c72ce
rubocop
danmayer Oct 24, 2024
b3eb73f
ruby only stackprof
danmayer Oct 24, 2024
6ed8d63
improved meta_set
danmayer Oct 24, 2024
0eb7c2f
perf fix for get_multi
danmayer Oct 25, 2024
af716a1
fix rubocop
danmayer Oct 25, 2024
a75d37d
different multi read which is compatible with our gateway
danmayer Oct 25, 2024
3411cca
Merge pull request #4 from Shopify/example_meta_get_with_meta_flags
danmayer Oct 28, 2024
cafc2e6
Call request from pipelined commands
grcooper Oct 29, 2024
ec1d47f
fix get multi bug
danmayer Oct 29, 2024
21d8c86
test and fix for infinite loop on timeout bug
danmayer Oct 29, 2024
47fd792
handle network errors
danmayer Oct 29, 2024
edfbd1e
fix rubocop and tests
danmayer Nov 3, 2024
f4f95f2
cleanup
danmayer Nov 3, 2024
df841fd
Merge branch 'main' into experimental_multi_set
danmayer Nov 3, 2024
b6935b9
rubocop
danmayer Nov 3, 2024
ffcbceb
fix tests
danmayer Nov 3, 2024
270cd0b
remove ruby 2.6
danmayer Nov 3, 2024
a09e408
more bundler fixes
danmayer Nov 3, 2024
b4e6849
ignore vendored files
danmayer Nov 3, 2024
67855cd
start toxiproxy
danmayer Nov 3, 2024
edd781c
remove older JRuby
danmayer Nov 4, 2024
d05db6c
skip network test on binary
danmayer Nov 4, 2024
082bcfe
Update lib/dalli/client.rb
danmayer Nov 4, 2024
42b7792
Merge pull request #6 from Shopify/experimental_multi_set
danmayer Nov 5, 2024
7b44603
Minor cleanups in Dalli
grcooper Nov 12, 2024
8a60218
Merge pull request #10 from Shopify/grcooper/minor-fixes-and-clarific…
grcooper Nov 12, 2024
5d169fa
Use buffers for get command
grcooper Nov 12, 2024
0d270ea
Use buffers more
grcooper Nov 12, 2024
f07cd0e
stuff
grcooper Nov 12, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6
ruby-version: 2.7
bundler-cache: true # 'bundle install' and cache
- name: Run RuboCop
run: bundle exec rubocop --parallel
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ jobs:
- '3.1'
- '3.0'
- '2.7'
- '2.6'
- jruby-9.3
- jruby-9.4
memcached-version: ['1.5.22', '1.6.23']

Expand All @@ -30,6 +28,8 @@ jobs:
run: |
chmod +x ./install_memcached.sh
./install_memcached.sh
- name: Install and start toxiproxy
run: ./bin/start-toxiproxy.sh
- name: Set up Ruby ${{ matrix.ruby-version }}
uses: ruby/setup-ruby@v1
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ profile.html
## Environment normalisation:
/.bundle/
/lib/bundler/man/
dev.yml

# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
Expand Down
5 changes: 4 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ require:

AllCops:
NewCops: enable
TargetRubyVersion: 2.6
TargetRubyVersion: 2.7
Exclude:
- 'bin/**/*'
- 'vendor/**/*'

Metrics/BlockLength:
Max: 50
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ group :development, :test do
gem 'rubocop-performance'
gem 'rubocop-rake'
gem 'simplecov'
gem 'stackprof', platform: :mri
gem 'toxiproxy'
end

group :test do
Expand Down
192 changes: 192 additions & 0 deletions bin/benchmark
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# This helps benchmark current performance of Dalli
# as well as compare performance of optimizated and non-optimized calls like multi-set vs set
#
# run with:
# bundle exec bin/benchmark
require 'bundler/inline'
require 'json'

gemfile do
source 'https://rubygems.org'
gem 'dalli'
gem 'benchmark-ips'
end

require 'dalli'
require 'benchmark/ips'

##
# StringSerializer is a serializer that avoids the overhead of Marshal or JSON.
##
class StringSerializer
def self.dump(value)
value
end

def self.load(value)
value
end
end

BENCH_TIME = (ENV['BENCH_TIME'] || 5).to_i
BENCH_JOB = ENV['BENCH_JOB'] || 'set_multi'
TERMINATOR = "\r\n"

client = Dalli::Client.new('localhost', compress: false)
meta_client = Dalli::Client.new('localhost', protocol: :meta, serializer: StringSerializer, compress: false)
string_client = Dalli::Client.new('localhost', serializer: StringSerializer, compress: false)
# The raw socket implementation is used to benchmark the performance of dalli & the overhead of the various abstractions
# in the library.
sock = TCPSocket.new('127.0.0.1', '11211', connect_timeout: 1)
sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
# Benchmarks didn't see any performance gains from increasing the SO_RCVBUF buffer size
# sock.setsockopt(Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, 1024 * 1024 * 8)
# Benchamrks did see an improvement in performance when increasing the SO_SNDBUF buffer size
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, 1024 * 1024 * 8)

TERMINATOR = "\r\n"
payload = 'B' * 1_000_000

# ensure the clients are all connected and working
client.set('key', payload)
string_client.set('string_key', payload)
meta_client.set('meta_key', payload)
sock.write("set sock_key 0 3600 1000000\r\n")
sock.write(payload)
sock.write(TERMINATOR)
sock.flush
sock.readline # clear the buffer

# ensure we have basic data for the benchmarks and get calls
payload_fifty = 'B' * 50_000
pairs = {}
100.times do |i|
pairs["multi_#{i}"] = payload_fifty
end
client.quiet do
pairs.each do |key, value|
client.set(key, value, 3600, raw: true)
end
end

###
# GC Suite
# benchmark without GC skewing things
###
class GCSuite
def warming(*)
run_gc
end

def running(*)
run_gc
end

def warmup_stats(*); end

def add_report(*); end

private

def run_gc
GC.enable
GC.start
GC.disable
end
end
suite = GCSuite.new

def sock_get_multi(sock, pairs)
count = pairs.length
pairs.each_key do |key|
count -= 1
tail = count.zero? ? '' : 'q'
sock.write("mg #{key} v f k #{tail}\r\n")
end
sock.flush
# read all the memcached responses back and build a hash of key value pairs
results = {}
last_result = false
while (line = sock.readline.chomp!(TERMINATOR)) != ''
last_result = true if line.start_with?('EN ')
next unless line.start_with?('VA ') || last_result

_, value_length, _flags, key = line.split
value = sock.read(value_length.to_i + TERMINATOR.length)
results[key[1..]] = value.chomp!(0..-TERMINATOR.length)
break if results.size == pairs.size
break if last_result
end
results
end

case BENCH_JOB
when 'set'
Benchmark.ips do |x|
x.config(warmup: 2, time: BENCH_TIME, suite: suite)
x.report('set 1MB MARSHAL') { client.set('key', payload) }
x.report('set 1MB STRING') { string_client.set('string_key', payload) }
x.report('meta_set 1MB STRING') { meta_client.set('meta_key', payload) }
x.report('set 1MB raw sock') do
sock.write("ms sock_key 1000000 c F0 T3600 MS \r\n")
sock.write(payload)
sock.write("\r\n")
sock.flush
sock.readline # clear the buffer
end
x.compare!
end
when 'get'
Benchmark.ips do |x|
x.config(warmup: 2, time: BENCH_TIME, suite: suite)
x.report('get 1MB MARSHAL') { client.get('key') }
x.report('get 1MB STRING') { string_client.get('string_key') }
x.report('meta_get 1MB STRING') { meta_client.get('meta_key') }
x.report('get 1MB raw sock') do
sock.write("get sock_key\r\n")
sock.readline
sock.read(1_000_000)
end
x.compare!
end
when 'get_multi'
Benchmark.ips do |x|
x.config(warmup: 2, time: BENCH_TIME, suite: suite)
x.report('binary get_mutli 100 keys') { client.get_multi(pairs.keys) }
x.report('meta get_mutli 100 keys') { meta_client.get_multi(pairs.keys) }
x.report('get 100 keys raw sock') { sock_get_multi(sock, pairs) }
x.compare!
end
when 'set_multi'
Benchmark.ips do |x|
x.config(warmup: 2, time: BENCH_TIME, suite: suite)
x.report('write 100 keys simple') do
client.quiet do
pairs.each do |key, value|
client.set(key, value, 3600, raw: true)
end
end
end
x.report('write 100 keys rawsock') do
count = pairs.length
tail = ''
value_bytesize = payload_fifty.bytesize
ttl = 3600

pairs.each do |key, value|
count -= 1
tail = count.zero? ? '' : 'q'
sock.write(String.new("ms #{key} #{value_bytesize} c F0 T#{ttl} MS #{tail}\r\n",
capacity: key.size + value_bytesize + 40) << value << TERMINATOR)
end
sock.flush
sock.gets(TERMINATOR) # clear the buffer
end
x.report('write_mutli 100 keys') { meta_client.set_multi(pairs, 3600, raw: true) }
x.compare!
end
end
15 changes: 15 additions & 0 deletions bin/get
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env ruby

require_relative '../lib/dalli'

puts "creating client"
client = Dalli::Client.new('localhost:11211', protocol: :meta, compress: false)
puts "SET_RESULT: #{client.set("foo", "b" * 1000, 0, raw: true)}"
puts "GET_RESULT: #{client.get("foo")[0..15]}"
puts "GET_RESULT_AGAIN: #{client.get("foo")[0..15]}"

while true
client.get("foo")
end

puts "DONE"
102 changes: 102 additions & 0 deletions bin/profile
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# This helps profile specific call paths in Dalli
# finding and fixing performance issues in these profiles should result in improvements in the dalli benchmarks
#
# run with:
# RUBY_YJIT_ENABLE=1 bundle exec bin/profile
require 'bundler/inline'
require 'json'

gemfile do
source 'https://rubygems.org'
gem 'dalli'
gem 'benchmark-ips'
gem 'stackprof'
end

require 'dalli'
require 'benchmark/ips'
require 'stackprof'

##
# StringSerializer is a serializer that avoids the overhead of Marshal or JSON.
##
class StringSerializer
def self.dump(value)
value
end

def self.load(value)
value
end
end

client = Dalli::Client.new('localhost', compress: false)
meta_client = Dalli::Client.new('localhost', protocol: :meta, serializer: StringSerializer, compress: false)
string_client = Dalli::Client.new('localhost', serializer: StringSerializer, compress: false)

payload = 'B' * 1_000_000
client.set('key', payload)
string_client.set('string_key', payload)
meta_client.set('meta_key', payload)

payload_fifty = 'B' * 50_000
keys = {}
100.times do |i|
keys["multi_#{i}"] = payload_fifty
end

# ensure theres is data for get calls
client.quiet do
keys.each do |key, value|
client.set(key, value, 3600, raw: true)
end
end

profile_job = ENV['PROFILE_JOB'] || 'set_multi'
method_name = ''

case profile_job
when 'get_multi'
profile = StackProf.run(mode: :wall, interval: 1_000) do
5_000.times do
meta_client.get_multi(keys.keys)
end
end
method_name = /Dalli::Client#get_multi/
when 'get_single'
profile = StackProf.run(mode: :wall, interval: 1_000) do
25_000.times do
keys.each_key do |key|
meta_client.get(key)
end
end
end
method_name = /Dalli::Client#get/
when 'set_single'
profile = StackProf.run(mode: :wall, interval: 1_000) do
5_000.times do
client.quiet do
keys.each do |key, value|
client.set(key, value, 3600, raw: true)
end
end
end
end
method_name = /Dalli::Client#set/
else
profile = StackProf.run(mode: :wall, interval: 1_000) do
5_000.times do
meta_client.set_multi(keys, 3600, raw: true)
end
end
method_name = /Dalli::Client#set_multi/
end

result = StackProf::Report.new(profile)
puts
result.print_text
puts "\n\n\n"
result.print_method(method_name)
16 changes: 16 additions & 0 deletions bin/start-toxiproxy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash -e

VERSION='v2.4.0'

if [[ "$OSTYPE" == "linux"* ]]; then
DOWNLOAD_TYPE="linux-amd64"
elif [[ "$OSTYPE" == "darwin"* ]]; then
DOWNLOAD_TYPE="darwin-amd64"
fi

echo "[dowload toxiproxy for $DOWNLOAD_TYPE]"
curl --silent -L https://github.com/Shopify/toxiproxy/releases/download/$VERSION/toxiproxy-server-$DOWNLOAD_TYPE -o ./bin/toxiproxy-server

echo "[start toxiproxy]"
chmod +x ./bin/toxiproxy-server
nohup bash -c "./bin/toxiproxy-server 2>&1 | sed -e 's/^/[toxiproxy] /' &"
2 changes: 1 addition & 1 deletion dalli.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Gem::Specification.new do |s|
'Gemfile'
]
s.homepage = 'https://github.com/petergoldstein/dalli'
s.required_ruby_version = '>= 2.6'
s.required_ruby_version = '>= 2.7'

s.metadata = {
'bug_tracker_uri' => 'https://github.com/petergoldstein/dalli/issues',
Expand Down
Loading