http-2: memory leak

Regarding https://github.com/ostinelli/net-http2/issues/7, I was able to reproduce the memory leak outside of net-http2 with a separate client, so the leak appears to be with http-2.

My class for sending messages is below, though even with the hack to delete @listeners it still leaks. (It’s been a while since I looked at this code, so I’m guessing that made it leak less.) In production sending a handful of messages every second over a persistent connection (Apple’s APNS servers), the process balloons to 512mb in about 7 hours (which triggers it to be restarted).

This is with Ruby 2.24p230 on OpenBSD. Any help troubleshooting this would be appreciated.

require "http/2"
require "openssl"
require "resolv"
require "ostruct"

# monkeypatch to free up memory
module HTTP2
  module Emitter
    def delete_listeners
      if @listeners
        @listeners.each do |k,v|
          @listeners.delete(k)
        end
      end

      @listeners = nil
    end
  end
end

class HTTP2Client
  attr_accessor :hostname, :ssl_context, :ssl_socket, :tcp_socket, :h2_client,
    :h2_stream, :headers, :body, :done

  DRAFT = "h2"

  def initialize(hostname, ip = nil, ctx = nil)
    if !ctx
      ctx = OpenSSL::SSL::SSLContext.new
    end

    ctx.npn_protocols = [ DRAFT ]
    ctx.npn_select_cb = lambda do |protocols|
      DRAFT if protocols.include?(DRAFT)
    end

    self.ssl_context = ctx

    if !ip
      ip = Resolv.getaddress(hostname)
    end

    begin
      Timeout.timeout(10) do
        self.tcp_socket = TCPSocket.new(ip, 443)

        sock = OpenSSL::SSL::SSLSocket.new(self.tcp_socket, self.ssl_context)
        sock.sync_close = true
        sock.hostname = hostname
        sock.connect

        self.ssl_socket = sock
      end
    rescue Timeout::Error
      self.log(:error, nil, "timed out connecting to #{host[:ip]}")
      self.store_client(nil, dev)
      return nil
    end

    self.hostname = hostname

    self.h2_client = HTTP2::Client.new
    self.h2_client.on(:frame) do |bytes|
      self.ssl_socket.print bytes
      self.ssl_socket.flush
      nil
    end
  end

  def call(method, path, options = {})
    if self.h2_stream
      self.cleanup_stream
    end

    headers = (options[:headers] || {})
    headers.merge!({
      ":scheme" => "https",
      ":method" => method.to_s.upcase,
      ":path" => path,
    })

    headers.merge!("host" => self.hostname)

    if options[:body]
      headers.merge!("content-length" => options[:body].bytesize.to_s)
    else
      headers.delete("content-length")
    end

    self.h2_stream = self.h2_client.new_stream

    self.headers = {}
    self.h2_stream.on(:headers) do |hs_array|
      hs = Hash[*hs_array.flatten]
      self.headers.merge!(hs)
      nil
    end

    self.body = ""
    self.h2_stream.on(:data) do |d|
      self.body << d
      nil
    end

    self.done = false
    self.h2_stream.on(:close) do |d|
      self.done = true
      nil
    end

    if options[:body]
      self.h2_stream.headers(headers, :end_stream => false)
      self.h2_stream.data(options[:body], :end_stream => true)
    else
      self.h2_stream.headers(headers, :end_stream => true)
    end

    while !self.ssl_socket.closed? && !self.ssl_socket.eof?
      data = self.ssl_socket.read_nonblock(1024)

      begin
        self.h2_client << data
      rescue => e
        self.ssl_socket.close
        raise e
      end

      if self.done
        break
      end
    end

    if self.done
      self.cleanup_stream
      return OpenStruct.new(:status => self.headers[":status"],
        :headers => self.headers, :body => self.body)
    else
      return nil
    end
  end

  def close
    begin
      if self.h2_client
        self.h2_client.close
      end
    rescue
    end

    begin
      if self.ssl_socket
        self.ssl_socket.close
      end
    rescue
    end

    begin
      if self.tcp_socket
        self.tcp_socket.close
      end
    rescue
    end
  end

  def cleanup_stream
    if self.h2_stream
      self.h2_stream.delete_listeners
    end

    begin
      self.h2_stream.close
    rescue
    end
  end
end

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 26 (8 by maintainers)

Commits related to this issue

Most upvoted comments

Memory usage is still reasonable after 48 hours.

USER       PID %CPU %MEM   VSZ   RSS TT  STAT  STARTED       TIME COMMAND
....     27033 21.2  3.0 267268 248136 p1- I     Mon10AM  897:08.06 ruby22

If @jcs confirms I think this can be released? 😃

/me summons @kazuho for some sage implementation advice… 😃

Context from https://github.com/ostinelli/net-http2/issues/7:

I’ve therefore looked at Http2’s connection.rb and I saw you are keeping a reference to the opened streams by adding them to the instance variable @streams here. However, I don’t seem to see anywhere these references being released once a stream is closed.

Therefore I’ve patched Http2’s connection.rb to release the reference once a stream gets closed, editing the code from (see here):

stream.once(:close) { @active_stream_count -= 1 } to:

stream.once(:close) do @streams.delete id @active_stream_count -= 1 end


You’re right, that’s an oversight on our part… we should be harvesting old streams. The actual spot/time on when to do so might require some experimentation though as h2 does allow us to receive frames on a closed stream in some cases. That said, we can start with something simple (as suggested above) and iterate from there.

@kazuho any recommendations for how to implement this, based on your experience with h2o?