<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[12549] CalendarServer/branches/users/cdaboo/scheduling-queue-refresh</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/12549">12549</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2014-02-03 17:26:39 -0800 (Mon, 03 Feb 2014)</dd>
</dl>
<h3>Log Message</h3>
<pre>Fully enable scheduling queues. This currently does a "monolithic" organizer work item (all attendees in one txn),
but the organizer PUT is not blocked on that. A later revision will split the organizer work into smaller chunks.
There is also a schedule work queue visualization tool showing the active work items.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshconfcaldavdtestplist">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/conf/caldavd-test.plist</a></li>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcaldavdatastoreschedulingimplicitpy">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/implicit.py</a></li>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcaldavdatastoreschedulingworkpy">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/work.py</a></li>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemacurrentoracledialectsql">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current-oracle-dialect.sql</a></li>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemacurrentsql">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current.sql</a></li>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemaupgradesoracledialectupgrade_from_33_to_34sql">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_33_to_34.sql</a></li>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemaupgradespostgresdialectupgrade_from_33_to_34sql">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_33_to_34.sql</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServerbranchesuserscdabooschedulingqueuerefreshcalendarservertoolsschedule_workitemspy">CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/calendarserver/tools/schedule_workitems.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshcalendarservertoolsschedule_workitemspy"></a>
<div class="addfile"><h4>Added: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/calendarserver/tools/schedule_workitems.py (0 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/calendarserver/tools/schedule_workitems.py         (rev 0)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/calendarserver/tools/schedule_workitems.py        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -0,0 +1,228 @@
</span><ins>+#!/usr/bin/env python
+
+##
+# Copyright (c) 2006-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from __future__ import print_function
+
+from getopt import getopt, GetoptError
+import os
+import sys
+import curses
+import datetime
+
+from twisted.internet.defer import inlineCallbacks, succeed
+from calendarserver.tools.cmdline import utilityMain, WorkerService
+from calendarserver.push.notifier import PushNotificationWork
+from txdav.caldav.datastore.scheduling.work import ScheduleOrganizerWork, \
+ ScheduleReplyWork, ScheduleRefreshWork
+
+useCurses = True
+
+def usage(e=None):
+
+ name = os.path.basename(sys.argv[0])
+ print("usage: %s [options]" % (name,))
+ print("")
+ print(" TODO: describe usage")
+ print("")
+ print("options:")
+ print(" -h --help: print this help and exit")
+ print(" -e --error: send stderr to stdout")
+ print(" -f --config <path>: Specify caldavd.plist configuration path")
+ print("")
+
+ if e:
+ sys.exit(64)
+ else:
+ sys.exit(0)
+
+
+
+def main():
+
+ try:
+ (optargs, _ignore_args) = getopt(
+ sys.argv[1:], "hef:", [
+ "help",
+ "error",
+ "config=",
+ ],
+ )
+ except GetoptError, e:
+ usage(e)
+
+ #
+ # Get configuration
+ #
+ configFileName = None
+ debug = False
+
+ for opt, arg in optargs:
+ if opt in ("-h", "--help"):
+ usage()
+
+ if opt in ("-e", "--error"):
+ debug = True
+
+ elif opt in ("-f", "--config"):
+ configFileName = arg
+
+ else:
+ raise NotImplementedError(opt)
+
+ utilityMain(configFileName, WorkItemMonitorService, verbose=debug)
+
+
+
+class WorkItemMonitorService(WorkerService, object):
+
+ def __init__(self, store):
+ super(WorkItemMonitorService, self).__init__(store)
+ from twisted.internet import reactor
+ self.reactor = reactor
+
+
+ def doWork(self):
+ self.screen = curses.initscr() if useCurses else None
+ self.windows = []
+ self.updateScreenGeometry()
+ self.reactor.callLater(0, self.updateDisplay)
+ return succeed(None)
+
+
+ def postStartService(self):
+ """
+ Don't quit right away
+ """
+ pass
+
+
+ def updateScreenGeometry(self):
+ for win in self.windows:
+ del win
+ winY, winX = self.screen.getmaxyx() if useCurses else (100, 100)
+ seencolumns = [1]
+ seenrows = [1]
+ heightSoFar = 0
+ begin_x = 0
+ begin_y = 0
+ # Specify height and width of each window as one of:
+ # absolute value (int), e.g.: 42
+ # percentage of window height / width (string), e.g.: "42%"
+ # Specify row and column for each window as though it is a cell in an invisible html table
+ # Itemize windows in ascending order by row, col
+ for title, height, width, row, col, workItemClass, fmt, attrs in (
+ ("Organizer Requests", "100%", "25%", 1, 1, ScheduleOrganizerWork, "%s: %d", ("icalendarUid", "attendeeCount")),
+ ("Attendee Replies", "100%", "25%", 1, 2, ScheduleReplyWork, "%s", ("icalendarUid",)),
+ ("Attendee Refresh", "100%", "25%", 1, 3, ScheduleRefreshWork, "%s: %d", ("icalendarUid", "attendeeCount")),
+# ("Auto Reply", "100%", "25%", 1, 4, ScheduleAutoReplyWork, "%s", ("icalendarUid")),
+ ("Push Notifications", "100%", "25%", 1, 4, PushNotificationWork, "%s: %d", ("pushID", "priority")),
+ ):
+ if (isinstance(height, basestring)):
+ height = max(int(winY * (float(height.strip("%")) / 100.0)), 3)
+ if (isinstance(width, basestring)):
+ width = max(int(winX * (float(width.strip("%")) / 100.0)), 10)
+ if col not in seencolumns:
+ heightSoFar = max(height, heightSoFar)
+ seencolumns.append(col)
+ if row not in seenrows:
+ begin_y = heightSoFar
+ heightSoFar += height
+ begin_x = 0
+ seenrows.append(row)
+ seencolumns = [col]
+ window = WorkWindow(height, width, begin_y, begin_x,
+ self.store, title, workItemClass, fmt, attrs)
+ self.windows.append(window)
+ begin_x += width
+
+
+ @inlineCallbacks
+ def updateDisplay(self):
+ for window in self.windows:
+ try:
+ yield window.update()
+ except Exception as e:
+ print(str(e))
+ if not useCurses:
+ print("-------------")
+
+ self.reactor.callLater(0.1, self.updateDisplay)
+
+
+
+class WorkWindow(object):
+ def __init__(self, nlines, ncols, begin_y, begin_x,
+ store, title, workItemClass, fmt, attrs):
+ self.window = curses.newwin(nlines, ncols, begin_y, begin_x) if useCurses else None
+ self.ncols = ncols
+ self.store = store
+ self.title = title
+ self.workItemClass = workItemClass
+ self.fmt = fmt
+ self.attrs = attrs
+ self.iter = 0
+
+
+ @inlineCallbacks
+ def update(self):
+ txn = self.store.newTransaction()
+ records = (yield self.workItemClass.all(txn))
+ self.iter += 1
+
+ if useCurses:
+ self.window.erase()
+ self.window.border()
+ self.window.addstr(0, 2, self.title + " %d (%d)" % (len(records), self.iter,))
+
+ x = 1
+ y = 1
+ for record in records:
+ txt = ""
+ seconds = record.notBefore - datetime.datetime.utcnow()
+ try:
+ if useCurses:
+ self.window.addstr(y, x, "%d seconds" % int(seconds.total_seconds()))
+ else:
+ txt = "%s:" % (self.title,)
+ except curses.error:
+ continue
+ y += 1
+ if self.attrs:
+ try:
+ s = self.fmt % tuple([getattr(record, str(a)) for a in self.attrs])
+ except Exception, e:
+ s = "Error: %s" % (str(e),)
+ try:
+ if useCurses:
+ self.window.addnstr(y, x, s, self.ncols - 2)
+ else:
+ txt += " " + s
+ except curses.error:
+ pass
+ y += 1
+
+ if not useCurses:
+ print(txt)
+
+ if useCurses:
+ self.window.refresh()
+
+ yield txn.commit()
+
+
+if __name__ == "__main__":
+ main()
</ins><span class="cx">Property changes on: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/calendarserver/tools/schedule_workitems.py
</span><span class="cx">___________________________________________________________________
</span></span></pre></div>
<a id="svnexecutable"></a>
<div class="addfile"><h4>Added: svn:executable</h4></div>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshconfcaldavdtestplist"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/conf/caldavd-test.plist (12548 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/conf/caldavd-test.plist        2014-02-04 01:25:14 UTC (rev 12548)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/conf/caldavd-test.plist        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -796,7 +796,7 @@
</span><span class="cx">                 <key>WorkQueues</key>
</span><span class="cx">                 <dict>
</span><span class="cx">                         <key>Enabled</key>
</span><del>-                        <false/>
</del><ins>+                        <true/>
</ins><span class="cx"> <key>RequestDelaySeconds</key>
</span><span class="cx"> <integer>1</integer>
</span><span class="cx"> <key>ReplyDelaySeconds</key>
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcaldavdatastoreschedulingimplicitpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/implicit.py (12548 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/implicit.py        2014-02-04 01:25:14 UTC (rev 12548)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/implicit.py        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -395,7 +395,7 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><del>- def queuedOrganizerProcessing(self, txn, action, home, resource, uid, calendar, smart_merge):
</del><ins>+ def queuedOrganizerProcessing(self, txn, action, home, resource, uid, calendar_old, calendar_new, smart_merge):
</ins><span class="cx"> """
</span><span class="cx"> Process an organizer scheduling work queue item. The basic goal here is to setup the ImplicitScheduler as if
</span><span class="cx"> this operation were the equivalent of the PUT that enqueued the work, and then do the actual work.
</span><span class="lines">@@ -407,46 +407,44 @@
</span><span class="cx"> self.calendar_home = home
</span><span class="cx"> self.resource = resource
</span><span class="cx"> self.do_smart_merge = smart_merge
</span><ins>+ self.queuedResponses = []
</ins><span class="cx">
</span><ins>+ cal_uid = calendar_old.resourceUID() if calendar_old is not None else (calendar_new.resourceUID() if calendar_new is not None else "unknown")
+
</ins><span class="cx"> # Handle different action scenarios
</span><span class="cx"> if action == "create":
</span><del>- # resource is None, calendar is None
</del><ins>+ # resource is None, calendar_old is None
</ins><span class="cx"> # Find the newly created resource
</span><span class="cx"> resources = (yield self.calendar_home.objectResourcesWithUID(uid, ignore_children=["inbox"], allowShared=False))
</span><span class="cx"> if len(resources) != 1:
</span><span class="cx"> # Ughh - what has happened? It is possible the resource was created then deleted before we could start work processing,
</span><span class="cx"> # so simply ignore this
</span><del>- log.debug("ImplicitScheduler - queuedOrganizerProcessing 'create' cannot find organizer resource for UID: {uid}", uid=calendar.resourceUID())
</del><ins>+ log.debug("ImplicitScheduler - queuedOrganizerProcessing 'create' cannot find organizer resource for UID: {uid}", uid=cal_uid)
</ins><span class="cx"> returnValue(None)
</span><span class="cx"> self.resource = resources[0]
</span><ins>+ self.calendar = calendar_new
</ins><span class="cx">
</span><del>- # The calendar data to use is the current calendar data, not what was stored in the work item, since it might have been
- # updated a few times after the create, but those modifications are effectively coalesced into the create
- self.calendar = (yield self.resource.componentForUser())
-
- elif action == "modify":
</del><ins>+ elif action in ("modify", "modify-cancelled"):
</ins><span class="cx"> # Check that the resource still exists - it may have been deleted after this work item was queued, in which
</span><span class="cx"> # case we have to ignore this (on the assumption that the "remove" action will have queued some work that will
</span><span class="cx"> # execute soon).
</span><span class="cx"> if self.resource is None:
</span><del>- log.debug("ImplicitScheduler - queuedOrganizerProcessing 'modify' cannot find organizer resource for UID: {uid}", uid=calendar.resourceUID())
</del><ins>+ log.debug("ImplicitScheduler - queuedOrganizerProcessing 'modify' cannot find organizer resource for UID: {uid}", uid=cal_uid)
</ins><span class="cx"> returnValue(None)
</span><span class="cx">
</span><del>- # The new calendar data is what is currently stored - other modifications may have causes coalescing.
- # Old calendar data is what was stored int he work item
- self.calendar = (yield self.resource.componentForUser())
- self.oldcalendar = calendar
</del><ins>+ # The new calendar_old data is what is currently stored - other modifications may have causes coalescing.
+ # Old calendar_old data is what was stored int he work item
+ self.calendar = calendar_new
+ self.oldcalendar = calendar_old
</ins><span class="cx">
</span><span class="cx"> elif action == "remove":
</span><del>- # Check whether the resource still exists - it cannot be in existence as once it is deleted, its resource-id
- # should never be used again.
- if self.resource is not None:
- log.debug("ImplicitScheduler - queuedOrganizerProcessing 'remove' found an organizer resource for UID: {uid}", uid=calendar.resourceUID())
- raise ImplicitSchedulingWorkError("Resource exists for queued 'remove' scheduling work")
</del><ins>+ # A remove can happen when the underlying resource is deleted, or when all scheduling properties
+ # (organizer and attendees) are removed from its content. So sometimes the resource will not exist, other
+ # times it might. Thus we cannot make any assumptions about resource existence.
</ins><span class="cx">
</span><del>- # The "new" calendar data is in fact the calendar data at the time of the remove - which is the data stored
</del><ins>+ # The "new" calendar_old data is in fact the calendar_old data at the time of the remove - which is the data stored
</ins><span class="cx"> # in the work item.
</span><del>- self.calendar = calendar
</del><ins>+ self.calendar = calendar_old
</ins><span class="cx">
</span><span class="cx"> yield self.extractCalendarData()
</span><span class="cx"> self.organizerPrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
</span><span class="lines">@@ -615,11 +613,10 @@
</span><span class="cx"> self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
</span><span class="cx">
</span><span class="cx"> # CANCEL always bumps sequence
</span><del>- if not queued or not config.Scheduling.Options.WorkQueues.Enabled:
- self.needs_sequence_change = True
</del><ins>+ self.needs_sequence_change = True
</ins><span class="cx">
</span><span class="cx"> # Check for a new resource or an update
</span><del>- elif self.action == "modify":
</del><ins>+ elif self.action in ("modify", "modify-cancelled"):
</ins><span class="cx">
</span><span class="cx"> # Read in existing data
</span><span class="cx"> if not queued or not config.Scheduling.Options.WorkQueues.Enabled:
</span><span class="lines">@@ -678,8 +675,7 @@
</span><span class="cx">
</span><span class="cx"> # For now we always bump the sequence number on modifications because we cannot track DTSTAMP on
</span><span class="cx"> # the Attendee side. But we check the old and the new and only bump if the client did not already do it.
</span><del>- if not queued or not config.Scheduling.Options.WorkQueues.Enabled:
- self.needs_sequence_change = self.calendar.needsiTIPSequenceChange(self.oldcalendar)
</del><ins>+ self.needs_sequence_change = self.calendar.needsiTIPSequenceChange(self.oldcalendar)
</ins><span class="cx">
</span><span class="cx"> elif self.action == "create":
</span><span class="cx"> if self.split_details is None:
</span><span class="lines">@@ -694,12 +690,12 @@
</span><span class="cx"> if attendee.parameterValue("PARTSTAT", "NEEDS-ACTION").upper() == "NEEDS-ACTION":
</span><span class="cx"> attendee.setParameter("RSVP", "TRUE")
</span><span class="cx">
</span><del>- if self.needs_sequence_change:
- self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
-
</del><span class="cx"> # If processing a queue item, actually execute the scheduling operations, else queue it.
</span><span class="cx"> # Note a split is always queued, so we do not need to re-queue
</span><span class="cx"> if queued or not config.Scheduling.Options.WorkQueues.Enabled or self.split_details is not None:
</span><ins>+ if self.needs_sequence_change:
+ self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
+
</ins><span class="cx"> yield self.scheduleWithAttendees()
</span><span class="cx"> else:
</span><span class="cx"> yield self.queuedScheduleWithAttendees()
</span><span class="lines">@@ -1040,10 +1036,18 @@
</span><span class="cx"> self.calendar_home,
</span><span class="cx"> self.resource,
</span><span class="cx"> self.oldcalendar,
</span><ins>+ self.calendar,
</ins><span class="cx"> self.organizerPrincipal.canonicalCalendarUserAddress(),
</span><ins>+ len(self.calendar.getAllUniqueAttendees()) - 1,
</ins><span class="cx"> self.do_smart_merge,
</span><span class="cx"> )
</span><span class="cx">
</span><ins>+ # We bump the sequence AFTER storing the work item data to make sure that the sequence
+ # change does not cause unchanged components to be treated as changed when the work
+ # item executes.
+ if self.needs_sequence_change:
+ self.calendar.bumpiTIPInfo(oldcalendar=self.oldcalendar, doSequence=True)
+
</ins><span class="cx"> # First process cancelled attendees
</span><span class="cx"> total = (yield self.processQueuedCancels())
</span><span class="cx">
</span><span class="lines">@@ -1287,20 +1291,24 @@
</span><span class="cx">
</span><span class="cx"> def handleSchedulingResponse(self, response, is_organizer):
</span><span class="cx">
</span><del>- # Map each recipient in the response to a status code
- responses = {}
- propname = self.calendar.mainComponent().recipientPropertyName() if is_organizer else "ORGANIZER"
- for item in response.responses:
- recipient = str(item.recipient.children[0])
- status = str(item.reqstatus)
- responses[recipient] = status
</del><ins>+ # For a queued operation we stash the response away for the work item to deal with
+ if hasattr(self, "queuedResponses"):
+ self.queuedResponses.append(response)
+ else:
+ # Map each recipient in the response to a status code
+ responses = {}
+ propname = self.calendar.mainComponent().recipientPropertyName() if is_organizer else "ORGANIZER"
+ for item in response.responses:
+ recipient = str(item.recipient.children[0])
+ status = str(item.reqstatus)
+ responses[recipient] = status
</ins><span class="cx">
</span><del>- # Now apply to each ATTENDEE/ORGANIZER in the original data
- self.calendar.setParameterToValueForPropertyWithValue(
- "SCHEDULE-STATUS",
- status.split(";")[0],
- propname,
- recipient)
</del><ins>+ # Now apply to each ATTENDEE/ORGANIZER in the original data
+ self.calendar.setParameterToValueForPropertyWithValue(
+ "SCHEDULE-STATUS",
+ status.split(";")[0],
+ propname,
+ recipient)
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcaldavdatastoreschedulingworkpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/work.py (12548 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/work.py        2014-02-04 01:25:14 UTC (rev 12548)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/caldav/datastore/scheduling/work.py        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -33,6 +33,7 @@
</span><span class="cx"> import datetime
</span><span class="cx"> import hashlib
</span><span class="cx"> from pycalendar.datetime import DateTime
</span><ins>+import traceback
</ins><span class="cx">
</span><span class="cx"> __all__ = [
</span><span class="cx"> "ScheduleOrganizerWork",
</span><span class="lines">@@ -87,8 +88,7 @@
</span><span class="cx"> self.transaction.postCommit(_post)
</span><span class="cx">
</span><span class="cx">
</span><del>- @inlineCallbacks
- def handleSchedulingResponse(self, response, calendar, resource, is_organizer):
</del><ins>+ def handleSchedulingResponse(self, response, calendar, is_organizer):
</ins><span class="cx"> """
</span><span class="cx"> Update a user's calendar object resource based on the results of a queued scheduling
</span><span class="cx"> message response. Note we only need to update in the case where there is an error response
</span><span class="lines">@@ -99,8 +99,6 @@
</span><span class="cx"> @type response: L{caldavxml.ScheduleResponse}
</span><span class="cx"> @param calendar: original calendar component
</span><span class="cx"> @type calendar: L{Component}
</span><del>- @param resource: calendar object resource to update
- @type resource: L{CalendarObject}
</del><span class="cx"> @param is_organizer: whether or not iTIP message was sent by the organizer
</span><span class="cx"> @type is_organizer: C{bool}
</span><span class="cx"> """
</span><span class="lines">@@ -123,8 +121,7 @@
</span><span class="cx"> )
</span><span class="cx"> changed = True
</span><span class="cx">
</span><del>- if changed:
- yield resource._setComponentInternal(calendar, internal_state=ComponentUpdateState.ATTENDEE_ITIP_UPDATE)
</del><ins>+ return changed
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -138,16 +135,23 @@
</span><span class="cx">
</span><span class="cx"> @classmethod
</span><span class="cx"> @inlineCallbacks
</span><del>- def schedule(cls, txn, uid, action, home, resource, calendar, organizer, smart_merge):
</del><ins>+ def schedule(cls, txn, uid, action, home, resource, calendar_old, calendar_new, organizer, attendee_count, smart_merge):
</ins><span class="cx"> """
</span><span class="cx"> The actual arguments depend on the action:
</span><span class="cx">
</span><del>- 1) If action is "create", resource is None, calendar is None
- 2) If action is "modify", resource is existing resource, calendar is the old calendar data
- 3) If action is "remove", resource is the existing resource, calendar is the old calendar data
</del><ins>+ 1) If action is "create", resource is None, calendar_old is None, calendar_new is the new data
+ 2) If action is "modify", resource is existing resource, calendar_old is the old calendar_old data, and
+ calendar_new is the new data
+ 3) If action is "remove", resource is the existing resource, calendar_old is the old calendar_old data,
+ and calendar_new is None
</ins><span class="cx">
</span><del>- Note that for (1), when the work executes the resource will be in existence so we need to load it.
- Note that for (3), when work executes the resource will have been removed.
</del><ins>+ Right now we will also create the iTIP message based on the diff of calendar_old and calendar_new rather than
+ looking at the current state of the orgnaizer's resource (which may have changed since this work item was
+ filed). That means that we are basically NOT doing any coalescing of changes - instead every change results
+ in its own iTIP message (pretty much as it would without the queue). Ultimately we need to support coalescing
+ for performance benefit, but the logic involved in doing that is tricky (e.g., certain properties like
+ SCHEDULE-FORCE-SEND are not preserved in the saved data, yet need to be accounted for because they change the
+ nature of the iTIP processing).
</ins><span class="cx"> """
</span><span class="cx"> # Always queue up new work - coalescing happens when work is executed
</span><span class="cx"> notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=config.Scheduling.Options.WorkQueues.RequestDelaySeconds)
</span><span class="lines">@@ -158,7 +162,9 @@
</span><span class="cx"> scheduleAction=scheduleActionToSQL[action],
</span><span class="cx"> homeResourceID=home.id(),
</span><span class="cx"> resourceID=resource.id() if resource else None,
</span><del>- icalendarText=calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference) if calendar else None,
</del><ins>+ icalendarTextOld=calendar_old.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference) if calendar_old else None,
+ icalendarTextNew=calendar_new.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference) if calendar_new else None,
+ attendeeCount=attendee_count,
</ins><span class="cx"> smartMerge=smart_merge
</span><span class="cx"> ))
</span><span class="cx"> cls._enqueued()
</span><span class="lines">@@ -185,7 +191,8 @@
</span><span class="cx"> resource = (yield home.objectResourceWithID(self.resourceID))
</span><span class="cx"> organizerPrincipal = home.directoryService().recordWithUID(home.uid())
</span><span class="cx"> organizer = organizerPrincipal.canonicalCalendarUserAddress()
</span><del>- calendar = Component.fromString(self.icalendarText) if self.icalendarText else None
</del><ins>+ calendar_old = Component.fromString(self.icalendarTextOld) if self.icalendarTextOld else None
+ calendar_new = Component.fromString(self.icalendarTextNew) if self.icalendarTextNew else None
</ins><span class="cx">
</span><span class="cx"> log.debug("ScheduleOrganizerWork - running for ID: {id}, UID: {uid}, organizer: {org}", id=self.workID, uid=self.icalendarUid, org=organizer)
</span><span class="cx">
</span><span class="lines">@@ -194,15 +201,39 @@
</span><span class="cx">
</span><span class="cx"> from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
</span><span class="cx"> scheduler = ImplicitScheduler()
</span><del>- yield scheduler.queuedOrganizerProcessing(self.transaction, scheduleActionFromSQL[self.scheduleAction], home, resource, self.icalendarUid, calendar, self.smartMerge)
</del><ins>+ yield scheduler.queuedOrganizerProcessing(
+ self.transaction,
+ scheduleActionFromSQL[self.scheduleAction],
+ home,
+ resource,
+ self.icalendarUid,
+ calendar_old,
+ calendar_new,
+ self.smartMerge
+ )
</ins><span class="cx">
</span><ins>+ # Handle responses - update the actual resource in the store. Note that for a create the resource did not previously
+ # exist and is stored as None for the work item, but the scheduler will attempt to find the new resources and use
+ # that. We need to grab the scheduler's resource for further processing.
+ resource = scheduler.resource
+ if resource is not None:
+ changed = False
+ calendar = (yield resource.componentForUser())
+ for response in scheduler.queuedResponses:
+ changed |= yield self.handleSchedulingResponse(response, calendar, True)
+
+ if changed:
+ yield resource._setComponentInternal(calendar, internal_state=ComponentUpdateState.ORGANIZER_ITIP_UPDATE)
+
</ins><span class="cx"> self._dequeued()
</span><span class="cx">
</span><span class="cx"> except Exception, e:
</span><span class="cx"> log.debug("ScheduleOrganizerWork - exception ID: {id}, UID: '{uid}', {err}", id=self.workID, uid=self.icalendarUid, err=str(e))
</span><ins>+ log.debug(traceback.format_exc())
</ins><span class="cx"> raise
</span><span class="cx"> except:
</span><span class="cx"> log.debug("ScheduleOrganizerWork - bare exception ID: {id}, UID: '{uid}'", id=self.workID, uid=self.icalendarUid)
</span><ins>+ log.debug(traceback.format_exc())
</ins><span class="cx"> raise
</span><span class="cx">
</span><span class="cx"> log.debug("ScheduleOrganizerWork - done for ID: {id}, UID: {uid}, organizer: {org}", id=self.workID, uid=self.icalendarUid, org=organizer)
</span><span class="lines">@@ -297,8 +328,11 @@
</span><span class="cx">
</span><span class="cx"> # Send scheduling message and process response
</span><span class="cx"> response = (yield self.sendToOrganizer(home, "REPLY", itipmsg, attendee, organizer))
</span><del>- yield self.handleSchedulingResponse(response, calendar, resource, False)
</del><ins>+ changed = yield self.handleSchedulingResponse(response, calendar, False)
</ins><span class="cx">
</span><ins>+ if changed:
+ yield resource._setComponentInternal(calendar, internal_state=ComponentUpdateState.ATTENDEE_ITIP_UPDATE)
+
</ins><span class="cx"> self._dequeued()
</span><span class="cx">
</span><span class="cx"> except Exception, e:
</span><span class="lines">@@ -441,7 +475,8 @@
</span><span class="cx"> icalendarUid=organizer_resource.uid(),
</span><span class="cx"> homeResourceID=organizer_resource._home.id(),
</span><span class="cx"> resourceID=organizer_resource.id(),
</span><del>- notBefore=notBefore
</del><ins>+ attendeeCount=len(attendees),
+ notBefore=notBefore,
</ins><span class="cx"> ))
</span><span class="cx"> cls._enqueued()
</span><span class="cx"> yield proposal.whenProposed()
</span><span class="lines">@@ -494,8 +529,10 @@
</span><span class="cx"> notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=config.Scheduling.Options.WorkQueues.AttendeeRefreshBatchIntervalSeconds)
</span><span class="cx"> yield self.transaction.enqueue(
</span><span class="cx"> self.__class__,
</span><ins>+ icalendarUid=self.icalendarUid,
</ins><span class="cx"> homeResourceID=self.homeResourceID,
</span><span class="cx"> resourceID=self.resourceID,
</span><ins>+ attendeeCount=len(pendingAttendees),
</ins><span class="cx"> notBefore=notBefore
</span><span class="cx"> )
</span><span class="cx">
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemacurrentoracledialectsql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current-oracle-dialect.sql (12548 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current-oracle-dialect.sql        2014-02-04 01:25:14 UTC (rev 12548)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current-oracle-dialect.sql        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -400,7 +400,8 @@
</span><span class="cx"> "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
</span><span class="cx"> "ICALENDAR_UID" nvarchar2(255),
</span><span class="cx"> "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
</span><del>- "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
</del><ins>+ "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+ "ATTENDEE_COUNT" integer
</ins><span class="cx"> );
</span><span class="cx">
</span><span class="cx"> create table SCHEDULE_REFRESH_ATTENDEES (
</span><span class="lines">@@ -424,7 +425,9 @@
</span><span class="cx"> "SCHEDULE_ACTION" integer not null,
</span><span class="cx"> "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
</span><span class="cx"> "RESOURCE_ID" integer,
</span><del>- "ICALENDAR_TEXT" nclob,
</del><ins>+ "ICALENDAR_TEXT_OLD" nclob,
+ "ICALENDAR_TEXT_NEW" nclob,
+ "ATTENDEE_COUNT" integer,
</ins><span class="cx"> "SMART_MERGE" integer
</span><span class="cx"> );
</span><span class="cx">
</span><span class="lines">@@ -435,7 +438,8 @@
</span><span class="cx">
</span><span class="cx"> insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('create', 0);
</span><span class="cx"> insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify', 1);
</span><del>-insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 2);
</del><ins>+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify-cancelled', 2);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 3);
</ins><span class="cx"> create table SCHEDULE_REPLY_WORK (
</span><span class="cx"> "WORK_ID" integer primary key not null,
</span><span class="cx"> "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemacurrentsql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current.sql (12548 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current.sql        2014-02-04 01:25:14 UTC (rev 12548)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/current.sql        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -751,7 +751,8 @@
</span><span class="cx"> NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP),
</span><span class="cx"> ICALENDAR_UID                         varchar(255) not null,
</span><span class="cx"> HOME_RESOURCE_ID integer not null references CALENDAR_HOME on delete cascade,
</span><del>- RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade
</del><ins>+ RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
+ ATTENDEE_COUNT                                integer
</ins><span class="cx"> );
</span><span class="cx">
</span><span class="cx"> create index SCHEDULE_REFRESH_WORK_HOME_RESOURCE_ID on
</span><span class="lines">@@ -796,7 +797,9 @@
</span><span class="cx"> SCHEDULE_ACTION                                integer                 not null, -- Enum SCHEDULE_ACTION
</span><span class="cx"> HOME_RESOURCE_ID integer not null references CALENDAR_HOME on delete cascade,
</span><span class="cx"> RESOURCE_ID integer,         -- this references a possibly non-existent CALENDR_OBJECT
</span><del>- ICALENDAR_TEXT                                text,
</del><ins>+ ICALENDAR_TEXT_OLD                        text,
+ ICALENDAR_TEXT_NEW                        text,
+ ATTENDEE_COUNT                                integer,
</ins><span class="cx"> SMART_MERGE                                        boolean
</span><span class="cx"> );
</span><span class="cx">
</span><span class="lines">@@ -814,7 +817,8 @@
</span><span class="cx">
</span><span class="cx"> insert into SCHEDULE_ACTION values (0, 'create');
</span><span class="cx"> insert into SCHEDULE_ACTION values (1, 'modify');
</span><del>-insert into SCHEDULE_ACTION values (2, 'remove');
</del><ins>+insert into SCHEDULE_ACTION values (2, 'modify-cancelled');
+insert into SCHEDULE_ACTION values (3, 'remove');
</ins><span class="cx">
</span><span class="cx"> -------------------------
</span><span class="cx"> -- Schedule Reply Work --
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemaupgradesoracledialectupgrade_from_33_to_34sql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_33_to_34.sql (12548 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_33_to_34.sql        2014-02-04 01:25:14 UTC (rev 12548)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_33_to_34.sql        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -25,7 +25,8 @@
</span><span class="cx"> "NOT_BEFORE" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
</span><span class="cx"> "ICALENDAR_UID" nvarchar2(255),
</span><span class="cx"> "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
</span><del>- "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade
</del><ins>+ "RESOURCE_ID" integer not null references CALENDAR_OBJECT on delete cascade,
+ "ATTENDEE_COUNT" integer
</ins><span class="cx"> );
</span><span class="cx">
</span><span class="cx"> create index SCHEDULE_REFRESH_WORK_26084c7b on SCHEDULE_REFRESH_WORK (
</span><span class="lines">@@ -72,7 +73,9 @@
</span><span class="cx"> "SCHEDULE_ACTION" integer not null,
</span><span class="cx"> "HOME_RESOURCE_ID" integer not null references CALENDAR_HOME on delete cascade,
</span><span class="cx"> "RESOURCE_ID" integer,
</span><del>- "ICALENDAR_TEXT" nclob,
</del><ins>+ "ICALENDAR_TEXT_OLD" nclob,
+ "ICALENDAR_TEXT_NEW" nclob,
+ "ATTENDEE_COUNT" integer,
</ins><span class="cx"> "SMART_MERGE" integer
</span><span class="cx"> );
</span><span class="cx">
</span><span class="lines">@@ -91,7 +94,8 @@
</span><span class="cx">
</span><span class="cx"> insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('create', 0);
</span><span class="cx"> insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify', 1);
</span><del>-insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 2);
</del><ins>+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('modify-cancelled', 2);
+insert into SCHEDULE_ACTION (DESCRIPTION, ID) values ('remove', 3);
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> create table SCHEDULE_REPLY_WORK (
</span></span></pre></div>
<a id="CalendarServerbranchesuserscdabooschedulingqueuerefreshtxdavcommondatastoresql_schemaupgradespostgresdialectupgrade_from_33_to_34sql"></a>
<div class="modfile"><h4>Modified: CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_33_to_34.sql (12548 => 12549)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_33_to_34.sql        2014-02-04 01:25:14 UTC (rev 12548)
+++ CalendarServer/branches/users/cdaboo/scheduling-queue-refresh/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_33_to_34.sql        2014-02-04 01:26:39 UTC (rev 12549)
</span><span class="lines">@@ -29,7 +29,8 @@
</span><span class="cx"> NOT_BEFORE timestamp default timezone('UTC', CURRENT_TIMESTAMP),
</span><span class="cx"> ICALENDAR_UID                         varchar(255) not null,
</span><span class="cx"> HOME_RESOURCE_ID integer not null references CALENDAR_HOME on delete cascade,
</span><del>- RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade
</del><ins>+ RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
+ ATTENDEE_COUNT                                integer
</ins><span class="cx"> );
</span><span class="cx">
</span><span class="cx"> create index SCHEDULE_REFRESH_WORK_HOME_RESOURCE_ID on
</span><span class="lines">@@ -74,7 +75,9 @@
</span><span class="cx"> SCHEDULE_ACTION                                integer                 not null, -- Enum SCHEDULE_ACTION
</span><span class="cx"> HOME_RESOURCE_ID integer not null references CALENDAR_HOME on delete cascade,
</span><span class="cx"> RESOURCE_ID integer,         -- this references a possibly non-existent CALENDR_OBJECT
</span><del>- ICALENDAR_TEXT                                text,
</del><ins>+ ICALENDAR_TEXT_OLD                        text,
+ ICALENDAR_TEXT_NEW                        text,
+ ATTENDEE_COUNT                                integer,
</ins><span class="cx"> SMART_MERGE                                        boolean
</span><span class="cx"> );
</span><span class="cx">
</span><span class="lines">@@ -92,7 +95,8 @@
</span><span class="cx">
</span><span class="cx"> insert into SCHEDULE_ACTION values (0, 'create');
</span><span class="cx"> insert into SCHEDULE_ACTION values (1, 'modify');
</span><del>-insert into SCHEDULE_ACTION values (2, 'remove');
</del><ins>+insert into SCHEDULE_ACTION values (2, 'modify-cancelled');
+insert into SCHEDULE_ACTION values (3, 'remove');
</ins><span class="cx">
</span><span class="cx"> -------------------------
</span><span class="cx"> -- Schedule Reply Work --
</span></span></pre>
</div>
</div>
</body>
</html>