You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@deltacloud.apache.org by mf...@redhat.com on 2011/01/07 14:24:30 UTC

Added new headers to API response

Hi,

This patch simply add ETag[1] support for all our string responses.
Suppose of this is to give client an option to check if response
was changed or not (like for checking instance status) without
parsing XML.

Another thing which this patch add is X-Runtime and X-Backend-Runtime
headers, where you can find timings or request and also timing
of 'backend' operation (mean how long takes to get response from backend)
This patch could be used for performance benchmarks...

  -- Michal

[1] http://en.wikipedia.org/wiki/HTTP_ETag


Re: [PATCH core] Added ETag HTTP header to give client opportunity to check if content was changed. Added X-Runtime and X-Backend-Runtime headers for benchmarking.

Posted by Michal Fojtik <mf...@redhat.com>.
On 07/01/11 14:24 +0100, mfojtik@redhat.com wrote:

Pushing this to master.

   -- Michal

>From: Michal Fojtik <mf...@redhat.com>
>
>---
> client/lib/deltacloud.rb                           |   17 +++++-
> .../lib/deltacloud/helpers/application_helper.rb   |   11 +++-
> server/lib/sinatra/rack_driver_select.rb           |   48 ++++++++--------
> server/lib/sinatra/rack_etag.rb                    |   60 ++++++++++++++++++++
> server/lib/sinatra/rack_runtime.rb                 |   28 +++++++++
> server/server.rb                                   |    6 ++-
> 6 files changed, 141 insertions(+), 29 deletions(-)
> create mode 100644 server/lib/sinatra/rack_etag.rb
> create mode 100644 server/lib/sinatra/rack_runtime.rb
>
>diff --git a/client/lib/deltacloud.rb b/client/lib/deltacloud.rb
>index 6985d5c..e8d6375 100644
>--- a/client/lib/deltacloud.rb
>+++ b/client/lib/deltacloud.rb
>@@ -34,9 +34,15 @@ module DeltaCloud
>   # @param [String, password] API password
>   # @param [String, user_name] API URL (eg. http://localhost:3001/api)
>   # @return [DeltaCloud::API]
>-  def self.new(user_name, password, api_url, opts={}, &block)
>-    opts ||= {}
>-    API.new(user_name, password, api_url, opts, &block)
>+  #def self.new(user_name, password, api_url, opts={}, &block)
>+  #  opts ||= {}
>+  #  API.new(user_name, password, api_url, opts, &block)
>+  #end
>+
>+  def self.new(opts={}, &block)
>+    opts ||={}
>+    client = API.new(opts[:username], opts[:password], opts[:url], opts, &block)
>+    client.use_driver(opts[:driver], opts) if opts[:driver]
>   end
>
>   # Check given credentials if their are valid against
>@@ -256,6 +262,11 @@ module DeltaCloud
>       return self
>     end
>
>+    def use_config!(opts={})
>+      @api_uri = URI.parse(opts[:url]) if opts[:url]
>+      use_driver(opts[:driver], opts)
>+    end
>+
>     def extended_headers
>       headers = {}
>       headers["X-Deltacloud-Driver"] = @api_driver.to_s if @api_driver
>diff --git a/server/lib/deltacloud/helpers/application_helper.rb b/server/lib/deltacloud/helpers/application_helper.rb
>index a734387..b018876 100644
>--- a/server/lib/deltacloud/helpers/application_helper.rb
>+++ b/server/lib/deltacloud/helpers/application_helper.rb
>@@ -17,6 +17,8 @@
>
> # Methods added to this helper will be available to all templates in the application.
>
>+require 'benchmark'
>+
> module ApplicationHelper
>
>   def bread_crumb
>@@ -74,7 +76,10 @@ module ApplicationHelper
>       filter.merge!(:state => params[:state]) if params[:state]
>       filter = nil if filter.keys.size.eql?(0)
>       singular = model.to_s.singularize.to_sym
>-      @elements = driver.send(model.to_sym, credentials, filter)
>+      @benchmark = Benchmark.measure do
>+        @elements = driver.send(model.to_sym, credentials, filter)
>+      end
>+      headers['X-Backend-Runtime'] = @benchmark.real.to_s
>       instance_variable_set(:"@#{model}", @elements)
>       respond_to do |format|
>         format.html { haml :"#{model}/index" }
>@@ -84,7 +89,9 @@ module ApplicationHelper
>   end
>
>   def show(model)
>-    @element = driver.send(model, credentials, { :id => params[:id]} )
>+    @benchmark = Benchmark.measure do
>+      @element = driver.send(model, credentials, { :id => params[:id]} )
>+    end
>     instance_variable_set("@#{model}", @element)
>     if @element
>       respond_to do |format|
>diff --git a/server/lib/sinatra/rack_driver_select.rb b/server/lib/sinatra/rack_driver_select.rb
>index 3e7d038..7b43d81 100644
>--- a/server/lib/sinatra/rack_driver_select.rb
>+++ b/server/lib/sinatra/rack_driver_select.rb
>@@ -1,29 +1,31 @@
>-class RackDriverSelect
>+module Rack
>+  class DriverSelect
>
>-  def initialize(app, opts={})
>-    @app = app
>-    @opts = opts
>-  end
>+    def initialize(app, opts={})
>+      @app = app
>+      @opts = opts
>+    end
>
>-  HEADER_TO_ENV_MAP = {
>-    'HTTP_X_DELTACLOUD_DRIVER' => :driver,
>-    'HTTP_X_DELTACLOUD_PROVIDER' => :provider
>-  }
>-
>-  def call(env)
>-    original_settings = { }
>-    HEADER_TO_ENV_MAP.each do |header, name|
>-      original_settings[name] = Thread.current[name]
>-      new_setting = extract_header(env, header)
>-      Thread.current[name] = new_setting if new_setting
>+    HEADER_TO_ENV_MAP = {
>+      'HTTP_X_DELTACLOUD_DRIVER' => :driver,
>+      'HTTP_X_DELTACLOUD_PROVIDER' => :provider
>+    }
>+
>+    def call(env)
>+      original_settings = { }
>+      HEADER_TO_ENV_MAP.each do |header, name|
>+        original_settings[name] = Thread.current[name]
>+        new_setting = extract_header(env, header)
>+        Thread.current[name] = new_setting if new_setting
>+      end
>+      @app.call(env)
>+    ensure
>+      original_settings.each { |name, value| Thread.current[name] = value }
>     end
>-    @app.call(env)
>-  ensure
>-    original_settings.each { |name, value| Thread.current[name] = value }
>-  end
>
>-  def extract_header(env, header)
>-    env[header].downcase if env[header]
>-  end
>+    def extract_header(env, header)
>+      env[header].downcase if env[header]
>+    end
>
>+  end
> end
>diff --git a/server/lib/sinatra/rack_etag.rb b/server/lib/sinatra/rack_etag.rb
>new file mode 100644
>index 0000000..fc5cf0a
>--- /dev/null
>+++ b/server/lib/sinatra/rack_etag.rb
>@@ -0,0 +1,60 @@
>+require 'digest/md5'
>+
>+module Rack
>+  # Automatically sets the ETag header on all String bodies.
>+  #
>+  # The ETag header is skipped if ETag or Last-Modified headers are sent or if
>+  # a sendfile body (body.responds_to :to_path) is given (since such cases
>+  # should be handled by apache/nginx).
>+  #
>+  # On initialization, you can pass two parameters: a Cache-Control directive
>+  # used when Etag is absent and a directive when it is present. The first
>+  # defaults to nil, while the second defaults to "max-age=0, privaute, must-revalidate"
>+  class ETag
>+    DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
>+
>+    def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
>+      @app = app
>+      @cache_control = cache_control
>+      @no_cache_control = no_cache_control
>+    end
>+
>+    def call(env)
>+      status, headers, body = @app.call(env)
>+
>+      if etag_status?(status) && etag_body?(body) && !http_caching?(headers)
>+        digest, body = digest_body(body)
>+        headers['ETag'] = %("#{digest}") if digest
>+      end
>+
>+      unless headers['Cache-Control']
>+        headers['Cache-Control'] = digest ? @cache_control : @no_cache_control
>+      end
>+
>+      [status, headers, body]
>+    end
>+
>+    private
>+
>+      def etag_status?(status)
>+        status == 200 || status == 201
>+      end
>+
>+      def etag_body?(body)
>+        !body.respond_to?(:to_path)
>+      end
>+
>+      def http_caching?(headers)
>+        headers.key?('ETag') || headers.key?('Last-Modified')
>+      end
>+
>+      def digest_body(body)
>+        parts = []
>+        body.each { |part| parts << part }
>+        string_body = parts.join
>+        digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
>+        [digest, parts]
>+      end
>+  end
>+end
>+
>diff --git a/server/lib/sinatra/rack_runtime.rb b/server/lib/sinatra/rack_runtime.rb
>new file mode 100644
>index 0000000..5da3940
>--- /dev/null
>+++ b/server/lib/sinatra/rack_runtime.rb
>@@ -0,0 +1,28 @@
>+module Rack
>+  # Sets an "X-Runtime" response header, indicating the response
>+  # time of the request, in seconds
>+  #
>+  # You can put it right before the application to see the processing
>+  # time, or before all the other middlewares to include time for them,
>+  # too.
>+  class Runtime
>+    def initialize(app, name = nil)
>+      @app = app
>+      @header_name = "X-Runtime"
>+      @header_name << "-#{name}" if name
>+    end
>+
>+    def call(env)
>+      start_time = Time.now
>+      status, headers, body = @app.call(env)
>+      request_time = Time.now - start_time
>+
>+      if !headers.has_key?(@header_name)
>+        headers[@header_name] = "%0.6f" % request_time
>+      end
>+
>+      [status, headers, body]
>+    end
>+  end
>+end
>+
>diff --git a/server/server.rb b/server/server.rb
>index 8c3b72c..097ff0f 100644
>--- a/server/server.rb
>+++ b/server/server.rb
>@@ -28,11 +28,15 @@ require 'haml'
> require 'open3'
> require 'lib/deltacloud/helpers/blob_stream'
> require 'sinatra/rack_driver_select'
>+require 'sinatra/rack_runtime'
>+require 'sinatra/rack_etag'
>
> set :version, '0.1.0'
>
>
>-use RackDriverSelect
>+use Rack::DriverSelect
>+use Rack::ETag
>+use Rack::Runtime
>
> configure do
>   set :raise_errors => false
>--
>1.7.3.4
>

-- 
--------------------------------------------------------
Michal Fojtik, mfojtik@redhat.com
Deltacloud API: http://deltacloud.org
--------------------------------------------------------

[PATCH core] Added ETag HTTP header to give client opportunity to check if content was changed. Added X-Runtime and X-Backend-Runtime headers for benchmarking.

Posted by mf...@redhat.com.
From: Michal Fojtik <mf...@redhat.com>

---
 client/lib/deltacloud.rb                           |   17 +++++-
 .../lib/deltacloud/helpers/application_helper.rb   |   11 +++-
 server/lib/sinatra/rack_driver_select.rb           |   48 ++++++++--------
 server/lib/sinatra/rack_etag.rb                    |   60 ++++++++++++++++++++
 server/lib/sinatra/rack_runtime.rb                 |   28 +++++++++
 server/server.rb                                   |    6 ++-
 6 files changed, 141 insertions(+), 29 deletions(-)
 create mode 100644 server/lib/sinatra/rack_etag.rb
 create mode 100644 server/lib/sinatra/rack_runtime.rb

diff --git a/client/lib/deltacloud.rb b/client/lib/deltacloud.rb
index 6985d5c..e8d6375 100644
--- a/client/lib/deltacloud.rb
+++ b/client/lib/deltacloud.rb
@@ -34,9 +34,15 @@ module DeltaCloud
   # @param [String, password] API password
   # @param [String, user_name] API URL (eg. http://localhost:3001/api)
   # @return [DeltaCloud::API]
-  def self.new(user_name, password, api_url, opts={}, &block)
-    opts ||= {}
-    API.new(user_name, password, api_url, opts, &block)
+  #def self.new(user_name, password, api_url, opts={}, &block)
+  #  opts ||= {}
+  #  API.new(user_name, password, api_url, opts, &block)
+  #end
+
+  def self.new(opts={}, &block)
+    opts ||={}
+    client = API.new(opts[:username], opts[:password], opts[:url], opts, &block)
+    client.use_driver(opts[:driver], opts) if opts[:driver]
   end
 
   # Check given credentials if their are valid against
@@ -256,6 +262,11 @@ module DeltaCloud
       return self
     end
 
+    def use_config!(opts={})
+      @api_uri = URI.parse(opts[:url]) if opts[:url]
+      use_driver(opts[:driver], opts)
+    end
+
     def extended_headers
       headers = {}
       headers["X-Deltacloud-Driver"] = @api_driver.to_s if @api_driver
diff --git a/server/lib/deltacloud/helpers/application_helper.rb b/server/lib/deltacloud/helpers/application_helper.rb
index a734387..b018876 100644
--- a/server/lib/deltacloud/helpers/application_helper.rb
+++ b/server/lib/deltacloud/helpers/application_helper.rb
@@ -17,6 +17,8 @@
 
 # Methods added to this helper will be available to all templates in the application.
 
+require 'benchmark'
+
 module ApplicationHelper
 
   def bread_crumb
@@ -74,7 +76,10 @@ module ApplicationHelper
       filter.merge!(:state => params[:state]) if params[:state]
       filter = nil if filter.keys.size.eql?(0)
       singular = model.to_s.singularize.to_sym
-      @elements = driver.send(model.to_sym, credentials, filter)
+      @benchmark = Benchmark.measure do
+        @elements = driver.send(model.to_sym, credentials, filter)
+      end
+      headers['X-Backend-Runtime'] = @benchmark.real.to_s
       instance_variable_set(:"@#{model}", @elements)
       respond_to do |format|
         format.html { haml :"#{model}/index" }
@@ -84,7 +89,9 @@ module ApplicationHelper
   end
 
   def show(model)
-    @element = driver.send(model, credentials, { :id => params[:id]} )
+    @benchmark = Benchmark.measure do
+      @element = driver.send(model, credentials, { :id => params[:id]} )
+    end
     instance_variable_set("@#{model}", @element)
     if @element
       respond_to do |format|
diff --git a/server/lib/sinatra/rack_driver_select.rb b/server/lib/sinatra/rack_driver_select.rb
index 3e7d038..7b43d81 100644
--- a/server/lib/sinatra/rack_driver_select.rb
+++ b/server/lib/sinatra/rack_driver_select.rb
@@ -1,29 +1,31 @@
-class RackDriverSelect
+module Rack
+  class DriverSelect
 
-  def initialize(app, opts={})
-    @app = app
-    @opts = opts
-  end
+    def initialize(app, opts={})
+      @app = app
+      @opts = opts
+    end
 
-  HEADER_TO_ENV_MAP = {
-    'HTTP_X_DELTACLOUD_DRIVER' => :driver,
-    'HTTP_X_DELTACLOUD_PROVIDER' => :provider
-  }
-  
-  def call(env)
-    original_settings = { }
-    HEADER_TO_ENV_MAP.each do |header, name|
-      original_settings[name] = Thread.current[name]
-      new_setting = extract_header(env, header)
-      Thread.current[name] = new_setting if new_setting
+    HEADER_TO_ENV_MAP = {
+      'HTTP_X_DELTACLOUD_DRIVER' => :driver,
+      'HTTP_X_DELTACLOUD_PROVIDER' => :provider
+    }
+    
+    def call(env)
+      original_settings = { }
+      HEADER_TO_ENV_MAP.each do |header, name|
+        original_settings[name] = Thread.current[name]
+        new_setting = extract_header(env, header)
+        Thread.current[name] = new_setting if new_setting
+      end
+      @app.call(env)
+    ensure
+      original_settings.each { |name, value| Thread.current[name] = value }
     end
-    @app.call(env)
-  ensure
-    original_settings.each { |name, value| Thread.current[name] = value }
-  end
 
-  def extract_header(env, header)
-    env[header].downcase if env[header]
-  end
+    def extract_header(env, header)
+      env[header].downcase if env[header]
+    end
 
+  end
 end
diff --git a/server/lib/sinatra/rack_etag.rb b/server/lib/sinatra/rack_etag.rb
new file mode 100644
index 0000000..fc5cf0a
--- /dev/null
+++ b/server/lib/sinatra/rack_etag.rb
@@ -0,0 +1,60 @@
+require 'digest/md5'
+
+module Rack
+  # Automatically sets the ETag header on all String bodies.
+  #
+  # The ETag header is skipped if ETag or Last-Modified headers are sent or if
+  # a sendfile body (body.responds_to :to_path) is given (since such cases
+  # should be handled by apache/nginx).
+  #
+  # On initialization, you can pass two parameters: a Cache-Control directive
+  # used when Etag is absent and a directive when it is present. The first
+  # defaults to nil, while the second defaults to "max-age=0, privaute, must-revalidate"
+  class ETag
+    DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
+
+    def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
+      @app = app
+      @cache_control = cache_control
+      @no_cache_control = no_cache_control
+    end
+
+    def call(env)
+      status, headers, body = @app.call(env)
+
+      if etag_status?(status) && etag_body?(body) && !http_caching?(headers)
+        digest, body = digest_body(body)
+        headers['ETag'] = %("#{digest}") if digest
+      end
+
+      unless headers['Cache-Control']
+        headers['Cache-Control'] = digest ? @cache_control : @no_cache_control
+      end
+
+      [status, headers, body]
+    end
+
+    private
+
+      def etag_status?(status)
+        status == 200 || status == 201
+      end
+
+      def etag_body?(body)
+        !body.respond_to?(:to_path)
+      end
+
+      def http_caching?(headers)
+        headers.key?('ETag') || headers.key?('Last-Modified')
+      end
+
+      def digest_body(body)
+        parts = []
+        body.each { |part| parts << part }
+        string_body = parts.join
+        digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
+        [digest, parts]
+      end
+  end
+end
+
diff --git a/server/lib/sinatra/rack_runtime.rb b/server/lib/sinatra/rack_runtime.rb
new file mode 100644
index 0000000..5da3940
--- /dev/null
+++ b/server/lib/sinatra/rack_runtime.rb
@@ -0,0 +1,28 @@
+module Rack
+  # Sets an "X-Runtime" response header, indicating the response
+  # time of the request, in seconds
+  #
+  # You can put it right before the application to see the processing
+  # time, or before all the other middlewares to include time for them,
+  # too.
+  class Runtime
+    def initialize(app, name = nil)
+      @app = app
+      @header_name = "X-Runtime"
+      @header_name << "-#{name}" if name
+    end
+
+    def call(env)
+      start_time = Time.now
+      status, headers, body = @app.call(env)
+      request_time = Time.now - start_time
+
+      if !headers.has_key?(@header_name)
+        headers[@header_name] = "%0.6f" % request_time
+      end
+      
+      [status, headers, body]
+    end
+  end
+end
+
diff --git a/server/server.rb b/server/server.rb
index 8c3b72c..097ff0f 100644
--- a/server/server.rb
+++ b/server/server.rb
@@ -28,11 +28,15 @@ require 'haml'
 require 'open3'
 require 'lib/deltacloud/helpers/blob_stream'
 require 'sinatra/rack_driver_select'
+require 'sinatra/rack_runtime'
+require 'sinatra/rack_etag'
 
 set :version, '0.1.0'
 
 
-use RackDriverSelect
+use Rack::DriverSelect
+use Rack::ETag
+use Rack::Runtime
 
 configure do
   set :raise_errors => false
-- 
1.7.3.4