diff options
author | Grzegorz Kossakowski <grek@google.com> | 2009-09-10 12:27:02 -0700 |
---|---|---|
committer | Grzegorz Kossakowski <grek@google.com> | 2009-09-10 14:32:09 -0700 |
commit | a6992c36fc0d23bba01e6f07e2eb2f998b535079 (patch) | |
tree | 497b41ccdae1a7bbbab7211e8fcf060c40682187 | |
parent | 6423b017843d490599b178b7064a28b34811fcdc (diff) | |
download | gimd-a6992c36fc0d23bba01e6f07e2eb2f998b535079.tar.gz |
Implemented modifications Gimd database stored using JGit library.
While this functionality a number of changes to API has been made, mainly:
* functionality of DatabaseSpi trait has been moved to Database trait
* code from JGitProvider has been moved to JGitSnapshot as all operations
should be executed on specific Snapshot of Database
Modifications itself are implemented by taking function that takes Snapshot
and returns DatabaseModification object which carries information about
modifications that should be applied on top of passed Snapshot. Then,
modified Snapshot is tried to be safely merged with latest Snapshot of
Database.
Also, made JGitDatabaseException to store reference to JGit repository
that was in use when exception had been thrown.
Change-Id: I3f1cbcc89559569c831441e87c533627f3a78c10
Signed-off-by: Grzegorz Kossakowski <grek@google.com>
-rw-r--r-- | src/main/scala/com/google/gimd/Database.scala | 57 | ||||
-rw-r--r-- | src/main/scala/com/google/gimd/Snapshot.scala (renamed from src/main/scala/com/google/gimd/DatabaseSpi.scala) | 19 | ||||
-rw-r--r-- | src/main/scala/com/google/gimd/jgit/JGitDatabase.scala | 157 | ||||
-rw-r--r-- | src/main/scala/com/google/gimd/jgit/JGitDatabaseException.scala (renamed from src/main/scala/com/google/gimd/jgit/JGitProviderException.scala) | 11 | ||||
-rw-r--r-- | src/main/scala/com/google/gimd/jgit/JGitFile.scala | 6 | ||||
-rw-r--r-- | src/main/scala/com/google/gimd/jgit/JGitMergeRetriesExceededException.scala | 26 | ||||
-rw-r--r-- | src/main/scala/com/google/gimd/jgit/JGitSnapshot.scala (renamed from src/main/scala/com/google/gimd/jgit/JGitProvider.scala) | 21 | ||||
-rw-r--r-- | src/test/scala/com/google/gimd/jgit/JGitDatabaseTestCase.scala (renamed from src/test/scala/com/google/gimd/jgit/JGitProviderTestCase.scala) | 74 | ||||
-rw-r--r-- | src/test/scala/com/google/gimd/jgit/JGitFileTestCase.scala | 3 |
9 files changed, 334 insertions, 40 deletions
diff --git a/src/main/scala/com/google/gimd/Database.scala b/src/main/scala/com/google/gimd/Database.scala new file mode 100644 index 0000000..9047606 --- /dev/null +++ b/src/main/scala/com/google/gimd/Database.scala @@ -0,0 +1,57 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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. + +package com.google.gimd + +import file.FileType +import modification.DatabaseModification +import query.Predicate + +trait Database { + + /** + * Query database for all user objects of type U stored in files of + * type FileType[W] satisfying predicate p using latest Snapshot of Database. + * + * @throws GimdException + */ + @throws(classOf[GimdException]) + def query[U,W](ft: FileType[W], p: Predicate[U]): Iterator[U] = + //this method forgets about handles as this method is not run in context of any Snapshot so they + //become invalid immediately + latestSnapshot.query(ft, p).map(_._2) + + /** + * <p>Method that allows modification of latest Snapshot of Database.</p> + * + * <p>For single DatabaseModification derived from Snapshot READ COMMITTED level of isolation is + * guaranteed. <strong>The level of isolation might be changed in a future but only to level which + * provides better isolation.</strong></p> + * + * @param modification A function <code>Snapshot => DatabaseModification</code> that should + * follow referential transparency rule as it can be called an arbitrary + * number of times. Result of this function is an object that specifies what + * kind of modifications should be performed on top of passed Snapshot. + * + * @throws GimdException + */ + @throws(classOf[GimdException]) + def modify(modification: Snapshot => DatabaseModification) + + /** + * Factory method returning latest Snapshot of Database. + */ + protected def latestSnapshot: Snapshot + +} diff --git a/src/main/scala/com/google/gimd/DatabaseSpi.scala b/src/main/scala/com/google/gimd/Snapshot.scala index 05cd146..6ee5208 100644 --- a/src/main/scala/com/google/gimd/DatabaseSpi.scala +++ b/src/main/scala/com/google/gimd/Snapshot.scala @@ -17,21 +17,15 @@ package com.google.gimd import file.{File, FileType} import query.{Predicate, Handle} -/** - * Trait that provides all functionality for Gimd to ask specific storage implementation - * for needed data in an efficient way. - */ -trait DatabaseSpi { - - /** - * @return iterator over collection of all Files that conform to passed FileType[T]. - */ - def all[T](fileType: FileType[T]): Iterator[File[T]] +trait Snapshot { /** * Query database for all user objects of type U stored in files of * type FileType[W] satisfying predicate p. + * + * @throws GimdException */ + @throws(classOf[GimdException]) def query[U,W](ft: FileType[W], p: Predicate[U]): Iterator[(Handle[U],U)] = { for { f <- all(ft) @@ -39,4 +33,9 @@ trait DatabaseSpi { } yield r } + /** + * @return iterator over collection of all Files that conform to passed FileType[T]. + */ + protected def all[T](fileType: FileType[T]): Iterator[File[T]] + } diff --git a/src/main/scala/com/google/gimd/jgit/JGitDatabase.scala b/src/main/scala/com/google/gimd/jgit/JGitDatabase.scala new file mode 100644 index 0000000..0b02a6f --- /dev/null +++ b/src/main/scala/com/google/gimd/jgit/JGitDatabase.scala @@ -0,0 +1,157 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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. + +package com.google.gimd.jgit + +import file.{FileType, File} +import java.io.{IOException, ByteArrayInputStream} +import text.Formatter +import modification.DatabaseModification +import org.spearce.jgit.lib._ +import org.spearce.jgit.merge.MergeStrategy +import org.spearce.jgit.lib.RefUpdate.Result +import org.spearce.jgit.dircache.{DirCache, DirCacheEditor, DirCacheEntry} +import org.spearce.jgit.revwalk.{RevCommit, RevTree, RevWalk} + +final class JGitDatabase(repository: Repository) extends Database { + + /** + * The maximal number of merge/transaction rebase retries. + * + * If this number of retries is reached attempt to apply modifications will be aborted by throwing + * an exception. + * + * This boundary is needed in order to avoid resource starvation, specifically - livelock. It may + * happen when there is another process constantly changing latest Snapshot so modification has to + * be constantly remerged or even rebased. Proper solution to this problem should be fixing + * scheduling algorithm that would assign priority to each modification task which is proportional + * to number of retries given task already performed. + * + * Unfortunately we don't have control over scheduling threads here. + */ + private val MERGE_RETRIES = 10 + + def latestSnapshot: JGitSnapshot = { + try { + val id = repository.resolve(Constants.HEAD) + new JGitSnapshot(repository, new RevWalk(repository).parseCommit(id)) + } catch { + case e: IOException => throw new JGitDatabaseException(repository, e) + } + } + + def modify(modification: Snapshot => DatabaseModification) = { + val successful = try { + retry(MERGE_RETRIES) { + val snapshot = latestSnapshot + val dbModification = modification(snapshot) + applyModification(dbModification, Constants.HEAD, snapshot.commit) + } + } catch { + case e: IOException => throw new JGitDatabaseException(repository, e) + } + + if (!successful) + throw new JGitMergeRetriesExceededException(repository, MERGE_RETRIES) + } + + def applyModification(modification: DatabaseModification, branch: String, onto: RevCommit): + Boolean = { + val treeId = writeMessages(modification, onto.getTree) + val commitId = createCommit(treeId, onto) + val result = updateRef(branch, onto, commitId) + result match { + //there was no change to database since onto commit so changes were applied cleanly + case Result.FAST_FORWARD => true + //if there was a change since onto commit update gets rejected. Still it might be that change + //did not affected files we are trying to modify. Thus we are trying merging both changes. + case Result.REJECTED => tryMerging(branch, commitId) + //TODO: There should be a special treatment of LOCK_FAILURE case but it's low-priority task + //the idea is to not try merging if just JGit fails to obtain lock for given reference + case _ => + throw new JGitDatabaseException(repository, + "RefUpdate returned unexpected result: %1s.".format(result)) + } + } + + private def updateRef(name: String, oldCommit: ObjectId, newCommit: ObjectId): Result = { + val refUpdate = repository.updateRef(name) + refUpdate.setExpectedOldObjectId(oldCommit) + refUpdate.setNewObjectId(newCommit) + refUpdate.update() + } + + private def createCommit(treeId: ObjectId, parents: ObjectId*): ObjectId = { + //TODO: This identity should be replaced with something more meaningful + val ident = new PersonIdent("A U Thor", "author@example.com") + val commit = new Commit(repository, Array(parents: _*)) + commit.setTreeId(treeId) + commit.setAuthor(ident) + commit.setCommitter(ident) + commit.setMessage("Comitted by Gimd.\n") + commit.commit() + commit.getCommitId + } + + private def writeMessages(modification: DatabaseModification, oldTree: RevTree): ObjectId = { + val dirCache = DirCache.newInCore + val builder = dirCache.builder + builder.addTree(new Array[byte](0), 0, repository, oldTree) + builder.finish + + val objectWriter = new ObjectWriter(repository) + class EditMessage(file: File[_], newMsg: Message) extends DirCacheEditor.PathEdit(file.path) { + def apply(entry: DirCacheEntry) { + val text = Formatter.format(newMsg) + val blobId = objectWriter.writeBlob(text.getBytes(Constants.CHARACTER_ENCODING)) + entry.setFileMode(FileMode.REGULAR_FILE) + entry.setObjectId(blobId) + } + } + val editor = dirCache.editor + for ((file, message) <- modification.reduce) { + message match { + case None => editor.add(new DirCacheEditor.DeletePath(file.path)) + case Some(x) => editor.add(new EditMessage(file, x)) + } + } + editor.finish + dirCache.writeTree(objectWriter) + } + + private def merge(branch: String, commitToBeMerged: ObjectId): Boolean = { + val baseCommit = repository.resolve(branch) + val merger = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repository) + if (merger.merge(baseCommit, commitToBeMerged)) { + val treeId = merger.getResultTreeId + val mergeCommit = createCommit(treeId, baseCommit, commitToBeMerged) + val result = updateRef(branch, baseCommit, mergeCommit) + Result.FAST_FORWARD == result + } else + false + } + + private def tryMerging(branch: String, commitToBeMerged: ObjectId) = + retry(MERGE_RETRIES)(merge(branch, commitToBeMerged)) + + private def retry(howManyTimes: Int)(what: => Boolean): Boolean = + if (howManyTimes > 0) + if (what) + true + else + retry(howManyTimes-1)(what) + else + false + +} diff --git a/src/main/scala/com/google/gimd/jgit/JGitProviderException.scala b/src/main/scala/com/google/gimd/jgit/JGitDatabaseException.scala index df0f3d6..1245df9 100644 --- a/src/main/scala/com/google/gimd/jgit/JGitProviderException.scala +++ b/src/main/scala/com/google/gimd/jgit/JGitDatabaseException.scala @@ -14,9 +14,14 @@ package com.google.gimd.jgit -class JGitProviderException(message: String, cause: Throwable) - extends DatabaseSpiException(message, cause) { - def this(message: String) = this(message, null) +import org.spearce.jgit.lib.Repository +class JGitDatabaseException(val repository: Repository, + val msg: String, + val cause: Throwable) extends GimdException(msg, cause) { + + def this(repository: Repository, msg: String) = this(repository, msg, null) + + def this(repository: Repository, cause: Throwable) = this(repository, null, cause) } diff --git a/src/main/scala/com/google/gimd/jgit/JGitFile.scala b/src/main/scala/com/google/gimd/jgit/JGitFile.scala index 9ff43cd..dd110d5 100644 --- a/src/main/scala/com/google/gimd/jgit/JGitFile.scala +++ b/src/main/scala/com/google/gimd/jgit/JGitFile.scala @@ -20,15 +20,15 @@ import org.spearce.jgit.lib.{ObjectId, Repository} import text.Parser final class JGitFile[T](val path: String, val blobId: ObjectId, val fileType: FileType[T], - val jgitRepository: Repository) extends File[T] { + val repository: Repository) extends File[T] { lazy val message = Parser.parse(new InputStreamReader(blobInputStream, "UTF-8")) lazy val userObject = fileType.userType.toUserObject(message) private lazy val blobInputStream = { - val objectLoader = jgitRepository.openBlob(blobId) + val objectLoader = repository.openBlob(blobId) if (objectLoader == null) - throw new JGitProviderException("Blob '" + blobId.name + "' does not exist.") + throw new JGitDatabaseException(repository, "Blob '" + blobId.name + "' does not exist.") new ByteArrayInputStream(objectLoader.getCachedBytes) } diff --git a/src/main/scala/com/google/gimd/jgit/JGitMergeRetriesExceededException.scala b/src/main/scala/com/google/gimd/jgit/JGitMergeRetriesExceededException.scala new file mode 100644 index 0000000..b982850 --- /dev/null +++ b/src/main/scala/com/google/gimd/jgit/JGitMergeRetriesExceededException.scala @@ -0,0 +1,26 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// 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. + +package com.google.gimd.jgit + +import org.spearce.jgit.lib.Repository + +/** + * An exception thrown in a case when modifications cannot be applied to JGitDatabase. + * + * @see JGitDatabase + */ +class JGitMergeRetriesExceededException(override val repository: Repository, val retries: Int) + extends JGitDatabaseException(repository, ("Tried to rebase transaction %1s times and " + + "failed to merge changes successfuly. Aborting.").format(retries)) diff --git a/src/main/scala/com/google/gimd/jgit/JGitProvider.scala b/src/main/scala/com/google/gimd/jgit/JGitSnapshot.scala index be9f47d..2fd01ef 100644 --- a/src/main/scala/com/google/gimd/jgit/JGitProvider.scala +++ b/src/main/scala/com/google/gimd/jgit/JGitSnapshot.scala @@ -14,23 +14,16 @@ package com.google.gimd.jgit -import file.{FileType, File} -import org.spearce.jgit.lib._ -import org.spearce.jgit.revwalk.{RevCommit, RevWalk} -import org.spearce.jgit.treewalk.filter.{AndTreeFilter, PathFilter, PathSuffixFilter} -import org.spearce.jgit.treewalk.filter.TreeFilter +import file.{File, FileType} +import org.spearce.jgit.lib.{Repository, FileMode} +import org.spearce.jgit.revwalk.RevCommit +import org.spearce.jgit.treewalk.filter.{PathFilter, PathSuffixFilter, AndTreeFilter, TreeFilter} import org.spearce.jgit.treewalk.TreeWalk -final class JGitProvider(val jgitRepository: Repository) extends DatabaseSpi { +final class JGitSnapshot(val repository: Repository, val commit: RevCommit) extends Snapshot { def all[T](fileType: FileType[T]): Iterator[File[T]] = { - val id = jgitRepository.resolve(Constants.HEAD) - val rw = new RevWalk(jgitRepository) - all(fileType, rw.parseCommit(id)) - } - - private def all[T](fileType: FileType[T], commit: RevCommit): Iterator[File[T]] = { - val treeWalk = new TreeWalk(jgitRepository) + val treeWalk = new TreeWalk(repository) treeWalk.reset(commit.getTree) treeWalk.setRecursive(true) treeWalk.setFilter(treeFilter(fileType)) @@ -42,7 +35,7 @@ final class JGitProvider(val jgitRepository: Repository) extends DatabaseSpi { if (!hasNext) throw new NoSuchElementException val result = - new JGitFile(treeWalk.getPathString, treeWalk.getObjectId(0), fileType, jgitRepository) + new JGitFile(treeWalk.getPathString, treeWalk.getObjectId(0), fileType, repository) doesHasNext = treeWalk.next result } diff --git a/src/test/scala/com/google/gimd/jgit/JGitProviderTestCase.scala b/src/test/scala/com/google/gimd/jgit/JGitDatabaseTestCase.scala index 1c32f6e..14ad484 100644 --- a/src/test/scala/com/google/gimd/jgit/JGitProviderTestCase.scala +++ b/src/test/scala/com/google/gimd/jgit/JGitDatabaseTestCase.scala @@ -15,11 +15,13 @@ package com.google.gimd.jgit import file.{File, FileType} +import modification.DatabaseModification import org.junit.Test import org.junit.Assert._ -import org.spearce.jgit.lib.ObjectId +import org.spearce.jgit.lib.{Constants, ObjectId} +import query.Predicate -final class JGitProviderTestCase extends AbstractJGitTestCase { +final class JGitDatabaseTestCase extends AbstractJGitTestCase { case class SimpleMessage(name: String, value: Int) @@ -50,9 +52,9 @@ final class JGitProviderTestCase extends AbstractJGitTestCase { ) commit(files) - val db = new JGitProvider(repository) + val db = new JGitDatabase(repository) - val foundFiles = db.all(SimpleMessageFileType).toList + val foundFiles = db.latestSnapshot.all(SimpleMessageFileType).toList val expected = List("sm/first.sm", "sm/second.sm") assertEquals(expected, foundFiles.map(_.path)) @@ -70,9 +72,9 @@ final class JGitProviderTestCase extends AbstractJGitTestCase { ) commit(files) - val db = new JGitProvider(repository) + val db = new JGitDatabase(repository) - val foundFiles = db.all(SimpleMessageFileType).toList + val foundFiles = db.latestSnapshot.all(SimpleMessageFileType).toList val expected = List(first, second) assertEquals(expected, foundFiles.map(_.userObject)) @@ -91,12 +93,68 @@ final class JGitProviderTestCase extends AbstractJGitTestCase { val commitId = createCommit("Test commit", treeId) moveHEAD(commitId) - val db = new JGitProvider(repository) + val db = new JGitDatabase(repository) - val foundFiles = db.all(SimpleMessageFileType).toList + val foundFiles = db.latestSnapshot.all(SimpleMessageFileType).toList assertEquals(Nil, foundFiles) } + @Test + def modifySimpleMessages { + import query.Predicate.functionLiteral2Predicate + + val first = SimpleMessage("first", 1) + val second = SimpleMessage("second", 2) + + val files = List( + ("sm/first.sm", writeMessage(SimpleMessageType, first)), + ("sm/second.sm", writeMessage(SimpleMessageType, second)) + ) + commit(files) + + val db = new JGitDatabase(repository) + + db.modify { snapshot => + val sms = snapshot.query(SimpleMessageFileType, (sm: SimpleMessage) => sm.name == "second") + sms.foldLeft(DatabaseModification.empty) { + case (m, (h, sm)) => m.modify(h, SimpleMessage(sm.name, sm.value+1)) + } + } + + val foundFiles = db.latestSnapshot.all(SimpleMessageFileType).toList + + val expected = List(first, SimpleMessage(second.name, second.value+1)) + assertEquals(expected, foundFiles.map(_.userObject)) + } + + @Test + def deleteSimpleMessage { + import query.Predicate.functionLiteral2Predicate + + val first = SimpleMessage("first", 1) + val second = SimpleMessage("second", 2) + + val files = List( + ("sm/first.sm", writeMessage(SimpleMessageType, first)), + ("sm/second.sm", writeMessage(SimpleMessageType, second)) + ) + commit(files) + + val db = new JGitDatabase(repository) + + db.modify { snapshot => + val sms = snapshot.query(SimpleMessageFileType, (sm: SimpleMessage) => sm.name == "second") + sms.foldLeft(DatabaseModification.empty) { + case (m, (h, sm)) => m.remove(h) + } + } + + val foundFiles = db.latestSnapshot.all(SimpleMessageFileType).toList + + val expected = List(first) + assertEquals(expected, foundFiles.map(_.userObject)) + } + private def commit(files: List[(String, ObjectId)]) { val treeId = addFiles(files) val commitId = createCommit("Test", treeId) diff --git a/src/test/scala/com/google/gimd/jgit/JGitFileTestCase.scala b/src/test/scala/com/google/gimd/jgit/JGitFileTestCase.scala index 4afd003..ec1a7ee 100644 --- a/src/test/scala/com/google/gimd/jgit/JGitFileTestCase.scala +++ b/src/test/scala/com/google/gimd/jgit/JGitFileTestCase.scala @@ -14,7 +14,6 @@ package com.google.gimd.jgit - import file.FileType import junit.framework.Assert._ import org.junit.Test @@ -61,7 +60,7 @@ class JGitFileTestCase extends AbstractJGitTestCase { assertEquals(expected, jGitFile.userObject) } - @Test{val expected = classOf[JGitProviderException]} + @Test{val expected = classOf[JGitDatabaseException]} def testMessageOfNonExistingObject { val jGitFile = new JGitFile("test", ObjectId.zeroId, SimpleMessageFileType, repository) jGitFile.message |