Migration to add close-time to changesets. This replaces the boolean 'open' attribute. Added checks to ensure that the maximum lifetime and number of changes in a changeset are enforced. Added some tests.

This commit is contained in:
Matt Amos 2008-11-17 19:00:01 +00:00
parent 9a4ea6bfd0
commit a90be5e69a
8 changed files with 166 additions and 20 deletions

View file

@ -53,7 +53,11 @@ class ChangesetController < ApplicationController
raise OSM::APIUserChangesetMismatchError
end
changeset.open = false
# to close the changeset, we'll just set its closed_at time to
# now. this might not be enough if there are concurrency issues,
# but we'll have to wait and see.
changeset.closed_at = DateTime.now
changeset.save!
render :nothing => true
rescue ActiveRecord::RecordNotFound
@ -361,10 +365,10 @@ class ChangesetController < ApplicationController
raise OSM::APIBadUserInput.new("bad time range") if times.size != 2
from, to = times.collect { |t| DateTime.parse(t) }
return ['created_at > ? and created_at < ?', from, to]
return ['closed_at >= ? and created_at <= ?', from, to]
else
# if there is no comma, assume its a lower limit on time
return ['created_at > ?', DateTime.parse(time)]
return ['closed_at >= ?', DateTime.parse(time)]
end
else
return nil
@ -380,7 +384,7 @@ class ChangesetController < ApplicationController
##
# restrict changes to those which are open
def conditions_open(open)
return open.nil? ? nil : ['open = ?', true]
return open.nil? ? nil : ['closed_at >= ?', DateTime.now]
end
end

View file

@ -12,17 +12,31 @@ class Changeset < ActiveRecord::Base
has_many :old_ways
has_many :old_relations
validates_presence_of :user_id, :created_at
validates_inclusion_of :open, :in => [ true, false ]
validates_presence_of :user_id, :created_at, :closed_at
# over-expansion factor to use when updating the bounding box
EXPAND = 0.1
# maximum number of elements allowed in a changeset
MAX_ELEMENTS = 50000
# maximum time a changeset is allowed to be open for (note that this
# is in days - so one hour is Rational(1,24)).
MAX_TIME_OPEN = 1
# idle timeout increment, one hour as a rational number of days.
IDLE_TIMEOUT = Rational(1,24)
# Use a method like this, so that we can easily change how we
# determine whether a changeset is open, without breaking code in at
# least 6 controllers
def is_open?
return open
# a changeset is open (that is, it will accept further changes) when
# it has not yet run out of time and its capacity is small enough.
# note that this may not be a hard limit - due to timing changes and
# concurrency it is possible that some changesets may be slightly
# longer than strictly allowed or have slightly more changes in them.
return ((closed_at > DateTime.now) and (num_changes <= MAX_ELEMENTS))
end
def self.from_xml(xml, create=false)
@ -36,6 +50,11 @@ class Changeset < ActiveRecord::Base
doc.find('//osm/changeset').each do |pt|
if create
cs.created_at = Time.now
# initial close time is 1h ahead, but will be increased on each
# modification.
cs.closed_at = Time.now + IDLE_TIMEOUT
# initially we have no changes in a changeset
cs.num_changes = 0
end
pt.find('tag').each do |tag|
@ -78,6 +97,14 @@ class Changeset < ActiveRecord::Base
self.min_lon, self.min_lat, self.max_lon, self.max_lat = @bbox
end
##
# the number of elements is also passed in so that we can ensure that
# a single changeset doesn't contain too many elements. this, of course,
# destroys the optimisation described in the bbox method above.
def add_changes!(elements)
self.num_changes += elements
end
def tags_as_hash
return tags
end
@ -107,8 +134,14 @@ class Changeset < ActiveRecord::Base
# do the changeset update and the changeset tags update in the
# same transaction to ensure consistency.
Changeset.transaction do
# fixme update modified_at time?
# FIXME there is no modified_at time, should it be added
# set the auto-close time to be one hour in the future unless
# that would make it more than 24h long, in which case clip to
# 24h, as this has been decided is a reasonable time limit.
if (closed_at - created_at) > (MAX_TIME_OPEN - IDLE_TIMEOUT)
self.closed_at = created_at + MAX_TIME_OPEN
else
self.closed_at = DateTime.now + IDLE_TIMEOUT
end
self.save!
tags = self.tags
@ -155,7 +188,8 @@ class Changeset < ActiveRecord::Base
end
el1['created_at'] = self.created_at.xmlschema
el1['open'] = self.open.to_s
el1['closed_at'] = self.closed_at.xmlschema unless is_open?
el1['open'] = is_open?.to_s
el1['min_lon'] = (bbox[0].to_f / GeoRecord::SCALE).to_s unless bbox[0].nil?
el1['min_lat'] = (bbox[1].to_f / GeoRecord::SCALE).to_s unless bbox[1].nil?
@ -180,7 +214,7 @@ class Changeset < ActiveRecord::Base
end
# can't change a closed changeset
unless open
unless is_open?
raise OSM::APIChangesetAlreadyClosedError
end

View file

@ -141,6 +141,9 @@ class Node < ActiveRecord::Base
old_node.timestamp = t
old_node.save_with_dependencies!
# tell the changeset we updated one element only
changeset.add_changes! 1
# save the changeset in case of bounding box updates
changeset.save!
end

View file

@ -307,6 +307,9 @@ class Relation < ActiveRecord::Base
end
end
# tell the changeset we updated one element only
changeset.add_changes! 1
# save the (maybe updated) changeset bounding box
changeset.save!
end

View file

@ -233,6 +233,10 @@ class Way < ActiveRecord::Base
# update and commit the bounding box, now that way nodes
# have been updated and we're in a transaction.
changeset.update_bbox!(bbox) unless nodes.empty?
# tell the changeset we updated one element only
changeset.add_changes! 1
changeset.save!
end
end

View file

@ -0,0 +1,34 @@
class AddEndTimeToChangesets < ActiveRecord::Migration
def self.up
# swap the boolean closed-or-not for a time when the changeset will
# close or has closed.
add_column(:changesets, :closed_at, :datetime, :null => false)
# it appears that execute will only accept string arguments, so
# this is an ugly, ugly hack to get some sort of mysql/postgres
# independence. now i have to go wash my brain with bleach.
execute("update changesets set closed_at=(now()-'1 hour') where open=(1=0)")
execute("update changesets set closed_at=(now()+'1 hour') where open=(1=1)")
# remove the open column as it is unnecessary now and denormalises
# the table.
remove_column :changesets, :open
# add a column to keep track of the number of changes in a changeset.
# could probably work out how many changes there are here, but i'm not
# sure its actually important.
add_column(:changesets, :num_changes, :integer,
:null => false, :default => 0)
end
def self.down
# in the reverse direction, we can look at the closed_at to figure out
# if changesets are closed or not.
add_column(:changesets, :open, :boolean, :null => false, :default => true)
execute("update changesets set open=(closed_at > now())")
remove_column :changesets, :closed_at
# remove the column for tracking number of changes
remove_column :changesets, :num_changes
end
end

View file

@ -7,33 +7,37 @@ normal_user_first_change:
id: 1
user_id: 1
created_at: "2007-01-01 00:00:00"
open: true
closed_at: <%= DateTime.now + Rational(1,24) %>
min_lon: <%= 1 * SCALE %>
min_lat: <%= 1 * SCALE %>
max_lon: <%= 5 * SCALE %>
max_lat: <%= 5 * SCALE %>
num_changes: 11
second_user_first_change:
id: 2
user_id: 2
created_at: "2008-05-01 01:23:45"
open: true
closed_at: <%= DateTime.now + Rational(1,24) %>
num_changes: 0
normal_user_closed_change:
id: 3
user_id: 1
created_at: "2007-01-01 00:00:00"
open: false
closed_at: "2007-01-02 00:00:00"
num_changes: 0
normal_user_version_change:
id: 4
user_id: 1
created_at: "2008-01-01 00:00:00"
open: true
closed_at: <%= DateTime.now + Rational(1,24) %>
min_lon: <%= 1 * SCALE %>
min_lat: <%= 1 * SCALE %>
max_lon: <%= 4 * SCALE %>
max_lat: <%= 4 * SCALE %>
num_changes: 8
# changeset to contain all the invalid stuff that is in the
# fixtures (nodes outside the world, etc...), but needs to have
@ -42,5 +46,5 @@ invalid_changeset:
id: 5
user_id: 3
created_at: "2008-01-01 00:00:00"
open: false
closed_at: "2008-01-02 00:00:00"
num_changes: 9

View file

@ -618,15 +618,19 @@ EOF
get :query, :time => '2007-12-31'
assert_response :success, "can't get changesets by time-since"
assert_changesets [2,4,5]
assert_changesets [1,2,4,5]
get :query, :time => '2008-01-01T12:34Z'
assert_response :success, "can't get changesets by time-since with hour"
assert_changesets [2]
assert_changesets [1,2,4,5]
get :query, :time => '2007-12-31T23:59Z,2008-01-01T00:01Z'
assert_response :success, "can't get changesets by time-range"
assert_changesets [4,5]
assert_changesets [1,4,5]
get :query, :open => 'true'
assert_response :success, "can't get changesets by open-ness"
assert_changesets [1,2,4]
end
##
@ -700,6 +704,62 @@ EOF
assert_response :conflict
end
##
# check that a changeset can contain a certain max number of changes.
def test_changeset_limits
basic_authorization "test@openstreetmap.org", "test"
# open a new changeset
content "<osm><changeset/></osm>"
put :create
assert_response :success, "can't create a new changeset"
cs_id = @response.body.to_i
# start the counter just short of where the changeset should finish.
offset = 10
# alter the database to set the counter on the changeset directly,
# otherwise it takes about 6 minutes to fill all of them.
changeset = Changeset.find(cs_id)
changeset.num_changes = Changeset::MAX_ELEMENTS - offset
changeset.save!
with_controller(NodeController.new) do
# create a new node
content "<osm><node changeset='#{cs_id}' lat='0.0' lon='0.0'/></osm>"
put :create
assert_response :success, "can't create a new node"
node_id = @response.body.to_i
get :read, :id => node_id
assert_response :success, "can't read back new node"
node_doc = XML::Parser.string(@response.body).parse
node_xml = node_doc.find("//osm/node").first
# loop until we fill the changeset with nodes
offset.times do |i|
node_xml['lat'] = rand.to_s
node_xml['lon'] = rand.to_s
node_xml['version'] = (i+1).to_s
content node_doc
put :update, :id => node_id
assert_response :success, "attempt #{i} should have succeeded"
end
# trying again should fail
node_xml['lat'] = rand.to_s
node_xml['lon'] = rand.to_s
node_xml['version'] = offset.to_s
content node_doc
put :update, :id => node_id
assert_response :conflict, "final attempt should have failed"
end
changeset = Changeset.find(cs_id)
assert_equal Changeset::MAX_ELEMENTS + 1, changeset.num_changes
end
#------------------------------------------------------------
# utility functions
#------------------------------------------------------------