Version Control Progress implementation

This commit is contained in:
Andrii Shvaika 2022-06-20 17:06:32 +03:00
parent bdd8432049
commit b08b39fb08
14 changed files with 309 additions and 105 deletions

View File

@ -49,6 +49,7 @@ public class ControllerConstants {
protected static final String RULE_NODE_ID_PARAM_DESCRIPTION = "A string value representing the rule node id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RULE_NODE_ID_PARAM_DESCRIPTION = "A string value representing the rule node id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String WIDGET_BUNDLE_ID_PARAM_DESCRIPTION = "A string value representing the widget bundle id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String WIDGET_BUNDLE_ID_PARAM_DESCRIPTION = "A string value representing the widget bundle id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String WIDGET_TYPE_ID_PARAM_DESCRIPTION = "A string value representing the widget type id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String WIDGET_TYPE_ID_PARAM_DESCRIPTION = "A string value representing the widget type id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String VC_REQUEST_ID_PARAM_DESCRIPTION = "A string value representing the version control request id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RESOURCE_ID_PARAM_DESCRIPTION = "A string value representing the resource id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String RESOURCE_ID_PARAM_DESCRIPTION = "A string value representing the resource id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority."; protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority.";
protected static final String SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority."; protected static final String SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority.";

View File

@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.sync.vc.EntityDataDiff;
import org.thingsboard.server.common.data.sync.vc.EntityDataInfo; import org.thingsboard.server.common.data.sync.vc.EntityDataInfo;
import org.thingsboard.server.common.data.sync.vc.EntityVersion; import org.thingsboard.server.common.data.sync.vc.EntityVersion;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult; import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.EntityTypeLoadResult;
import org.thingsboard.server.common.data.sync.vc.VersionLoadResult; import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo; import org.thingsboard.server.common.data.sync.vc.VersionedEntityInfo;
import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest; import org.thingsboard.server.common.data.sync.vc.request.create.VersionCreateRequest;
@ -65,6 +64,7 @@ import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VERSI
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.VC_REQUEST_ID_PARAM_DESCRIPTION;
@RestController @RestController
@TbCoreComponent @TbCoreComponent
@ -117,14 +117,19 @@ public class EntitiesVersionControlController extends BaseController {
" }\n" + " }\n" +
"}\n```") "}\n```")
@PostMapping("/version") @PostMapping("/version")
public DeferredResult<VersionCreationResult> saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws ThingsboardException { public DeferredResult<UUID> saveEntitiesVersion(@RequestBody VersionCreateRequest request) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.WRITE); return wrapFuture(versionControlService.saveEntitiesVersion(user, request));
return wrapFuture(versionControlService.saveEntitiesVersion(user, request)); }
} catch (Exception e) {
throw handleException(e); @ApiOperation(value = "", notes = "")
} @GetMapping(value = "/version/{requestId}/status")
public VersionCreationResult getVersionCreateRequestStatus(@ApiParam(value = VC_REQUEST_ID_PARAM_DESCRIPTION, required = true)
@PathVariable UUID requestId
) throws Exception {
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
return versionControlService.getVersionCreateStatus(getCurrentUser(), requestId);
} }
@ApiOperation(value = "", notes = "" + @ApiOperation(value = "", notes = "" +
@ -147,15 +152,11 @@ public class EntitiesVersionControlController extends BaseController {
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws Exception {
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
EntityId externalEntityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink));
return wrapFuture(versionControlService.listEntityVersions(getTenantId(), branch, externalEntityId, pageLink));
} catch (Exception e) {
throw handleException(e);
}
} }
@ApiOperation(value = "", notes = "" + @ApiOperation(value = "", notes = "" +
@ -177,14 +178,10 @@ public class EntitiesVersionControlController extends BaseController {
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws Exception {
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink));
return wrapFuture(versionControlService.listEntityTypeVersions(getTenantId(), branch, entityType, pageLink));
} catch (Exception e) {
throw handleException(e);
}
} }
@ApiOperation(value = "", notes = "" + @ApiOperation(value = "", notes = "" +
@ -213,65 +210,45 @@ public class EntitiesVersionControlController extends BaseController {
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp") @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = "timestamp")
@RequestParam(required = false) String sortProperty, @RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException { @RequestParam(required = false) String sortOrder) throws Exception {
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink));
return wrapFuture(versionControlService.listVersions(getTenantId(), branch, pageLink));
} catch (Exception e) {
throw handleException(e);
}
} }
@GetMapping("/entity/{branch}/{entityType}/{versionId}") @GetMapping("/entity/{branch}/{entityType}/{versionId}")
public DeferredResult<List<VersionedEntityInfo>> listEntitiesAtVersion(@PathVariable String branch, public DeferredResult<List<VersionedEntityInfo>> listEntitiesAtVersion(@PathVariable String branch,
@PathVariable EntityType entityType, @PathVariable EntityType entityType,
@PathVariable String versionId) throws ThingsboardException { @PathVariable String versionId) throws Exception {
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType));
return wrapFuture(versionControlService.listEntitiesAtVersion(getTenantId(), branch, versionId, entityType));
} catch (Exception e) {
throw handleException(e);
}
} }
@GetMapping("/entity/{branch}/{versionId}") @GetMapping("/entity/{branch}/{versionId}")
public DeferredResult<List<VersionedEntityInfo>> listAllEntitiesAtVersion(@PathVariable String branch, public DeferredResult<List<VersionedEntityInfo>> listAllEntitiesAtVersion(@PathVariable String branch,
@PathVariable String versionId) throws ThingsboardException { @PathVariable String versionId) throws Exception {
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId));
return wrapFuture(versionControlService.listAllEntitiesAtVersion(getTenantId(), branch, versionId));
} catch (Exception e) {
throw handleException(e);
}
} }
@GetMapping("/info/{versionId}/{entityType}/{externalEntityUuid}") @GetMapping("/info/{versionId}/{entityType}/{externalEntityUuid}")
public DeferredResult<EntityDataInfo> getEntityDataInfo(@PathVariable String versionId, public DeferredResult<EntityDataInfo> getEntityDataInfo(@PathVariable String versionId,
@PathVariable EntityType entityType, @PathVariable EntityType entityType,
@PathVariable UUID externalEntityUuid) throws ThingsboardException { @PathVariable UUID externalEntityUuid) throws Exception {
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, externalEntityUuid); return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId));
return wrapFuture(versionControlService.getEntityDataInfo(getCurrentUser(), entityId, versionId));
} catch (Exception e) {
throw handleException(e);
}
} }
@GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}") @GetMapping("/diff/{branch}/{entityType}/{internalEntityUuid}")
public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@PathVariable String branch, public DeferredResult<EntityDataDiff> compareEntityDataToVersion(@PathVariable String branch,
@PathVariable EntityType entityType, @PathVariable EntityType entityType,
@PathVariable UUID internalEntityUuid, @PathVariable UUID internalEntityUuid,
@RequestParam String versionId) throws ThingsboardException { @RequestParam String versionId) throws Exception {
try { accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ);
accessControlService.checkPermission(getCurrentUser(), Resource.VERSION_CONTROL, Operation.READ); EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid);
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, internalEntityUuid); return wrapFuture(versionControlService.compareEntityDataToVersion(getCurrentUser(), branch, entityId, versionId));
return wrapFuture(versionControlService.compareEntityDataToVersion(getCurrentUser(), branch, entityId, versionId));
} catch (Exception e) {
throw handleException(e);
}
} }
@ApiOperation(value = "", notes = "" + @ApiOperation(value = "", notes = "" +
@ -307,14 +284,10 @@ public class EntitiesVersionControlController extends BaseController {
" }\n" + " }\n" +
"}\n```") "}\n```")
@PostMapping("/entity") @PostMapping("/entity")
public DeferredResult<VersionLoadResult> loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws ThingsboardException { public UUID loadEntitiesVersion(@RequestBody VersionLoadRequest request) throws Exception {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
try { accessControlService.checkPermission(user, Resource.VERSION_CONTROL, Operation.WRITE);
accessControlService.checkPermission(user, Resource.VERSION_CONTROL, Operation.READ); return versionControlService.loadEntitiesVersion(user, request);
return wrapFuture(versionControlService.loadEntitiesVersion(user, request));
} catch (Exception e) {
throw handleException(e);
}
} }

View File

@ -25,9 +25,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.common.util.TbStopWatch;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -73,6 +77,7 @@ import org.thingsboard.server.service.sync.ie.EntitiesExportImportService;
import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService; import org.thingsboard.server.service.sync.ie.exporting.ExportableEntitiesService;
import org.thingsboard.server.service.sync.ie.importing.impl.MissingEntityException; import org.thingsboard.server.service.sync.ie.importing.impl.MissingEntityException;
import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService; import org.thingsboard.server.service.sync.vc.autocommit.TbAutoCommitSettingsService;
import org.thingsboard.server.service.sync.vc.data.CommitGitRequest;
import org.thingsboard.server.service.sync.vc.data.ComplexEntitiesExportCtx; import org.thingsboard.server.service.sync.vc.data.ComplexEntitiesExportCtx;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx; import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@ -110,6 +115,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
private final ExportableEntitiesService exportableEntitiesService; private final ExportableEntitiesService exportableEntitiesService;
private final TbNotificationEntityService entityNotificationService; private final TbNotificationEntityService entityNotificationService;
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
private final TbTransactionalCache<UUID, VersionControlTaskCacheEntry> taskCache;
private ListeningExecutorService executor; private ListeningExecutorService executor;
@ -130,23 +136,55 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@SuppressWarnings("UnstableApiUsage") @SuppressWarnings("UnstableApiUsage")
@Override @Override
public ListenableFuture<VersionCreationResult> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception { public ListenableFuture<UUID> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception {
var pendingCommit = gitServiceQueue.prepareCommit(user, request); var pendingCommit = gitServiceQueue.prepareCommit(user, request);
DonAsynchron.withCallback(pendingCommit, commit -> {
return transformAsync(pendingCommit, commit -> { cachePut(commit.getTxId(), new VersionCreationResult());
List<ListenableFuture<Void>> gitFutures = new ArrayList<>(); try {
switch (request.getType()) { List<ListenableFuture<Void>> gitFutures = new ArrayList<>();
case SINGLE_ENTITY: { switch (request.getType()) {
handleSingleEntityRequest(new SimpleEntitiesExportCtx(user, commit, (SingleEntityVersionCreateRequest) request)); case SINGLE_ENTITY: {
break; handleSingleEntityRequest(new SimpleEntitiesExportCtx(user, commit, (SingleEntityVersionCreateRequest) request));
} break;
case COMPLEX: { }
handleComplexRequest(new ComplexEntitiesExportCtx(user, commit, (ComplexVersionCreateRequest) request)); case COMPLEX: {
break; handleComplexRequest(new ComplexEntitiesExportCtx(user, commit, (ComplexVersionCreateRequest) request));
break;
}
} }
var resultFuture = Futures.transformAsync(Futures.allAsList(gitFutures), f -> gitServiceQueue.push(commit), executor);
DonAsynchron.withCallback(resultFuture, result -> cachePut(commit.getTxId(), result), e -> processCommitError(user, request, commit, e), executor);
} catch (Exception e) {
processCommitError(user, request, commit, e);
} }
return transformAsync(Futures.allAsList(gitFutures), success -> gitServiceQueue.push(commit), executor); }, t -> log.debug("[{}] Failed to prepare the commit: {}", user.getId(), request, t));
}, executor);
return transform(pendingCommit, CommitGitRequest::getTxId, MoreExecutors.directExecutor());
}
@Override
public VersionCreationResult getVersionCreateStatus(SecurityUser user, UUID requestId) throws ThingsboardException {
return getStatus(user, requestId, VersionControlTaskCacheEntry::getExportResult);
}
@Override
public VersionLoadResult getVersionLoadStatus(SecurityUser user, UUID requestId) throws ThingsboardException {
return getStatus(user, requestId, VersionControlTaskCacheEntry::getImportResult);
}
private <T> T getStatus(SecurityUser user, UUID requestId, Function<VersionControlTaskCacheEntry, T> getter) throws ThingsboardException {
var cacheEntry = taskCache.get(requestId);
if (cacheEntry == null || cacheEntry.get() == null) {
throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND);
} else {
var entry = cacheEntry.get();
var result = getter.apply(entry);
if (result == null) {
throw new ThingsboardException(ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else {
return result;
}
}
} }
private void handleSingleEntityRequest(SimpleEntitiesExportCtx ctx) throws Exception { private void handleSingleEntityRequest(SimpleEntitiesExportCtx ctx) throws Exception {
@ -214,26 +252,31 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@SuppressWarnings({"UnstableApiUsage", "rawtypes"}) @SuppressWarnings({"UnstableApiUsage", "rawtypes"})
@Override @Override
public ListenableFuture<VersionLoadResult> loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception { public UUID loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception {
EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId());
switch (request.getType()) { switch (request.getType()) {
case SINGLE_ENTITY: { case SINGLE_ENTITY: {
SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request; SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request;
VersionLoadConfig config = versionLoadRequest.getConfig(); VersionLoadConfig config = versionLoadRequest.getConfig();
ListenableFuture<EntityExportData> future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId()); ListenableFuture<EntityExportData> future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId());
return Futures.transform(future, entityData -> doInTemplate(user, request, ctx -> loadSingleEntity(ctx, config, entityData)), executor); DonAsynchron.withCallback(future, entityData -> doInTemplate(ctx, request, c -> loadSingleEntity(c, config, entityData)),
e -> processLoadError(ctx, e), executor);
break;
} }
case ENTITY_TYPE: { case ENTITY_TYPE: {
EntityTypeVersionLoadRequest versionLoadRequest = (EntityTypeVersionLoadRequest) request; EntityTypeVersionLoadRequest versionLoadRequest = (EntityTypeVersionLoadRequest) request;
return executor.submit(() -> doInTemplate(user, request, ctx -> loadMultipleEntities(ctx, versionLoadRequest))); executor.submit(() -> doInTemplate(ctx, request, c -> loadMultipleEntities(c, versionLoadRequest)));
break;
} }
default: default:
throw new IllegalArgumentException("Unsupported version load request"); throw new IllegalArgumentException("Unsupported version load request");
} }
return ctx.getRequestId();
} }
private <R> VersionLoadResult doInTemplate(SecurityUser user, VersionLoadRequest request, Function<EntitiesImportCtx, VersionLoadResult> function) { private <R> VersionLoadResult doInTemplate(EntitiesImportCtx ctx, VersionLoadRequest request, Function<EntitiesImportCtx, VersionLoadResult> function) {
try { try {
EntitiesImportCtx ctx = new EntitiesImportCtx(user, request.getVersionId());
VersionLoadResult result = transactionTemplate.execute(status -> function.apply(ctx)); VersionLoadResult result = transactionTemplate.execute(status -> function.apply(ctx));
try { try {
for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) { for (ThrowingRunnable throwingRunnable : ctx.getEventCallbacks()) {
@ -246,7 +289,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} catch (LoadEntityException e) { } catch (LoadEntityException e) {
return onError(e.getData(), e.getCause()); return onError(e.getData(), e.getCause());
} catch (Exception e) { } catch (Exception e) {
log.info("[{}] Failed to process request [{}] due to: ", user.getTenantId(), request, e); log.info("[{}] Failed to process request [{}] due to: ", ctx.getTenantId(), request, e);
throw e; throw e;
} }
} }
@ -477,7 +520,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
@Override @Override
public ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception { public ListenableFuture<UUID> autoCommit(SecurityUser user, EntityId entityId) throws Exception {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null) { if (repositorySettings == null) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
@ -504,7 +547,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
@Override @Override
public ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityType entityType, List<UUID> entityIds) throws Exception { public ListenableFuture<UUID> autoCommit(SecurityUser user, EntityType entityType, List<UUID> entityIds) throws Exception {
var repositorySettings = repositorySettingsService.get(user.getTenantId()); var repositorySettings = repositorySettingsService.get(user.getTenantId());
if (repositorySettings == null) { if (repositorySettings == null) {
return Futures.immediateFuture(null); return Futures.immediateFuture(null);
@ -550,5 +593,21 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
} }
} }
private void processCommitError(SecurityUser user, VersionCreateRequest request, CommitGitRequest commit, Throwable e) {
log.debug("[{}] Failed to prepare the commit: {}", user.getId(), request, e);
cachePut(commit.getTxId(), new VersionCreationResult(e.getMessage()));
}
private void processLoadError(EntitiesImportCtx ctx, Throwable e) {
log.debug("[{}] Failed to load the commit: {}", ctx.getRequestId(), ctx.getVersionId(), e);
cachePut(ctx.getRequestId(), VersionLoadResult.error(EntityLoadError.runtimeError(e.getMessage())));
}
private void cachePut(UUID requestId, VersionCreationResult result) {
taskCache.put(requestId, VersionControlTaskCacheEntry.newForExport(result));
}
private void cachePut(UUID requestId, VersionLoadResult result) {
taskCache.put(requestId, VersionControlTaskCacheEntry.newForImport(result));
}
} }

View File

@ -16,7 +16,9 @@
package org.thingsboard.server.service.sync.vc; package org.thingsboard.server.service.sync.vc;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
@ -38,7 +40,9 @@ import java.util.UUID;
public interface EntitiesVersionControlService { public interface EntitiesVersionControlService {
ListenableFuture<VersionCreationResult> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception; ListenableFuture<UUID> saveEntitiesVersion(SecurityUser user, VersionCreateRequest request) throws Exception;
VersionCreationResult getVersionCreateStatus(SecurityUser user, UUID requestId) throws ThingsboardException;
ListenableFuture<PageData<EntityVersion>> listEntityVersions(TenantId tenantId, String branch, EntityId externalId, PageLink pageLink) throws Exception; ListenableFuture<PageData<EntityVersion>> listEntityVersions(TenantId tenantId, String branch, EntityId externalId, PageLink pageLink) throws Exception;
@ -50,7 +54,9 @@ public interface EntitiesVersionControlService {
ListenableFuture<List<VersionedEntityInfo>> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception; ListenableFuture<List<VersionedEntityInfo>> listAllEntitiesAtVersion(TenantId tenantId, String branch, String versionId) throws Exception;
ListenableFuture<VersionLoadResult> loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception; UUID loadEntitiesVersion(SecurityUser user, VersionLoadRequest request) throws Exception;
VersionLoadResult getVersionLoadStatus(SecurityUser user, UUID requestId) throws ThingsboardException;
ListenableFuture<EntityDataDiff> compareEntityDataToVersion(SecurityUser user, String branch, EntityId entityId, String versionId) throws Exception; ListenableFuture<EntityDataDiff> compareEntityDataToVersion(SecurityUser user, String branch, EntityId entityId, String versionId) throws Exception;
@ -64,9 +70,10 @@ public interface EntitiesVersionControlService {
ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception; ListenableFuture<Void> checkVersionControlAccess(TenantId tenantId, RepositorySettings settings) throws Exception;
ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityId entityId) throws Exception; ListenableFuture<UUID> autoCommit(SecurityUser user, EntityId entityId) throws Exception;
ListenableFuture<VersionCreationResult> autoCommit(SecurityUser user, EntityType entityType, List<UUID> entityIds) throws Exception; ListenableFuture<UUID> autoCommit(SecurityUser user, EntityType entityType, List<UUID> entityIds) throws Exception;
ListenableFuture<EntityDataInfo> getEntityDataInfo(SecurityUser user, EntityId entityId, String versionId); ListenableFuture<EntityDataInfo> getEntityDataInfo(SecurityUser user, EntityId entityId, String versionId);
} }

View File

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.sync.vc;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.sync.vc.VersionCreationResult;
import org.thingsboard.server.common.data.sync.vc.VersionLoadResult;
import java.io.Serializable;
@Data
@AllArgsConstructor
public class VersionControlTaskCacheEntry implements Serializable {
private static final long serialVersionUID = -7875992200801588119L;
private VersionCreationResult exportResult;
private VersionLoadResult importResult;
public static VersionControlTaskCacheEntry newForExport(VersionCreationResult result) {
return new VersionControlTaskCacheEntry(result, null);
}
public static VersionControlTaskCacheEntry newForImport(VersionLoadResult result) {
return new VersionControlTaskCacheEntry(null, result);
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.sync.vc;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CaffeineTbTransactionalCache;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.UUID;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@Service("VersionControlTaskCache")
public class VersionControlTaskCaffeineCache extends CaffeineTbTransactionalCache<UUID, VersionControlTaskCacheEntry> {
public VersionControlTaskCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.VERSION_CONTROL_TASK_CACHE);
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.sync.vc;
import com.google.protobuf.InvalidProtocolBufferException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Service;
import org.thingsboard.server.cache.CacheSpecsMap;
import org.thingsboard.server.cache.RedisTbTransactionalCache;
import org.thingsboard.server.cache.TBRedisCacheConfiguration;
import org.thingsboard.server.cache.TbRedisSerializer;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.UUID;
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Service("VersionControlTaskCache")
public class VersionControlTaskRedisCache extends RedisTbTransactionalCache<UUID, VersionControlTaskCacheEntry> {
public VersionControlTaskRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.VERSION_CONTROL_TASK_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbRedisSerializer<>());
}
}

View File

@ -35,11 +35,13 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
@Slf4j @Slf4j
@Data @Data
public class EntitiesImportCtx { public class EntitiesImportCtx {
private final UUID requestId;
private final SecurityUser user; private final SecurityUser user;
private final String versionId; private final String versionId;
@ -57,11 +59,12 @@ public class EntitiesImportCtx {
private EntityImportSettings settings; private EntityImportSettings settings;
private EntityImportResult<?> currentImportResult; private EntityImportResult<?> currentImportResult;
public EntitiesImportCtx(SecurityUser user, String versionId) { public EntitiesImportCtx(UUID requestId, SecurityUser user, String versionId) {
this(user, versionId, null); this(requestId, user, versionId, null);
} }
public EntitiesImportCtx(SecurityUser user, String versionId, EntityImportSettings settings) { public EntitiesImportCtx(UUID requestId, SecurityUser user, String versionId, EntityImportSettings settings) {
this.requestId = requestId;
this.user = user; this.user = user;
this.versionId = versionId; this.versionId = versionId;
this.settings = settings; this.settings = settings;

View File

@ -421,6 +421,9 @@ cache:
twoFaVerificationCodes: twoFaVerificationCodes:
timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:60}" timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:60}"
maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}" maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}"
versionControlTask:
timeToLiveInMinutes: "${CACHE_SPECS_VERSION_CONTROL_TASK_TTL:60}"
maxSize: "${CACHE_SPECS_VERSION_CONTROL_TASK_MAX_SIZE:100000}"
redis: redis:
# standalone or cluster # standalone or cluster

View File

@ -34,4 +34,5 @@ public class CacheConstants {
public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings"; public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings";
public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings"; public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings";
public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes";
public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask";
} }

View File

@ -30,6 +30,7 @@ public class EntityLoadError {
private String type; private String type;
private EntityId source; private EntityId source;
private EntityId target; private EntityId target;
private String message;
public static EntityLoadError credentialsError(EntityId sourceId) { public static EntityLoadError credentialsError(EntityId sourceId) {
return EntityLoadError.builder().type("DEVICE_CREDENTIALS_CONFLICT").source(sourceId).build(); return EntityLoadError.builder().type("DEVICE_CREDENTIALS_CONFLICT").source(sourceId).build();
@ -39,4 +40,8 @@ public class EntityLoadError {
return EntityLoadError.builder().type("MISSING_REFERENCED_ENTITY").source(sourceId).target(targetId).build(); return EntityLoadError.builder().type("MISSING_REFERENCED_ENTITY").source(sourceId).target(targetId).build();
} }
public static EntityLoadError runtimeError(String msg) {
return EntityLoadError.builder().type("RUNTIME").message(msg).build();
}
} }

View File

@ -19,10 +19,15 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class EntityVersion { public class EntityVersion implements Serializable {
private static final long serialVersionUID = -3705022663019175258L;
private long timestamp; private long timestamp;
private String id; private String id;
private String name; private String name;

View File

@ -16,11 +16,34 @@
package org.thingsboard.server.common.data.sync.vc; package org.thingsboard.server.common.data.sync.vc;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data @Data
public class VersionCreationResult { @NoArgsConstructor
public class VersionCreationResult implements Serializable {
private static final long serialVersionUID = 8032189124530267838L;
private EntityVersion version; private EntityVersion version;
private int added; private int added;
private int modified; private int modified;
private int removed; private int removed;
private String error;
private boolean done;
public VersionCreationResult(EntityVersion version, int added, int modified, int removed) {
this.version = version;
this.added = added;
this.modified = modified;
this.removed = removed;
this.done = true;
}
public VersionCreationResult(String error) {
this.error = error;
this.done = true;
}
} }

View File

@ -19,26 +19,30 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import java.io.Serializable;
import java.util.List; import java.util.List;
@Data @Data
@Builder @Builder
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
public class VersionLoadResult { public class VersionLoadResult implements Serializable {
private static final long serialVersionUID = -1386093599856747449L;
private List<EntityTypeLoadResult> result; private List<EntityTypeLoadResult> result;
private EntityLoadError error; private EntityLoadError error;
private boolean done;
public static VersionLoadResult success(List<EntityTypeLoadResult> result) { public static VersionLoadResult success(List<EntityTypeLoadResult> result) {
return VersionLoadResult.builder().result(result).build(); return VersionLoadResult.builder().result(result).done(true).build();
} }
public static VersionLoadResult success(EntityTypeLoadResult result) { public static VersionLoadResult success(EntityTypeLoadResult result) {
return VersionLoadResult.builder().result(List.of(result)).build(); return VersionLoadResult.builder().result(List.of(result)).done(true).build();
} }
public static VersionLoadResult error(EntityLoadError error) { public static VersionLoadResult error(EntityLoadError error) {
return VersionLoadResult.builder().error(error).build(); return VersionLoadResult.builder().error(error).done(true).build();
} }
} }