Permalink
Browse files

Add thread_local_timezones extension, and refactor timezone support i…

…n modules

The thread_local_timezones extension allows you to set a per-thread
timezone that will override the default global timezone while the
thread is executing.  The main use case is for web applications that
execute each request in its own thread, and want to set the timezones
based on the request.  The most common example is having the database
always store time in UTC, but have the application deal with the
timezone of the current user.  That can be done with:

  Sequel.database_timezone = :utc
  # In each thread:
  Sequel.thread_application_timezone = current_user.timezone

This extension is designed to work with the named_timezones
extension.

This extension adds the thread_application_timezone=,
thread_database_timezone=, and thread_typecast_timezone= methods to
the Sequel module.  It overrides the application_timezone,
database_timezone, and typecast_timezone methods to check the related
thread local timezone first, and use it if present. If the related
thread local timezone is not present, it falls back to the default
global timezone.

There is one special case of note.  If you have a default global
timezone and you want to have a nil thread local timezone, you have
to set the thread local value to :nil instead of nil:

  Sequel.application_timezone = :utc
  Sequel.thread_application_timezone = nil
  Sequel.application_timezone # => :utc
  Sequel.thread_application_timezone = :nil
  Sequel.application_timezone # => nil

In order to implement this more easily, and just for generally better
design, the timezone support in the main Sequel module was separated
into the Sequel::Timezones module, which extends the main Sequel
module.  The named_timezones extension now uses a NamedTimezones
module which also extends Sequel.  And the new thread_local_timezones
extension adds a ThreadLocalTimezones which also extends Sequel.

A new private convert_timezone_setter_arg method was added to
ease implementation and allow the thread_local_timezones and
named_timezones extensions to work better together.
  • Loading branch information...
1 parent 97c11f0 commit f899e55b585e64bc9c739c70b14bf380922c81b5 @jeremyevans jeremyevans committed Sep 16, 2009
View
@@ -59,28 +59,12 @@
# You can set the SEQUEL_NO_CORE_EXTENSIONS constant or environment variable to have
# Sequel not extend the core classes.
module Sequel
- # The offset of the current time zone from UTC, in seconds.
- LOCAL_DATETIME_OFFSET_SECS = Time.now.utc_offset
-
- # The offset of the current time zone from UTC, as a fraction of a day.
- LOCAL_DATETIME_OFFSET = respond_to?(:Rational, true) ? Rational(LOCAL_DATETIME_OFFSET_SECS, 60*60*24) : LOCAL_DATETIME_OFFSET_SECS/60/60/24.0
-
- @application_timezone = nil
@convert_two_digit_years = true
- @database_timezone = nil
@datetime_class = Time
- @typecast_timezone = nil
@virtual_row_instance_eval = true
class << self
attr_accessor :convert_two_digit_years, :datetime_class, :virtual_row_instance_eval
- attr_accessor :application_timezone, :database_timezone, :typecast_timezone
- end
-
- # Convert the given Time/DateTime object into the database timezone, used when
- # literalizing objects in an SQL string.
- def self.application_to_database_timestamp(v)
- convert_output_timestamp(v, Sequel.database_timezone)
end
# Returns true if the passed object could be a specifier of conditions, false otherwise.
@@ -127,20 +111,6 @@ def self.convert_exception_class(exception, klass)
e
end
- # Convert the given object into an object of Sequel.datetime_class in the
- # application_timezone. Used when coverting datetime/timestamp columns
- # returned by the database.
- def self.database_to_application_timestamp(v)
- convert_timestamp(v, Sequel.database_timezone)
- end
-
- # Sets the database, application, and typecasting timezones to the given timezone.
- def self.default_timezone=(tz)
- self.database_timezone = tz
- self.application_timezone = tz
- self.typecast_timezone = tz
- end
-
# Load all Sequel extensions given. Only loads extensions included in this
# release of Sequel, doesn't load external extensions.
#
@@ -236,13 +206,6 @@ def self.string_to_time(s)
raise convert_exception_class(e, InvalidValue)
end
end
-
- # Convert the given object into an object of Sequel.datetime_class in the
- # application_timezone. Used when typecasting values when assigning them
- # to model datetime attributes.
- def self.typecast_to_application_timestamp(v)
- convert_timestamp(v, Sequel.typecast_timezone)
- end
# If the supplied block takes a single argument,
# yield a new SQL::VirtualRow instance to the block
@@ -273,111 +236,6 @@ def self.adapter_method(adapter, *args, &block) # :nodoc:
end
connect(opts, &block)
end
-
- # Convert the given DateTime to the given input_timezone, keeping the
- # same time and just modifying the timezone.
- def self.convert_input_datetime_no_offset(v, input_timezone) # :nodoc:
- case input_timezone
- when :utc
- v# DateTime assumes UTC if no offset is given
- when :local
- v.new_offset(LOCAL_DATETIME_OFFSET) - LOCAL_DATETIME_OFFSET
- else
- convert_input_datetime_other(v, input_timezone)
- end
- end
-
- # Convert the given DateTime to the given input_timezone that is not supported
- # by default (such as nil, :local, or :utc). Raises an error by default.
- # Can be overridden in extensions.
- def self.convert_input_datetime_other(v, input_timezone) # :nodoc:
- raise InvalidValue, "Invalid input_timezone: #{input_timezone.inspect}"
- end
-
- # Converts the object from a String, Array, Date, DateTime, or Time into an
- # instance of Sequel.datetime_class. If given an array or a string that doesn't
- # contain an offset, assume that the array/string is already in the given input_timezone.
- def self.convert_input_timestamp(v, input_timezone) # :nodoc:
- case v
- when String
- v2 = Sequel.string_to_datetime(v)
- if !input_timezone || Date._parse(v).has_key?(:offset)
- v2
- else
- # Correct for potentially wrong offset if string doesn't include offset
- if v2.is_a?(DateTime)
- v2 = convert_input_datetime_no_offset(v2, input_timezone)
- else
- # Time assumes local time if no offset is given
- v2 = v2.getutc + LOCAL_DATETIME_OFFSET_SECS if input_timezone == :utc
- end
- v2
- end
- when Array
- y, mo, d, h, mi, s = v
- if datetime_class == DateTime
- convert_input_datetime_no_offset(DateTime.civil(y, mo, d, h, mi, s, 0), input_timezone)
- else
- Time.send(input_timezone == :utc ? :utc : :local, y, mo, d, h, mi, s)
- end
- when Time
- if datetime_class == DateTime
- v.respond_to?(:to_datetime) ? v.to_datetime : string_to_datetime(v.iso8601)
- else
- v
- end
- when DateTime
- if datetime_class == DateTime
- v
- else
- v.respond_to?(:to_time) ? v.to_time : string_to_datetime(v.to_s)
- end
- when Date
- convert_input_timestamp(v.to_s, input_timezone)
- else
- raise InvalidValue, "Invalid convert_input_timestamp type: #{v.inspect}"
- end
- end
-
- # Convert the given DateTime to the given output_timezone that is not supported
- # by default (such as nil, :local, or :utc). Raises an error by default.
- # Can be overridden in extensions.
- def self.convert_output_datetime_other(v, output_timezone) # :nodoc:
- raise InvalidValue, "Invalid output_timezone: #{output_timezone.inspect}"
- end
-
- # Converts the object to the given output_timezone.
- def self.convert_output_timestamp(v, output_timezone) # :nodoc:
- if output_timezone
- if v.is_a?(DateTime)
- case output_timezone
- when :utc
- v.new_offset(0)
- when :local
- v.new_offset(LOCAL_DATETIME_OFFSET)
- else
- convert_output_datetime_other(v, output_timezone)
- end
- else
- v.send(output_timezone == :utc ? :getutc : :getlocal)
- end
- else
- v
- end
- end
-
- # Converts the given object from the given input timezone to the
- # application timezone using convert_input_timestamp and
- # convert_output_timestamp.
- def self.convert_timestamp(v, input_timezone) # :nodoc:
- begin
- convert_output_timestamp(convert_input_timestamp(v, input_timezone), Sequel.application_timezone)
- rescue InvalidValue
- raise
- rescue => e
- raise convert_exception_class(e, InvalidValue)
- end
- end
# Method that adds a database adapter class method to Sequel that calls
# Sequel.adapter_method.
@@ -387,12 +245,9 @@ def self.def_adapter_method(*adapters) # :nodoc:
end
end
- private_class_method :adapter_method, :convert_input_datetime_no_offset,
- :convert_input_datetime_other, :convert_input_timestamp,
- :convert_output_datetime_other, :convert_output_timestamp,
- :convert_timestamp, :def_adapter_method
+ private_class_method :adapter_method, :def_adapter_method
- require(%w"metaprogramming sql connection_pool exceptions dataset database version")
+ require(%w"metaprogramming sql connection_pool exceptions dataset database timezones version")
require(%w"schema_generator schema_methods schema_sql", 'database')
require(%w"convenience graph prepared_statements sql", 'dataset')
require('core_sql') if !defined?(::SEQUEL_NO_CORE_EXTENSIONS) && !ENV.has_key?('SEQUEL_NO_CORE_EXTENSIONS')
@@ -27,29 +27,35 @@
module Sequel
self.datetime_class = DateTime
- %w'application database typecast'.each do |t|
- instance_eval("def #{t}_timezone=(tz); @#{t}_timezone = tz.is_a?(String) ? TZInfo::Timezone.get(tz) : tz; end", __FILE__, __LINE__)
- end
-
- # Assume the given DateTime has a correct time but a wrong timezone. It is
- # currently in UTC timezone, but it should be converted to the input_timzone.
- # Keep the time the same but convert the timezone to the input_timezone.
- # Expects the input_timezone to be a TZInfo::Timezone instance.
- def self.convert_input_datetime_other(v, input_timezone) # :nodoc:
- local_offset = input_timezone.period_for_local(v).utc_total_offset_rational
- (v - local_offset).new_offset(local_offset)
- end
+ module NamedTimezones
+ private
+
+ # Assume the given DateTime has a correct time but a wrong timezone. It is
+ # currently in UTC timezone, but it should be converted to the input_timzone.
+ # Keep the time the same but convert the timezone to the input_timezone.
+ # Expects the input_timezone to be a TZInfo::Timezone instance.
+ def convert_input_datetime_other(v, input_timezone)
+ local_offset = input_timezone.period_for_local(v).utc_total_offset_rational
+ (v - local_offset).new_offset(local_offset)
+ end
- # Convert the given DateTime to use the given output_timezone.
- # Expects the output_timezone to be a TZInfo::Timezone instance.
- def self.convert_output_datetime_other(v, output_timezone) # :nodoc:
- # TZInfo converts times, but expects the given DateTime to have an offset
- # of 0 and always leaves the timezone offset as 0
- v = output_timezone.utc_to_local(v.new_offset(0))
- local_offset = output_timezone.period_for_local(v).utc_total_offset_rational
- # Convert timezone offset from UTC to the offset for the output_timezone
- (v - local_offset).new_offset(local_offset)
+ # Convert the given DateTime to use the given output_timezone.
+ # Expects the output_timezone to be a TZInfo::Timezone instance.
+ def convert_output_datetime_other(v, output_timezone)
+ # TZInfo converts times, but expects the given DateTime to have an offset
+ # of 0 and always leaves the timezone offset as 0
+ v = output_timezone.utc_to_local(v.new_offset(0))
+ local_offset = output_timezone.period_for_local(v).utc_total_offset_rational
+ # Convert timezone offset from UTC to the offset for the output_timezone
+ (v - local_offset).new_offset(local_offset)
+ end
+
+ # Convert the timezone setter argument. Returns argument given by default,
+ # exists for easier overriding in extensions.
+ def convert_timezone_setter_arg(tz)
+ tz.is_a?(String) ? TZInfo::Timezone.get(tz) : super
+ end
end
- private_class_method :convert_input_datetime_other, :convert_output_datetime_other
+ extend NamedTimezones
end
@@ -0,0 +1,48 @@
+# The thread_local_timezones extension allows you to set a per-thread timezone that
+# will override the default global timezone while the thread is executing. The
+# main use case is for web applications that execute each request in its own thread,
+# and want to set the timezones based on the request. The most common example
+# is having the database always store time in UTC, but have the application deal
+# with the timezone of the current user. That can be done with:
+#
+# Sequel.database_timezone = :utc
+# # In each thread:
+# Sequel.thread_application_timezone = current_user.timezone
+#
+# This extension is designed to work with the named_timezones extension.
+#
+# This extension adds the thread_application_timezone=, thread_database_timezone=,
+# and thread_typecast_timezone= methods to the Sequel module. It overrides
+# the application_timezone, database_timezone, and typecast_timezone
+# methods to check the related thread local timezone first, and use it if present.
+# If the related thread local timezone is not present, it falls back to the
+# default global timezone.
+#
+# There is one special case of note. If you have a default global timezone
+# and you want to have a nil thread local timezone, you have to set the thread
+# local value to :nil instead of nil:
+#
+# Sequel.application_timezone = :utc
+# Sequel.thread_application_timezone = nil
+# Sequel.application_timezone # => :utc
+# Sequel.thread_application_timezone = :nil
+# Sequel.application_timezone # => nil
+
+module Sequel
+ module ThreadLocalTimezones
+ %w'application database typecast'.each do |t|
+ class_eval("def thread_#{t}_timezone=(tz); Thread.current[:#{t}_timezone] = convert_timezone_setter_arg(tz); end", __FILE__, __LINE__)
+ class_eval(<<END, __FILE__, __LINE__)
+ def #{t}_timezone
+ if tz = Thread.current[:#{t}_timezone]
+ tz unless tz == :nil
+ else
+ super
+ end
+ end
+END
+ end
+ end
+
+ extend ThreadLocalTimezones
+end
Oops, something went wrong.

0 comments on commit f899e55

Please sign in to comment.