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:31 UTC

[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.

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


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
--------------------------------------------------------