Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGC.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGC.java (date 1440488475000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGC.java (date 1440490852000) @@ -21,16 +21,28 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.System.nanoTime; +import static org.apache.jackrabbit.oak.commons.IOUtils.humanReadableByteCount; import static org.apache.jackrabbit.oak.management.ManagementOperation.Status.formatTime; import static org.apache.jackrabbit.oak.management.ManagementOperation.done; import static org.apache.jackrabbit.oak.management.ManagementOperation.newManagementOperation; +import java.util.Date; +import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import javax.annotation.Nonnull; import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; +import javax.management.openmbean.TabularData; +import javax.management.openmbean.TabularDataSupport; +import javax.management.openmbean.TabularType; +import org.apache.jackrabbit.oak.commons.jmx.AnnotatedStandardMBean; import org.apache.jackrabbit.oak.management.ManagementOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +50,7 @@ /** * Default implementation of {@link BlobGCMBean} based on a {@link BlobGarbageCollector}. */ -public class BlobGC implements BlobGCMBean { +public class BlobGC extends AnnotatedStandardMBean implements BlobGCMBean { private static final Logger log = LoggerFactory.getLogger(BlobGC.class); public static final String OP_NAME = "Blob garbage collection"; @@ -55,6 +67,7 @@ public BlobGC( @Nonnull BlobGarbageCollector blobGarbageCollector, @Nonnull Executor executor) { + super(BlobGCMBean.class); this.blobGarbageCollector = checkNotNull(blobGarbageCollector); this.executor = checkNotNull(executor); } @@ -80,5 +93,74 @@ @Override public CompositeData getBlobGCStatus() { return gcOp.getStatus().toCompositeData(); + } + + @Override + public TabularData getGlobalMarkStats() { + TabularDataSupport tds; + try { + TabularType tt = new TabularType(BlobGC.class.getName(), + "Garbage collection global mark phase Stats", + TYPE, + new String[] {"repositoryId"}); + tds = new TabularDataSupport(tt); + List stats = blobGarbageCollector.getStats(); + for (GarbageCollectionRepoStats stat : stats) { + tds.put(toCompositeData(stat)); + } + } catch (Exception e) { + throw new IllegalStateException(e); + } + return tds; + } + + private CompositeDataSupport toCompositeData(GarbageCollectionRepoStats statObj) throws OpenDataException { + Object[] values = new Object[] { + statObj.getRepositoryId(), + (statObj.getLastModified() == 0 ? "" : (new Date(statObj.getLastModified()))).toString(), + statObj.getLength(), + humanReadableByteCount(statObj.getLength()), + statObj.getNumLines() + }; + return new CompositeDataSupport(TYPE, FIELD_NAMES, values); + } + + private static final String[] FIELD_NAMES = new String[] { + "repositoryId", + "referencesLastModifiedTime", + "referenceFileSizeBytes", + "referencesFileSize", + "numReferences", + }; + + private static final String[] FIELD_DESCRIPTIONS = new String[] { + "Repository ID", + "Last modified time of references", + "References file size in bytes", + "References file size in human readable format", + "Number of references" + }; + + private static final OpenType[] FIELD_TYPES = new OpenType[] { + SimpleType.STRING, + SimpleType.STRING, + SimpleType.LONG, + SimpleType.STRING, + SimpleType.INTEGER + }; + + private static final CompositeType TYPE = createCompositeType(); + + private static CompositeType createCompositeType() { + try { + return new CompositeType( + GarbageCollectionRepoStats.class.getName(), + "Composite data type for datastore GC statistics", + FIELD_NAMES, + FIELD_DESCRIPTIONS, + FIELD_TYPES); + } catch (OpenDataException e) { + throw new IllegalStateException(e); + } - } + } } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGCMBean.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGCMBean.java (date 1440488475000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGCMBean.java (date 1440490852000) @@ -19,8 +19,12 @@ package org.apache.jackrabbit.oak.plugins.blob; +import org.apache.jackrabbit.oak.commons.jmx.Description; +import org.apache.jackrabbit.oak.commons.jmx.Name; + import javax.annotation.Nonnull; import javax.management.openmbean.CompositeData; +import javax.management.openmbean.TabularData; /** * MBean for starting and monitoring the progress of @@ -37,8 +41,11 @@ * @param markOnly whether to only mark references and not sweep in the mark and sweep operation. * @return the status of the operation right after it was initiated */ - CompositeData startBlobGC(boolean markOnly); - + CompositeData startBlobGC(@Name("markOnly") + @Description("Set to true to only mark references and not sweep in the mark and sweep operation. " + + "This mode is to be used when the underlying BlobStore is shared between multiple " + + "different repositories. For all other cases set it to false to perform full garbage collection") + boolean markOnly); /** * Data store garbage collection status * @@ -47,5 +54,12 @@ */ @Nonnull CompositeData getBlobGCStatus(); + + /** + * Show details of the data Store garbage collection process. + * + * @return List of available repositories and their status + */ + TabularData getGlobalMarkStats(); } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGarbageCollector.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGarbageCollector.java (date 1440488475000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/BlobGarbageCollector.java (date 1440490852000) @@ -16,6 +16,8 @@ */ package org.apache.jackrabbit.oak.plugins.blob; +import java.util.List; + /** * Interface for blob garbage collector */ @@ -29,4 +31,12 @@ * @throws Exception the exception */ void collectGarbage(boolean markOnly) throws Exception; + + /** + * Retuns the list of stats + * + * @return stats + * @throws Exception + */ + List getStats() throws Exception; } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/GarbageCollectionRepoStats.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/GarbageCollectionRepoStats.java (date 1440490852000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/GarbageCollectionRepoStats.java (date 1440490852000) @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.jackrabbit.oak.plugins.blob; + +/** + * Garbage collection stats for the repository. + */ +public class GarbageCollectionRepoStats { + private String repositoryId; + + private long lastModified; + + private long length; + + private int numLines; + + public String getRepositoryId() { + return repositoryId; + } + + public void setRepositoryId(String repositoryId) { + this.repositoryId = repositoryId; + } + + public long getLastModified() { + return lastModified; + } + + public void setLastModified(long lastModified) { + this.lastModified = lastModified; + } + + public long getLength() { + return length; + } + + public void setLength(long length) { + this.length = length; + } + + public void setNumLines(int numLines) { + this.numLines = numLines; + } + + public int getNumLines() { + return numLines; + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/MarkSweepGarbageCollector.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/MarkSweepGarbageCollector.java (date 1440488475000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/blob/MarkSweepGarbageCollector.java (date 1440490852000) @@ -23,9 +23,12 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; import java.sql.Timestamp; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentLinkedQueue; @@ -34,12 +37,14 @@ import java.util.concurrent.atomic.AtomicInteger; import com.google.common.base.Charsets; +import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.StandardSystemProperty; import com.google.common.base.Stopwatch; import com.google.common.collect.AbstractIterator; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.google.common.collect.PeekingIterator; import com.google.common.io.Closeables; import com.google.common.io.Files; @@ -57,6 +62,8 @@ import javax.annotation.Nullable; +import static com.google.common.collect.Lists.newArrayList; + /** * Mark and sweep garbage collector. * @@ -74,8 +81,6 @@ public static final int DEFAULT_BATCH_COUNT = 2048; - public static enum State {NOT_RUNNING, MARKING, SWEEPING} - /** The last modified time before current time of blobs to consider for garbage collection. */ private final long maxLastModifiedInterval; @@ -84,19 +89,15 @@ /** Helper class to mark blob references which **/ private final BlobReferenceRetriever marker; - + - /** The garbage collector file state */ - private final GarbageCollectorFileState fs; - private final Executor executor; /** The batch count. */ private final int batchCount; - private String repoId; + private final String repoId; - /** Flag to indicate the state of the gc **/ - private State state = State.NOT_RUNNING; + private final String root; /** * Creates an instance of MarkSweepGarbageCollector @@ -127,7 +128,7 @@ this.batchCount = batchCount; this.maxLastModifiedInterval = maxLastModifiedInterval; this.repoId = repositoryId; - fs = new GarbageCollectorFileState(root); + this.root = root; } /** @@ -158,14 +159,55 @@ } /** - * Gets the state of the gc process. + * Returns the stats related to GC for all repos - * + * - * @return the state + * @return a list of GarbageCollectionRepoStats objects + * @throws Exception */ - public State getState() { - return state; + @Override + public List getStats() throws Exception { + List stats = newArrayList(); + if (SharedDataStoreUtils.isShared(blobStore)) { + // Get all the references available + List refFiles = + ((SharedDataStore) blobStore).getAllMetadataRecords(SharedStoreRecordType.REFERENCES.getType()); + Map references = Maps.uniqueIndex(refFiles, new Function() { + @Override + public String apply(DataRecord input) { + return SharedStoreRecordType.REFERENCES.getIdFromName(input.getIdentifier().toString()); - } + } + }); - + + // Get all the repositories registered + List repoFiles = + ((SharedDataStore) blobStore).getAllMetadataRecords(SharedStoreRecordType.REPOSITORY.getType()); + + for (DataRecord repoRec : repoFiles) { + String repoId = SharedStoreRecordType.REFERENCES.getIdFromName(repoRec.getIdentifier().toString()); + GarbageCollectionRepoStats stat = new GarbageCollectionRepoStats(); + stat.setRepositoryId(repoId); + if (references.containsKey(repoId)) { + DataRecord refRec = references.get(repoId); + stat.setLastModified(refRec.getLastModified()); + stat.setLength(refRec.getLength()); + + LineNumberReader reader = null; + try { + reader = new LineNumberReader(new InputStreamReader(refRec.getStream())); + while (reader.readLine() != null) { + } + stat.setNumLines(reader.getLineNumber()); + } finally { + Closeables.close(reader, true); + } + } + stats.add(stat); + } + } + return stats; + } + + /** * Mark and sweep. Main entry method for GC. * @@ -174,13 +216,14 @@ */ private void markAndSweep(boolean markOnly) throws Exception { boolean threw = true; + GarbageCollectorFileState fs = new GarbageCollectorFileState(root); try { Stopwatch sw = Stopwatch.createStarted(); LOG.info("Starting Blob garbage collection"); - mark(); + mark(fs); if (!markOnly) { - long deleteCount = sweep(); + long deleteCount = sweep(fs); threw = false; LOG.info("Blob garbage collection completed in {}. Number of blobs deleted [{}]", sw.toString(), @@ -190,19 +233,18 @@ if (!LOG.isTraceEnabled()) { Closeables.close(fs, threw); } - state = State.NOT_RUNNING; } } /** * Mark phase of the GC. + * @param fs the garbage collector file state */ - private void mark() throws IOException, DataStoreException { - state = State.MARKING; + private void mark(GarbageCollectorFileState fs) throws IOException, DataStoreException { LOG.debug("Starting mark phase of the garbage collector"); // Mark all used references - iterateNodeTree(); + iterateNodeTree(fs); // Move the marked references file to the data store meta area if applicable GarbageCollectionType.get(blobStore).addMarked(blobStore, fs, repoId); @@ -213,10 +255,11 @@ /** * Difference phase where the GC candidates are identified. * + * @param fs the garbage collector file state * @throws IOException * Signals that an I/O exception has occurred. */ - private void difference() throws IOException { + private void difference(GarbageCollectorFileState fs) throws IOException { LOG.debug("Starting difference phase of the garbage collector"); FileLineDifferenceIterator iter = new FileLineDifferenceIterator( @@ -226,7 +269,7 @@ BufferedWriter bufferWriter = null; try { bufferWriter = Files.newWriter(fs.getGcCandidates(), Charsets.UTF_8); - List expiredSet = Lists.newArrayList(); + List expiredSet = newArrayList(); int numCandidates = 0; while (iter.hasNext()) { @@ -279,8 +322,9 @@ * * @return the number of blobs deleted * @throws Exception the exception + * @param fs the garbage collector file state */ - private long sweep() throws Exception { + private long sweep(GarbageCollectorFileState fs) throws Exception { long earliestRefAvailTime; // Merge all the blob references available from all the reference files in the data store meta store // Only go ahead if merge succeeded @@ -293,14 +337,13 @@ } // Find all blob references after iterating over the whole repository - (new BlobIdRetriever()).call(); + (new BlobIdRetriever(fs)).call(); // Calculate the references not used - difference(); + difference(fs); long count = 0; long deleted = 0; - state = State.SWEEPING; LOG.debug("Starting sweep phase of the garbage collector"); LOG.debug("Sweeping blobs with modified time > than the configured max deleted time ({}). ", timestampToString(getLastMaxModifiedTime(earliestRefAvailTime))); @@ -309,7 +352,7 @@ LineIterator iterator = FileUtils.lineIterator(fs.getGcCandidates(), Charsets.UTF_8.name()); - List ids = Lists.newArrayList(); + List ids = newArrayList(); while (iterator.hasNext()) { ids.add(iterator.next()); @@ -317,7 +360,7 @@ if (ids.size() >= getBatchCount()) { count += ids.size(); deleted += sweepInternal(ids, exceptionQueue, earliestRefAvailTime); - ids = Lists.newArrayList(); + ids = newArrayList(); } } if (!ids.isEmpty()) { @@ -329,7 +372,7 @@ try { if (!exceptionQueue.isEmpty()) { writer = Files.newWriter(fs.getGarbage(), Charsets.UTF_8); - saveBatchToFile(Lists.newArrayList(exceptionQueue), writer); + saveBatchToFile(newArrayList(exceptionQueue), writer); } } finally { LineIterator.closeQuietly(iterator); @@ -400,8 +443,9 @@ /** * Iterates the complete node tree and collect all blob references + * @param fs the garbage collector file state */ - private void iterateNodeTree() throws IOException { + private void iterateNodeTree(GarbageCollectorFileState fs) throws IOException { final BufferedWriter writer = Files.newWriter(fs.getMarkedRefs(), Charsets.UTF_8); final AtomicInteger count = new AtomicInteger(); try { @@ -458,6 +502,12 @@ * BlobIdRetriever class to retrieve all blob ids. */ private class BlobIdRetriever implements Callable { + private final GarbageCollectorFileState fs; + + public BlobIdRetriever(GarbageCollectorFileState fs) { + this.fs = fs; + } + @Override public Integer call() throws Exception { LOG.debug("Starting retrieve of all blobs"); @@ -467,7 +517,7 @@ bufferWriter = new BufferedWriter( new FileWriter(fs.getAvailableRefs())); Iterator idsIter = blobStore.getAllChunkIds(0); - List ids = Lists.newArrayList(); + List ids = newArrayList(); while (idsIter.hasNext()) { ids.add(idsIter.next()); @@ -614,7 +664,7 @@ SharedDataStoreUtils.refsNotAvailableFromRepos(repoFiles, refFiles); if (unAvailRepos.isEmpty()) { // List of files to be merged - List files = Lists.newArrayList(); + List files = newArrayList(); for (DataRecord refFile : refFiles) { File file = GarbageCollectorFileState.copy(refFile.getStream()); files.add(file); Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java (date 1440488475000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentNodeStoreService.java (date 1440490852000) @@ -591,14 +591,8 @@ final long blobGcMaxAgeInSecs = toLong(prop(PROP_BLOB_GC_MAX_AGE), DEFAULT_BLOB_GC_MAX_AGE); if (store.getBlobStore() instanceof GarbageCollectableBlobStore) { - BlobGarbageCollector gc = new BlobGarbageCollector() { - @Override - public void collectGarbage(boolean sweep) throws Exception { - store.createBlobGarbageCollector(blobGcMaxAgeInSecs, - ClusterRepositoryInfo.getId(mk.getNodeStore())) - .collectGarbage(sweep); - } - }; + BlobGarbageCollector gc = store.createBlobGarbageCollector(blobGcMaxAgeInSecs, + ClusterRepositoryInfo.getId(mk.getNodeStore())); registrations.add(registerMBean(whiteboard, BlobGCMBean.class, new BlobGC(gc, executor), BlobGCMBean.TYPE, "Document node store blob garbage collection")); } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java (date 1440488475000) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/SegmentNodeStoreService.java (date 1440490852000) @@ -450,17 +450,11 @@ } if (store.getBlobStore() instanceof GarbageCollectableBlobStore) { - BlobGarbageCollector gc = new BlobGarbageCollector() { - @Override - public void collectGarbage(boolean sweep) throws Exception { - MarkSweepGarbageCollector gc = new MarkSweepGarbageCollector( + BlobGarbageCollector gc = new MarkSweepGarbageCollector( - new SegmentBlobReferenceRetriever(store.getTracker()), - (GarbageCollectableBlobStore) store.getBlobStore(), - executor, TimeUnit.SECONDS.toMillis(blobGcMaxAgeInSecs), - ClusterRepositoryInfo.getId(delegate)); + new SegmentBlobReferenceRetriever(store.getTracker()), + (GarbageCollectableBlobStore) store.getBlobStore(), + executor, TimeUnit.SECONDS.toMillis(blobGcMaxAgeInSecs), + ClusterRepositoryInfo.getId(delegate)); - gc.collectGarbage(sweep); - } - }; blobGCRegistration = registerMBean(whiteboard, BlobGCMBean.class, new BlobGC(gc, executor), BlobGCMBean.TYPE, "Segment node store blob garbage collection"); Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/SharedBlobStoreGCTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/SharedBlobStoreGCTest.java (date 1440488475000) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/SharedBlobStoreGCTest.java (date 1440490852000) @@ -41,6 +41,7 @@ import org.apache.jackrabbit.core.data.DataStore; import org.apache.jackrabbit.oak.api.Blob; import org.apache.jackrabbit.oak.plugins.blob.BlobGarbageCollector; +import org.apache.jackrabbit.oak.plugins.blob.GarbageCollectionRepoStats; import org.apache.jackrabbit.oak.plugins.blob.MarkSweepGarbageCollector; import org.apache.jackrabbit.oak.plugins.blob.SharedDataStore; import org.apache.jackrabbit.oak.plugins.blob.datastore.DataStoreBlobStore; @@ -134,6 +135,33 @@ } @Test + public void testGCStats() throws Exception { + log.debug("Running testGCStats()"); + // Only run the mark phase on both the clusters to get the stats + cluster1.gc.collectGarbage(true); + cluster2.gc.collectGarbage(true); + + Set actualRepoIds = Sets.newHashSet(); + actualRepoIds.add(cluster1.repoId); + actualRepoIds.add(cluster2.repoId); + + Set actualNumBlobs = Sets.newHashSet(); + actualNumBlobs.add(cluster1.initBlobs.size()); + actualNumBlobs.add(cluster2.initBlobs.size()); + + List statsList = cluster1.gc.getStats(); + Set observedNumBlobs = Sets.newHashSet(); + Set observedRepoIds = Sets.newHashSet(); + for (GarbageCollectionRepoStats stat : statsList) { + observedNumBlobs.add(stat.getNumLines()); + observedRepoIds.add(stat.getRepositoryId()); + } + + Assert.assertTrue(Sets.difference(actualNumBlobs, observedNumBlobs).isEmpty()); + Assert.assertTrue(Sets.difference(actualRepoIds, observedRepoIds).isEmpty()); + } + + @Test // GC should fail public void testOnly1ClusterMark() throws Exception { log.debug("Running testOnly1ClusterMark()"); @@ -180,7 +208,7 @@ private int seed; private BlobGarbageCollector gc; private Date startDate; - + private String repoId; private Set initBlobs = new HashSet(); protected Set getInitBlobs() { @@ -190,19 +218,14 @@ public Cluster(final DocumentNodeStore ds, final String repoId, int seed) throws IOException { this.ds = ds; - this.gc = new BlobGarbageCollector() { - @Override - public void collectGarbage(boolean markOnly) throws Exception { - MarkSweepGarbageCollector gc = new MarkSweepGarbageCollector( + this.gc = new MarkSweepGarbageCollector( new DocumentBlobReferenceRetriever(ds), (GarbageCollectableBlobStore) ds.getBlobStore(), MoreExecutors.sameThreadExecutor(), "./target", 5, 0, repoId); - gc.collectGarbage(markOnly); - } - }; this.startDate = new Date(); this.seed = seed; + this.repoId = repoId; } /**