diff --git a/resources/sql/patches/029.cursors.sql b/resources/sql/patches/029.cursors.sql new file mode 100644 index 0000000000..0512e45e12 --- /dev/null +++ b/resources/sql/patches/029.cursors.sql @@ -0,0 +1,13 @@ +ALTER TABLE phabricator_timeline.timeline_event + ADD dataID int unsigned; + +ALTER TABLE phabricator_timeline.timeline_event + ADD UNIQUE KEY (dataID); + +UPDATE phabricator_timeline.timeline_event e, + phabricator_timeline.timeline_eventdata d + SET e.dataID = d.id + WHERE d.eventID = e.id; + +ALTER TABLE phabricator_timeline.timeline_eventdata + DROP eventID; diff --git a/scripts/sql/upgrade_schema.php b/scripts/sql/upgrade_schema.php index 3cc2854009..26417647c2 100755 --- a/scripts/sql/upgrade_schema.php +++ b/scripts/sql/upgrade_schema.php @@ -1,142 +1,153 @@ #!/usr/bin/env php establishConnection('w'); $create_sql = <<establishConnection('w'); $create_sql = <<withSuffix('sql'); $results = $finder->find(); $patches = array(); foreach ($results as $r) { $matches = array(); if (preg_match('/(\d+)\..*\.sql$/', $r, $matches)) { $patches[] = array('version' => (int)$matches[1], 'file' => $r); } else { print "*** WARNING : File {$r} does not follow the normal naming ". "convention. ***\n"; } } // Files are in some 'random' order returned by the operating system // We need to apply them in proper order $patches = isort($patches, 'version'); $patch_applied = false; foreach ($patches as $patch) { if ($patch['version'] < $next_version) { continue; } print "Applying patch {$patch['file']}\n"; $path = Filesystem::resolvePath($patches_dir.$patch['file']); $user = PhabricatorEnv::getEnvConfig('mysql.user'); $pass = PhabricatorEnv::getEnvConfig('mysql.pass'); $host = PhabricatorEnv::getEnvConfig('mysql.host'); list($stdout, $stderr) = execx( "mysql --user=%s --password=%s --host=%s < %s", $user, $pass, $host, $path); if ($stderr) { print $stderr; exit(-1); } // Patch was successful, update the db with the latest applied patch version // 'DELETE' and 'INSERT' instead of update, because the table might be empty queryfx($conn, 'DELETE FROM %T', TABLE_NAME); queryfx($conn, 'INSERT INTO %T values (%d)', TABLE_NAME, $patch['version']); $patch_applied = true; } if (!$patch_applied) { print "Your database is already up-to-date\n"; } diff --git a/src/applications/daemon/controller/timelineevent/PhabricatorDaemonTimelineEventController.php b/src/applications/daemon/controller/timelineevent/PhabricatorDaemonTimelineEventController.php index 126d37a1b4..a1499d141d 100644 --- a/src/applications/daemon/controller/timelineevent/PhabricatorDaemonTimelineEventController.php +++ b/src/applications/daemon/controller/timelineevent/PhabricatorDaemonTimelineEventController.php @@ -1,79 +1,80 @@ id = $data['id']; } public function processRequest() { $event = id(new PhabricatorTimelineEvent('NULL'))->load($this->id); if (!$event) { return new Aphront404Response(); } $request = $this->getRequest(); $user = $request->getUser(); - $data = id(new PhabricatorTimelineEventData())->loadOneWhere( - 'eventID = %d', - $event->getID()); + if ($event->getDataID()) { + $data = id(new PhabricatorTimelineEventData())->load( + $event->getDataID()); + } if ($data) { $data = json_encode($data->getEventData()); } else { $data = 'null'; } $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('ID') ->setValue($event->getID())) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Type') ->setValue($event->getType())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setDisabled(true) ->setLabel('Data') ->setValue($data)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/daemon/timeline/')); $panel = new AphrontPanelView(); $panel->setHeader('Event'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Timeline Event', )); } } diff --git a/src/infrastructure/daemon/timeline/cursor/iterator/PhabricatorTimelineIterator.php b/src/infrastructure/daemon/timeline/cursor/iterator/PhabricatorTimelineIterator.php index 1aadedeefc..0fc1b5bcd4 100644 --- a/src/infrastructure/daemon/timeline/cursor/iterator/PhabricatorTimelineIterator.php +++ b/src/infrastructure/daemon/timeline/cursor/iterator/PhabricatorTimelineIterator.php @@ -1,116 +1,116 @@ cursorName = $cursor_name; $this->eventTypes = $event_types; } protected function loadEvents() { if (!$this->cursor) { $this->cursor = id(new PhabricatorTimelineCursor())->loadOneWhere( 'name = %s', $this->cursorName); if (!$this->cursor) { $cursor = new PhabricatorTimelineCursor(); $cursor->setName($this->cursorName); $cursor->setPosition(0); $cursor->save(); $this->cursor = $cursor; } } $event = new PhabricatorTimelineEvent('NULL'); $event_data = new PhabricatorTimelineEventData(); $raw_data = queryfx_all( $event->establishConnection('r'), 'SELECT event.*, event_data.eventData eventData FROM %T event - LEFT JOIN %T event_data ON event_data.eventID = event.id + LEFT JOIN %T event_data ON event_data.id = event.dataID WHERE event.id > %d AND event.type in (%Ls) ORDER BY event.id ASC LIMIT %d', $event->getTableName(), $event_data->getTableName(), $this->cursor->getPosition(), $this->eventTypes, self::LOAD_CHUNK_SIZE); $events = $event->loadAllFromArray($raw_data); $events = mpull($events, null, 'getID'); $raw_data = ipull($raw_data, 'eventData', 'id'); foreach ($raw_data as $id => $data) { if ($data) { $decoded = json_decode($data, true); $events[$id]->setData($decoded); } } $this->events = $events; if ($this->events) { $this->events = array_values($this->events); $this->index = 0; } else { $this->cursor = null; } } public function current() { return $this->events[$this->index]; } public function key() { return $this->events[$this->index]->getID(); } public function next() { if ($this->valid()) { $this->cursor->setPosition($this->key()); $this->cursor->save(); } $this->index++; if (!$this->valid()) { $this->loadEvents(); } } public function valid() { return isset($this->events[$this->index]); } public function rewind() { if (!$this->valid()) { $this->loadEvents(); } } } diff --git a/src/infrastructure/daemon/timeline/storage/event/PhabricatorTimelineEvent.php b/src/infrastructure/daemon/timeline/storage/event/PhabricatorTimelineEvent.php index 966fa4e94d..8ff507624a 100644 --- a/src/infrastructure/daemon/timeline/storage/event/PhabricatorTimelineEvent.php +++ b/src/infrastructure/daemon/timeline/storage/event/PhabricatorTimelineEvent.php @@ -1,65 +1,71 @@ type = $type; $this->data = $data; } public function getConfiguration() { return array( self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } public function recordEvent() { if ($this->getID()) { throw new Exception("Event has already been recorded!"); } - $this->save(); - + // Save the data first and point to it from the event to avoid a race + // condition where we insert the event before the data and a consumer reads + // it immediately. if ($this->data !== null) { $data = new PhabricatorTimelineEventData(); - $data->setEventID($this->getID()); $data->setEventData($this->data); $data->save(); + + $this->setDataID($data->getID()); } + + $this->save(); } public function setData($data) { $this->data = $data; return $this; } public function getData() { return $this->data; } } diff --git a/src/infrastructure/daemon/timeline/storage/eventdata/PhabricatorTimelineEventData.php b/src/infrastructure/daemon/timeline/storage/eventdata/PhabricatorTimelineEventData.php index 0bcf9d3035..407d4e1ece 100644 --- a/src/infrastructure/daemon/timeline/storage/eventdata/PhabricatorTimelineEventData.php +++ b/src/infrastructure/daemon/timeline/storage/eventdata/PhabricatorTimelineEventData.php @@ -1,33 +1,32 @@ array( 'eventData' => self::SERIALIZATION_JSON, ), self::CONFIG_TIMESTAMPS => false, ) + parent::getConfiguration(); } }