diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-05 16:04:44 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-05 16:04:44 +0100 |
commit | 5209fac8c90c46ba4dd6d9ce73eae7f2dc428a9b (patch) | |
tree | b12cea78009c77471fea3c873afa92ad093ce167 /sonar-core | |
parent | 15efef19303c240d12ec345e774987ba8a075d8d (diff) | |
download | sonarqube-5209fac8c90c46ba4dd6d9ce73eae7f2dc428a9b.tar.gz sonarqube-5209fac8c90c46ba4dd6d9ce73eae7f2dc428a9b.zip |
Improve batch execution of DELETE statements in PurgeDao
Diffstat (limited to 'sonar-core')
8 files changed, 224 insertions, 81 deletions
diff --git a/sonar-core/src/main/java/org/sonar/core/purge/PurgeDao.java b/sonar-core/src/main/java/org/sonar/core/purge/PurgeDao.java index 72322a2e180..3a262e92f19 100644 --- a/sonar-core/src/main/java/org/sonar/core/purge/PurgeDao.java +++ b/sonar-core/src/main/java/org/sonar/core/purge/PurgeDao.java @@ -34,6 +34,9 @@ import org.sonar.core.resource.ResourceDto; import java.util.Collections; import java.util.List; +/** + * @since 2.14 + */ public class PurgeDao { private final MyBatis mybatis; private final ResourceDao resourceDao; @@ -50,10 +53,10 @@ public class PurgeDao { try { List<ResourceDto> projects = getProjects(rootResourceId, session); for (ResourceDto project : projects) { + LOG.info("-> Clean " + project.getLongName() + " [id=" + project.getId() + "]"); deleteAbortedBuilds(project, session, purgeMapper); purge(project, scopesWithoutHistoricalData, session, purgeMapper); } - for (ResourceDto project : projects) { disableOrphanResources(project, session, purgeMapper); } @@ -65,12 +68,13 @@ public class PurgeDao { private void deleteAbortedBuilds(ResourceDto project, SqlSession session, PurgeMapper purgeMapper) { if (hasAbortedBuilds(project.getId(), purgeMapper)) { - LOG.info("<- Deleting aborted builds of " + project.getLongName()); + LOG.info("<- Delete aborted builds"); PurgeSnapshotQuery query = PurgeSnapshotQuery.create() .setIslast(false) .setStatus(new String[]{"U"}) .setRootProjectId(project.getId()); - deleteSnapshots(query, session, purgeMapper); + deleteSnapshots(query, purgeMapper); + session.commit(); } } @@ -87,29 +91,24 @@ public class PurgeDao { PurgeSnapshotQuery.create().setResourceId(project.getId()).setIslast(false).setNotPurged(true) ); for (final Long projectSnapshotId : projectSnapshotIds) { - // TODO log date + LOG.info("<- Clean snapshot " + projectSnapshotId); if (!ArrayUtils.isEmpty(scopesWithoutHistoricalData)) { PurgeSnapshotQuery query = PurgeSnapshotQuery.create() .setIslast(false) .setScopes(scopesWithoutHistoricalData) .setRootSnapshotId(projectSnapshotId); - deleteSnapshots(query, session, purgeMapper); + deleteSnapshots(query, purgeMapper); + session.commit(); } PurgeSnapshotQuery query = PurgeSnapshotQuery.create().setRootSnapshotId(projectSnapshotId).setNotPurged(true); - session.select("org.sonar.core.purge.PurgeMapper.selectSnapshotIds", query, new ResultHandler() { - public void handleResult(ResultContext resultContext) { - Long snapshotId = (Long) resultContext.getResultObject(); - if (snapshotId != null) { - purgeSnapshot(snapshotId, purgeMapper); - } - } - }); + purgeSnapshots(query, purgeMapper); + session.commit(); // must be executed at the end for reentrance - purgeSnapshot(projectSnapshotId, purgeMapper); + purgeSnapshots(PurgeSnapshotQuery.create().setId(projectSnapshotId).setNotPurged(true), purgeMapper); + session.commit(); } - session.commit(); } private void disableOrphanResources(final ResourceDto project, final SqlSession session, final PurgeMapper purgeMapper) { @@ -144,6 +143,7 @@ public class PurgeDao { final PurgeVendorMapper vendorMapper = session.getMapper(PurgeVendorMapper.class); try { deleteProject(rootProjectId, session, mapper, vendorMapper); + session.commit(); return this; } finally { MyBatis.closeQuietly(session); @@ -160,22 +160,15 @@ public class PurgeDao { public void handleResult(ResultContext context) { Long resourceId = (Long) context.getResultObject(); if (resourceId != null) { - deleteResource(resourceId, session, mapper, vendorMapper); + deleteResource(resourceId, mapper, vendorMapper); } } }); - session.commit(); } - void deleteResource(final long resourceId, final SqlSession session, final PurgeMapper mapper, final PurgeVendorMapper vendorMapper) { - session.select("org.sonar.core.purge.PurgeMapper.selectSnapshotIdsByResource", resourceId, new ResultHandler() { - public void handleResult(ResultContext context) { - Long snapshotId = (Long) context.getResultObject(); - if (snapshotId != null) { - deleteSnapshot(snapshotId, mapper); - } - } - }); + void deleteResource(final long resourceId, final PurgeMapper mapper, final PurgeVendorMapper vendorMapper) { + deleteSnapshots(PurgeSnapshotQuery.create().setResourceId(resourceId), mapper); + // possible optimization: filter requests according to resource scope mapper.deleteResourceLinks(resourceId); mapper.deleteResourceProperties(resourceId); @@ -203,7 +196,7 @@ public class PurgeDao { final SqlSession session = mybatis.openBatchSession(); try { final PurgeMapper mapper = session.getMapper(PurgeMapper.class); - deleteSnapshots(query, session, mapper); + deleteSnapshots(mapper.selectSnapshotIds(query), mapper); session.commit(); return this; @@ -212,15 +205,66 @@ public class PurgeDao { } } - private void deleteSnapshots(PurgeSnapshotQuery query, SqlSession session, final PurgeMapper mapper) { - session.select("org.sonar.core.purge.PurgeMapper.selectSnapshotIds", query, new ResultHandler() { - public void handleResult(ResultContext context) { - Long snapshotId = (Long) context.getResultObject(); - if (snapshotId != null) { - deleteSnapshot(snapshotId, mapper); - } - } - }); + @VisibleForTesting + void deleteSnapshots(final PurgeSnapshotQuery query, final PurgeMapper mapper) { + deleteSnapshots(mapper.selectSnapshotIds(query), mapper); + } + + private void deleteSnapshots(final List<Long> snapshotIds, final PurgeMapper mapper) { + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotDependencies(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotDuplications(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotEvents(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotMeasureData(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotMeasures(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotSource(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotViolations(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshot(snapshotId); + } + } + + @VisibleForTesting + void purgeSnapshots(final PurgeSnapshotQuery query, final PurgeMapper mapper) { + purgeSnapshots(mapper.selectSnapshotIds(query), mapper); + } + + private void purgeSnapshots(final List<Long> snapshotIds, final PurgeMapper mapper) { + // note that events are not deleted + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotDependencies(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotDuplications(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotSource(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotViolations(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotWastedMeasures(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.deleteSnapshotMeasuresOnQualityModelRequirements(snapshotId); + } + for (Long snapshotId : snapshotIds) { + mapper.updatePurgeStatusToOne(snapshotId); + } } /** @@ -233,27 +277,4 @@ public class PurgeDao { return projects; } - @VisibleForTesting - void purgeSnapshot(long snapshotId, PurgeMapper mapper) { - // note that events are not deleted - mapper.deleteSnapshotDependencies(snapshotId); - mapper.deleteSnapshotDuplications(snapshotId); - mapper.deleteSnapshotSource(snapshotId); - mapper.deleteSnapshotViolations(snapshotId); - mapper.deleteSnapshotWastedMeasures(snapshotId); - mapper.deleteSnapshotMeasuresOnQualityModelRequirements(snapshotId); - mapper.updatePurgeStatusToOne(snapshotId); - } - - @VisibleForTesting - void deleteSnapshot(long snapshotId, PurgeMapper mapper) { - mapper.deleteSnapshotDependencies(snapshotId); - mapper.deleteSnapshotDuplications(snapshotId); - mapper.deleteSnapshotEvents(snapshotId); - mapper.deleteSnapshotMeasureData(snapshotId); - mapper.deleteSnapshotMeasures(snapshotId); - mapper.deleteSnapshotSource(snapshotId); - mapper.deleteSnapshotViolations(snapshotId); - mapper.deleteSnapshot(snapshotId); - } } diff --git a/sonar-core/src/main/java/org/sonar/core/purge/PurgeSnapshotQuery.java b/sonar-core/src/main/java/org/sonar/core/purge/PurgeSnapshotQuery.java index 7fca0f6773a..c3905abcd67 100644 --- a/sonar-core/src/main/java/org/sonar/core/purge/PurgeSnapshotQuery.java +++ b/sonar-core/src/main/java/org/sonar/core/purge/PurgeSnapshotQuery.java @@ -20,6 +20,7 @@ package org.sonar.core.purge; public final class PurgeSnapshotQuery { + private Long id; private Long rootProjectId; private Long rootSnapshotId; private Long resourceId; @@ -37,6 +38,15 @@ public final class PurgeSnapshotQuery { return new PurgeSnapshotQuery(); } + public Long getId() { + return id; + } + + public PurgeSnapshotQuery setId(Long l) { + this.id = l; + return this; + } + public Long getRootProjectId() { return rootProjectId; } diff --git a/sonar-core/src/main/resources/org/sonar/core/purge/PurgeMapper.xml b/sonar-core/src/main/resources/org/sonar/core/purge/PurgeMapper.xml index aad082e3516..931b6e953f0 100644 --- a/sonar-core/src/main/resources/org/sonar/core/purge/PurgeMapper.xml +++ b/sonar-core/src/main/resources/org/sonar/core/purge/PurgeMapper.xml @@ -13,7 +13,10 @@ and (s.purge_status is null or s.purge_status=0) </if> <if test="rootSnapshotId != null"> - and (s.root_snapshot_id=#{rootSnapshotId} or s.id=#{rootSnapshotId}) + and s.root_snapshot_id=#{rootSnapshotId} + </if> + <if test="id != null"> + and s.id=#{id} </if> <if test="rootProjectId != null"> and s.root_project_id=#{rootProjectId} @@ -73,10 +76,6 @@ select id from projects where root_id=#{id} or id=#{id} </select> - <select id="selectSnapshotIdsByResource" parameterType="long" resultType="long"> - select id from snapshots where project_id=#{id} - </select> - <delete id="deleteSnapshotMeasures" parameterType="long"> delete from project_measures where snapshot_id=#{id} </delete> diff --git a/sonar-core/src/test/java/org/sonar/core/purge/PurgeDaoTest.java b/sonar-core/src/test/java/org/sonar/core/purge/PurgeDaoTest.java index 0c6ea4e6d9a..7cd74d9f511 100644 --- a/sonar-core/src/test/java/org/sonar/core/purge/PurgeDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/purge/PurgeDaoTest.java @@ -54,14 +54,21 @@ public class PurgeDaoTest extends DaoTestCase { SqlSession session = getMyBatis().openSession(); try { // this method does not commit and close the session - dao.deleteSnapshot(5L, session.getMapper(PurgeMapper.class)); + dao.deleteSnapshots(PurgeSnapshotQuery.create().setId(5L), session.getMapper(PurgeMapper.class)); session.commit(); } finally { MyBatis.closeQuietly(session); } checkTables("shouldDeleteSnapshot", - "snapshots", "project_measures", "measure_data", "rule_failures", "snapshot_sources", "duplications_index", "events", "dependencies"); + "snapshots", "project_measures", "measure_data", "rule_failures", "snapshot_sources", "duplications_index", "events", "dependencies"); + } + + @Test + public void shouldDeleteAbortedBuilds() { + setupData("shouldDeleteAbortedBuilds"); + dao.purge(1L, new String[0]); + checkTables("shouldDeleteAbortedBuilds", "snapshots"); } /** @@ -73,15 +80,16 @@ public class PurgeDaoTest extends DaoTestCase { SqlSession session = getMyBatis().openSession(); try { - // this method does not commit and close the session - dao.purgeSnapshot(1L, session.getMapper(PurgeMapper.class)); + dao.purgeSnapshots(PurgeSnapshotQuery.create().setId(1L), session.getMapper(PurgeMapper.class)); + + // the above method does not commit and close the session session.commit(); } finally { MyBatis.closeQuietly(session); } checkTables("shouldPurgeSnapshot", - "snapshots", "project_measures", "measure_data", "rule_failures", "snapshot_sources", "duplications_index", "events", "dependencies", "reviews"); + "snapshots", "project_measures", "measure_data", "rule_failures", "snapshot_sources", "duplications_index", "events", "dependencies", "reviews"); } @Test @@ -90,8 +98,9 @@ public class PurgeDaoTest extends DaoTestCase { SqlSession session = getMyBatis().openSession(); try { - // this method does not commit and close the session - dao.purgeSnapshot(1L, session.getMapper(PurgeMapper.class)); + dao.purgeSnapshots(PurgeSnapshotQuery.create().setId(1L), session.getMapper(PurgeMapper.class)); + +// the above method does not commit and close the session session.commit(); } finally { @@ -106,8 +115,9 @@ public class PurgeDaoTest extends DaoTestCase { SqlSession session = getMyBatis().openSession(); try { - // this method does not commit and close the session dao.disableResource(1L, session.getMapper(PurgeMapper.class)); + + // the above method does not commit and close the session session.commit(); } finally { @@ -161,8 +171,9 @@ public class PurgeDaoTest extends DaoTestCase { setupData("shouldDeleteResource"); SqlSession session = getMyBatis().openSession(); try { - // this method does not commit and close the session - dao.deleteResource(1L, session, session.getMapper(PurgeMapper.class), session.getMapper(PurgeVendorMapper.class)); + dao.deleteResource(1L, session.getMapper(PurgeMapper.class), session.getMapper(PurgeVendorMapper.class)); + + // the above method does not commit and close the session session.commit(); } finally { @@ -196,9 +207,9 @@ public class PurgeDaoTest extends DaoTestCase { public void describeTo(Description description) { description - .appendText("snapshotId").appendValue(snapshotId) - .appendText("isLast").appendValue(isLast) - .appendText("hasEvents").appendValue(hasEvents); + .appendText("snapshotId").appendValue(snapshotId) + .appendText("isLast").appendValue(isLast) + .appendText("hasEvents").appendValue(hasEvents); } } } diff --git a/sonar-core/src/test/java/org/sonar/core/resource/ResourceDaoTest.java b/sonar-core/src/test/java/org/sonar/core/resource/ResourceDaoTest.java index bdda1f21aeb..cb9d765a95b 100644 --- a/sonar-core/src/test/java/org/sonar/core/resource/ResourceDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/resource/ResourceDaoTest.java @@ -27,10 +27,8 @@ import org.sonar.core.persistence.DaoTestCase; import java.util.List; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.core.IsNot.not; -import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; -import static org.junit.matchers.JUnitMatchers.hasItems; public class ResourceDaoTest extends DaoTestCase { @@ -57,5 +55,22 @@ public class ResourceDaoTest extends DaoTestCase { assertThat(dao.getDescendantProjects(33333L).size(), Is.is(0)); } + + @Test + public void getResource() { + setupData("fixture"); + + ResourceDto resource = dao.getResource(1L); + assertThat(resource.getName(), Is.is("Struts")); + assertThat(resource.getLongName(), Is.is("Apache Struts")); + assertThat(resource.getScope(), Is.is("PRJ")); + } + + @Test + public void getResource_not_found() { + setupData("fixture"); + + assertNull(dao.getResource(987654321L)); + } } diff --git a/sonar-core/src/test/resources/org/sonar/core/purge/PurgeDaoTest/shouldDeleteAbortedBuilds-result.xml b/sonar-core/src/test/resources/org/sonar/core/purge/PurgeDaoTest/shouldDeleteAbortedBuilds-result.xml new file mode 100644 index 00000000000..7f21ab01f6e --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/purge/PurgeDaoTest/shouldDeleteAbortedBuilds-result.xml @@ -0,0 +1,46 @@ +<!-- + +Snapshot 2 has been deleted + +--> +<dataset> + + <!-- the project --> + <projects id="1" enabled="[true]" root_id="[null]" + long_name="[null]" scope="PRJ" qualifier="TRK" kee="project" name="project" + description="[null]" language="java" copy_resource_id="[null]" person_id="[null]" profile_id="[null]"/> + + <!-- past snapshot with status "processed" and already purged --> + <snapshots id="1" + project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]" + status="P" islast="[false]" purge_status="1" + period1_mode="[null]" period1_param="[null]" period1_date="[null]" + period2_mode="[null]" period2_param="[null]" period2_date="[null]" + period3_mode="[null]" period3_param="[null]" period3_date="[null]" + period4_mode="[null]" period4_param="[null]" period4_date="[null]" + period5_mode="[null]" period5_param="[null]" period5_date="[null]" + depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00" build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/> + + <!-- snapshot with status "unprocessed" -> to be deleted --> + <!--<snapshots id="2"--> + <!--project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]"--> + <!--status="U" islast="[false]" purge_status="0"--> + <!--period1_mode="[null]" period1_param="[null]" period1_date="[null]"--> + <!--period2_mode="[null]" period2_param="[null]" period2_date="[null]"--> + <!--period3_mode="[null]" period3_param="[null]" period3_date="[null]"--> + <!--period4_mode="[null]" period4_param="[null]" period4_date="[null]"--> + <!--period5_mode="[null]" period5_param="[null]" period5_date="[null]"--> + <!--depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00" build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/>--> + + <!-- snapshot with status "processed" and flagged as "last" -> do not purge and do not delete --> + <snapshots id="3" + project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]" + status="P" islast="[true]" purge_status="0" + period1_mode="[null]" period1_param="[null]" period1_date="[null]" + period2_mode="[null]" period2_param="[null]" period2_date="[null]" + period3_mode="[null]" period3_param="[null]" period3_date="[null]" + period4_mode="[null]" period4_param="[null]" period4_date="[null]" + period5_mode="[null]" period5_param="[null]" period5_date="[null]" + depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00" build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/> + +</dataset>
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/purge/PurgeDaoTest/shouldDeleteAbortedBuilds.xml b/sonar-core/src/test/resources/org/sonar/core/purge/PurgeDaoTest/shouldDeleteAbortedBuilds.xml new file mode 100644 index 00000000000..da6f52837c7 --- /dev/null +++ b/sonar-core/src/test/resources/org/sonar/core/purge/PurgeDaoTest/shouldDeleteAbortedBuilds.xml @@ -0,0 +1,41 @@ +<dataset> + + <!-- the project --> + <projects id="1" enabled="[true]" root_id="[null]" + long_name="[null]" scope="PRJ" qualifier="TRK" kee="project" name="project" + description="[null]" language="java" copy_resource_id="[null]" person_id="[null]" profile_id="[null]"/> + + <!-- past snapshot with status "processed" and already purged --> + <snapshots id="1" + project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]" + status="P" islast="[false]" purge_status="1" + period1_mode="[null]" period1_param="[null]" period1_date="[null]" + period2_mode="[null]" period2_param="[null]" period2_date="[null]" + period3_mode="[null]" period3_param="[null]" period3_date="[null]" + period4_mode="[null]" period4_param="[null]" period4_date="[null]" + period5_mode="[null]" period5_param="[null]" period5_date="[null]" + depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00" build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/> + + <!-- snapshot with status "unprocessed" -> to be deleted --> + <snapshots id="2" + project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]" + status="U" islast="[false]" purge_status="0" + period1_mode="[null]" period1_param="[null]" period1_date="[null]" + period2_mode="[null]" period2_param="[null]" period2_date="[null]" + period3_mode="[null]" period3_param="[null]" period3_date="[null]" + period4_mode="[null]" period4_param="[null]" period4_date="[null]" + period5_mode="[null]" period5_param="[null]" period5_date="[null]" + depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00" build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/> + + <!-- snapshot with status "processed" and flagged as "last" -> do not purge and do not delete --> + <snapshots id="3" + project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]" + status="P" islast="[true]" purge_status="0" + period1_mode="[null]" period1_param="[null]" period1_date="[null]" + period2_mode="[null]" period2_param="[null]" period2_date="[null]" + period3_mode="[null]" period3_param="[null]" period3_date="[null]" + period4_mode="[null]" period4_param="[null]" period4_date="[null]" + period5_mode="[null]" period5_param="[null]" period5_date="[null]" + depth="[null]" scope="PRJ" qualifier="TRK" created_at="2008-12-02 13:58:00.00" build_date="2008-12-02 13:58:00.00" version="[null]" path="[null]"/> + +</dataset>
\ No newline at end of file diff --git a/sonar-core/src/test/resources/org/sonar/core/resource/ResourceDaoTest/fixture.xml b/sonar-core/src/test/resources/org/sonar/core/resource/ResourceDaoTest/fixture.xml index a993fe82a6f..e92a256f553 100644 --- a/sonar-core/src/test/resources/org/sonar/core/resource/ResourceDaoTest/fixture.xml +++ b/sonar-core/src/test/resources/org/sonar/core/resource/ResourceDaoTest/fixture.xml @@ -2,7 +2,7 @@ <!-- root project --> <projects id="1" root_id="[null]" scope="PRJ" qualifier="TRK" kee="org.struts:struts" name="Struts" - description="[null]" long_name="Struts" + description="[null]" long_name="Apache Struts" enabled="[true]" language="java" copy_resource_id="[null]" person_id="[null]"/> <snapshots id="1" project_id="1" parent_snapshot_id="[null]" root_project_id="1" root_snapshot_id="[null]" status="P" islast="[false]" purge_status="[null]" |