mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-3164 file based lock controller; lock project when we try update d… (#1564)
* CB-3164 file based lock controller; lock project when we try update datasources
This commit is contained in:
@@ -27,6 +27,7 @@ Export-Package: io.cloudbeaver,
|
||||
io.cloudbeaver.model.app,
|
||||
io.cloudbeaver.model.rm,
|
||||
io.cloudbeaver.model.rm.local,
|
||||
io.cloudbeaver.model.rm.lock,
|
||||
io.cloudbeaver.model.session,
|
||||
io.cloudbeaver.model.user,
|
||||
io.cloudbeaver.registry,
|
||||
|
||||
+2
-1
@@ -66,7 +66,8 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, @NotNull DBPWorkspace workspace) {
|
||||
public RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider,
|
||||
@NotNull DBPWorkspace workspace) throws DBException {
|
||||
throw new IllegalStateException("Resource controller is not supported by " + getClass().getSimpleName());
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -60,7 +60,8 @@ public interface WebApplication extends DBPApplication {
|
||||
|
||||
DBSSecretController getSecretController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException;
|
||||
|
||||
RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, @NotNull DBPWorkspace workspace);
|
||||
RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider,
|
||||
@NotNull DBPWorkspace workspace) throws DBException;
|
||||
|
||||
String getServerURL();
|
||||
|
||||
|
||||
+71
-50
@@ -20,6 +20,7 @@ import io.cloudbeaver.DBWConstants;
|
||||
import io.cloudbeaver.WebProjectImpl;
|
||||
import io.cloudbeaver.model.app.WebApplication;
|
||||
import io.cloudbeaver.model.rm.RMUtils;
|
||||
import io.cloudbeaver.model.rm.lock.RMFileLockController;
|
||||
import io.cloudbeaver.service.security.SMUtils;
|
||||
import io.cloudbeaver.service.sql.WebSQLConstants;
|
||||
import io.cloudbeaver.utils.WebAppUtils;
|
||||
@@ -34,7 +35,6 @@ import org.jkiss.dbeaver.model.DBPDataSourceContainer;
|
||||
import org.jkiss.dbeaver.model.DBPDataSourceFolder;
|
||||
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
|
||||
import org.jkiss.dbeaver.model.app.DBPProject;
|
||||
import org.jkiss.dbeaver.model.app.DBPResourceTypeDescriptor;
|
||||
import org.jkiss.dbeaver.model.app.DBPWorkspace;
|
||||
import org.jkiss.dbeaver.model.auth.SMCredentials;
|
||||
import org.jkiss.dbeaver.model.auth.SMCredentialsProvider;
|
||||
@@ -82,6 +82,7 @@ public class LocalResourceController implements RMController {
|
||||
private final Path sharedProjectsPath;
|
||||
private final String globalProjectName;
|
||||
private Supplier<SMController> smControllerSupplier;
|
||||
private final RMFileLockController lockController;
|
||||
|
||||
private final Map<String, WebProjectImpl> projectRegistries = new LinkedHashMap<>();
|
||||
|
||||
@@ -92,13 +93,14 @@ public class LocalResourceController implements RMController {
|
||||
Path userProjectsPath,
|
||||
Path sharedProjectsPath,
|
||||
Supplier<SMController> smControllerSupplier
|
||||
) {
|
||||
) throws DBException {
|
||||
this.workspace = workspace;
|
||||
this.credentialsProvider = credentialsProvider;
|
||||
this.rootPath = rootPath;
|
||||
this.userProjectsPath = userProjectsPath;
|
||||
this.sharedProjectsPath = sharedProjectsPath;
|
||||
this.smControllerSupplier = smControllerSupplier;
|
||||
this.lockController = new RMFileLockController(WebAppUtils.getWebApplication());
|
||||
|
||||
this.globalProjectName = DBWorkbench.getPlatform().getApplication().getDefaultProjectName();
|
||||
}
|
||||
@@ -287,16 +289,18 @@ public class LocalResourceController implements RMController {
|
||||
|
||||
@Override
|
||||
public void deleteProject(@NotNull String projectId) throws DBException {
|
||||
RMProject project = makeProjectFromId(projectId, false);
|
||||
Path targetPath = getProjectPath(projectId);
|
||||
if (!Files.exists(targetPath)) {
|
||||
throw new DBException("Project '" + project.getName() + "' doesn't exists");
|
||||
}
|
||||
try {
|
||||
IOUtils.deleteDirectory(targetPath);
|
||||
getSecurityController().deleteAllObjectPermissions(projectId, SMObjects.PROJECT);
|
||||
} catch (IOException e) {
|
||||
throw new DBException("Error deleting project '" + project.getName() + "'", e);
|
||||
try (var projectLock = lockController.lockProject(projectId, "deleteProject")) {
|
||||
RMProject project = makeProjectFromId(projectId, false);
|
||||
Path targetPath = getProjectPath(projectId);
|
||||
if (!Files.exists(targetPath)) {
|
||||
throw new DBException("Project '" + project.getName() + "' doesn't exists");
|
||||
}
|
||||
try {
|
||||
IOUtils.deleteDirectory(targetPath);
|
||||
getSecurityController().deleteAllObjectPermissions(projectId, SMObjects.PROJECT);
|
||||
} catch (IOException e) {
|
||||
throw new DBException("Error deleting project '" + project.getName() + "'", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +351,9 @@ public class LocalResourceController implements RMController {
|
||||
@NotNull String configuration,
|
||||
@Nullable List<String> dataSourceIds
|
||||
) throws DBException {
|
||||
updateProjectDataSources(projectId, configuration, dataSourceIds);
|
||||
try (var projectLock = lockController.lockProject(projectId, "createDatasources")) {
|
||||
updateProjectDataSources(projectId, configuration, dataSourceIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -368,32 +374,38 @@ public class LocalResourceController implements RMController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteProjectDataSources(@NotNull String projectId, @NotNull String[] dataSourceIds) throws DBException {
|
||||
final DBPProject project = getProjectMetadata(projectId, false);
|
||||
final DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
public void deleteProjectDataSources(@NotNull String projectId,
|
||||
@NotNull String[] dataSourceIds) throws DBException {
|
||||
try (var projectLock = lockController.lockProject(projectId, "deleteDatasources")) {
|
||||
final DBPProject project = getProjectMetadata(projectId, false);
|
||||
final DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
|
||||
for (String dataSourceId : dataSourceIds) {
|
||||
final DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId);
|
||||
for (String dataSourceId : dataSourceIds) {
|
||||
final DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId);
|
||||
|
||||
if (dataSource != null) {
|
||||
registry.removeDataSource(dataSource);
|
||||
} else {
|
||||
log.warn("Could not find datasource " + dataSourceId + " for deletion");
|
||||
if (dataSource != null) {
|
||||
registry.removeDataSource(dataSource);
|
||||
} else {
|
||||
log.warn("Could not find datasource " + dataSourceId + " for deletion");
|
||||
}
|
||||
}
|
||||
registry.checkForErrors();
|
||||
}
|
||||
registry.checkForErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createProjectDataSourceFolder(@NotNull String projectId, @NotNull String folderPath) throws DBException {
|
||||
DBPProject project = getProjectMetadata(projectId, false);
|
||||
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
var result = Path.of(folderPath);
|
||||
var newName = result.getFileName().toString();
|
||||
var parent = result.getParent();
|
||||
var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/"));
|
||||
DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName);
|
||||
registry.checkForErrors();
|
||||
public void createProjectDataSourceFolder(@NotNull String projectId,
|
||||
@NotNull String folderPath) throws DBException {
|
||||
try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) {
|
||||
DBPProject project = getProjectMetadata(projectId, false);
|
||||
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
var result = Path.of(folderPath);
|
||||
var newName = result.getFileName().toString();
|
||||
var parent = result.getParent();
|
||||
var parentFolder = parent == null ? null : registry.getFolder(parent.toString().replace("\\", "/"));
|
||||
DBPDataSourceFolder newFolder = registry.addFolder(parentFolder, newName);
|
||||
registry.checkForErrors();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -402,17 +414,19 @@ public class LocalResourceController implements RMController {
|
||||
@NotNull String[] folderPaths,
|
||||
boolean dropContents
|
||||
) throws DBException {
|
||||
DBPProject project = getProjectMetadata(projectId, false);
|
||||
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
for (String folderPath : folderPaths) {
|
||||
DBPDataSourceFolder folder = registry.getFolder(folderPath);
|
||||
if (folder != null) {
|
||||
registry.removeFolder(folder, dropContents);
|
||||
} else {
|
||||
log.warn("Can not find folder by path [" + folderPath + "] for deletion");
|
||||
try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) {
|
||||
DBPProject project = getProjectMetadata(projectId, false);
|
||||
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
for (String folderPath : folderPaths) {
|
||||
DBPDataSourceFolder folder = registry.getFolder(folderPath);
|
||||
if (folder != null) {
|
||||
registry.removeFolder(folder, dropContents);
|
||||
} else {
|
||||
log.warn("Can not find folder by path [" + folderPath + "] for deletion");
|
||||
}
|
||||
}
|
||||
registry.checkForErrors();
|
||||
}
|
||||
registry.checkForErrors();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -421,10 +435,12 @@ public class LocalResourceController implements RMController {
|
||||
@NotNull String oldPath,
|
||||
@NotNull String newPath
|
||||
) throws DBException {
|
||||
DBPProject project = getProjectMetadata(projectId, false);
|
||||
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
registry.moveFolder(oldPath, newPath);
|
||||
registry.checkForErrors();
|
||||
try (var projectLock = lockController.lockProject(projectId, "createDatasourceFolder")) {
|
||||
DBPProject project = getProjectMetadata(projectId, false);
|
||||
DBPDataSourceRegistry registry = project.getDataSourceRegistry();
|
||||
registry.moveFolder(oldPath, newPath);
|
||||
registry.checkForErrors();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -692,9 +708,11 @@ public class LocalResourceController implements RMController {
|
||||
@NotNull String propertyName,
|
||||
@Nullable Object propertyValue
|
||||
) throws DBException {
|
||||
validateResourcePath(resourcePath);
|
||||
getProjectMetadata(projectId, false).setResourceProperty(resourcePath, propertyName, propertyValue);
|
||||
return DEFAULT_CHANGE_ID;
|
||||
try (var projectLock = lockController.lockProject(projectId, "resourcePropertyUpdate")) {
|
||||
validateResourcePath(resourcePath);
|
||||
getProjectMetadata(projectId, false).setResourceProperty(resourcePath, propertyName, propertyValue);
|
||||
return DEFAULT_CHANGE_ID;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateResourcePath(String resourcePath) throws DBException {
|
||||
@@ -923,9 +941,12 @@ public class LocalResourceController implements RMController {
|
||||
);
|
||||
}
|
||||
|
||||
public static Builder builder(SMCredentialsProvider credentialsProvider, DBPWorkspace workspace, Supplier<SMController> smControllerSupplier) {
|
||||
public static Builder builder(SMCredentialsProvider credentialsProvider,
|
||||
DBPWorkspace workspace,
|
||||
Supplier<SMController> smControllerSupplier) {
|
||||
return new Builder(workspace, credentialsProvider, smControllerSupplier);
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final SMCredentialsProvider credentialsProvider;
|
||||
private final Supplier<SMController> smController;
|
||||
@@ -959,7 +980,7 @@ public class LocalResourceController implements RMController {
|
||||
return this;
|
||||
}
|
||||
|
||||
public LocalResourceController build() {
|
||||
public LocalResourceController build() throws DBException {
|
||||
return new LocalResourceController(workspace, credentialsProvider, rootPath, userProjectsPath, sharedProjectsPath, smController);
|
||||
}
|
||||
}
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2023 DBeaver Corp and others
|
||||
*
|
||||
* 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 io.cloudbeaver.model.rm.lock;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import io.cloudbeaver.model.app.WebApplication;
|
||||
import org.jkiss.code.Nullable;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.jkiss.dbeaver.model.app.DBPWorkspace;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* File based resource locks
|
||||
*/
|
||||
public class RMFileLockController {
|
||||
private static final Log log = Log.getLog(RMFileLockController.class);
|
||||
private static final int DEFAULT_MAX_LOCK_TIME = 1 * 60 * 1000; // 1 min
|
||||
private static final int CHECK_PERIOD = 10;
|
||||
|
||||
private static final String LOCK_META_FOLDER = ".locks";
|
||||
private static final String LOCK_FILE_EXTENSION = ".lock";
|
||||
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private final Path lockFolderPath;
|
||||
private final String applicationId;
|
||||
private final int maxLockTime;
|
||||
|
||||
|
||||
public RMFileLockController(WebApplication application) throws DBException {
|
||||
this(application, DEFAULT_MAX_LOCK_TIME);
|
||||
}
|
||||
|
||||
// for tests
|
||||
public RMFileLockController(WebApplication application, int maxLockTime) throws DBException {
|
||||
this.lockFolderPath = application.getWorkspaceDirectory()
|
||||
.resolve(DBPWorkspace.METADATA_FOLDER)
|
||||
.resolve(LOCK_META_FOLDER);
|
||||
this.applicationId = application.getApplicationInstanceId();
|
||||
this.maxLockTime = maxLockTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the project for the duration of any operation.
|
||||
* Other threads/processes will also see this lock, and will wait for it to end
|
||||
* or force intercept lock, if the operation will take too long and
|
||||
* exceeds the maximum available locking time {@link #maxLockTime} or the lock is invalid {@link #awaitUnlock)}.
|
||||
*
|
||||
* @param projectId - project to be locked
|
||||
* @param operationName - executed operation name
|
||||
* @return - lock
|
||||
*/
|
||||
public RMLock lockProject(String projectId, String operationName) throws DBException {
|
||||
synchronized (RMFileLockController.class) {
|
||||
try {
|
||||
createLockFolderIfNeeded();
|
||||
createProjectFolder(projectId);
|
||||
Path projectLockFile = getProjectLockFilePath(projectId);
|
||||
|
||||
RMLockInfo lockInfo = new RMLockInfo.Builder(projectId, UUID.randomUUID().toString())
|
||||
.setApplicationId(applicationId)
|
||||
.setOperationName(operationName)
|
||||
.setOperationStartTime(System.currentTimeMillis())
|
||||
.build();
|
||||
createLockFile(projectLockFile, lockInfo);
|
||||
return new RMLock(projectLockFile);
|
||||
} catch (Exception e) {
|
||||
throw new DBException("Failed to lock project: " + projectId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that project locked
|
||||
*/
|
||||
public boolean isProjectLocked(String projectId) {
|
||||
Path projectLockFilePath = getProjectLockFilePath(projectId);
|
||||
return isLocked(projectLockFilePath);
|
||||
}
|
||||
|
||||
protected boolean isLocked(Path lockFilePath) {
|
||||
return Files.exists(lockFilePath);
|
||||
}
|
||||
|
||||
private void createLockFile(Path projectLockFile, RMLockInfo lockInfo) throws DBException, InterruptedException {
|
||||
boolean lockFileCreated = false;
|
||||
while (!lockFileCreated) {
|
||||
if (Files.exists(projectLockFile)) {
|
||||
awaitUnlock(lockInfo.getProjectId(), projectLockFile);
|
||||
}
|
||||
try {
|
||||
Files.createFile(projectLockFile);
|
||||
lockFileCreated = true;
|
||||
} catch (IOException e) {
|
||||
if (Files.exists(projectLockFile)) {
|
||||
log.info("Looks like file was locked by another rm instance at the same time");
|
||||
continue;
|
||||
} else {
|
||||
throw new DBException("Failed to create lock file: " + projectLockFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Files.write(projectLockFile, gson.toJson(lockInfo).getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to write lock info, unlock project: " + lockInfo.getProjectId());
|
||||
try {
|
||||
Files.deleteIfExists(lockFolderPath);
|
||||
} catch (IOException ex) {
|
||||
throw new DBException("Failed to remove invalid lock file: " + projectLockFile, ex);
|
||||
}
|
||||
throw new DBException("Failed to lock project: " + lockInfo.getProjectId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void createProjectFolder(String projectId) throws DBException {
|
||||
Path projectLocksFolder = lockFolderPath.resolve(projectId);
|
||||
if (Files.exists(projectLocksFolder)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.createDirectories(projectLocksFolder);
|
||||
} catch (Exception e) {
|
||||
if (Files.exists(projectLocksFolder)) {
|
||||
// ignore, because file can be created by another server
|
||||
} else {
|
||||
throw new DBException("Failed to create project lock folder: " + projectId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void awaitUnlock(String projectId, Path projectLockFile) throws InterruptedException, DBException {
|
||||
if (!isLocked(projectLockFile)) {
|
||||
return;
|
||||
}
|
||||
awaitingUnlock(projectId, projectLockFile);
|
||||
}
|
||||
|
||||
protected void awaitingUnlock(String projectId, Path projectLockFile) throws DBException, InterruptedException {
|
||||
log.info("Waiting for a file to be unlocked: " + projectLockFile);
|
||||
RMLockInfo originalLockInfo = readLockInfo(projectId, projectLockFile);
|
||||
boolean fileUnlocked = originalLockInfo == null; //lock can be removed at the moment when we try to read lock file info
|
||||
int maxIterations = maxLockTime / CHECK_PERIOD;
|
||||
int currentCheckCount = 0;
|
||||
|
||||
while (!fileUnlocked) {
|
||||
fileUnlocked = !isLocked(projectLockFile);
|
||||
if (currentCheckCount >= maxIterations || fileUnlocked) {
|
||||
break;
|
||||
}
|
||||
if (originalLockInfo != null & originalLockInfo.isBlank()) {
|
||||
// possible in situation where the project has just been locked
|
||||
// and the lock information has not yet been written
|
||||
originalLockInfo = readLockInfo(projectId, projectLockFile);
|
||||
}
|
||||
currentCheckCount++;
|
||||
Thread.sleep(CHECK_PERIOD);
|
||||
}
|
||||
if (fileUnlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
RMLockInfo currentLockInfo = readLockInfo(projectId, projectLockFile);
|
||||
if (currentLockInfo == null) {
|
||||
// file unlocked now
|
||||
return;
|
||||
}
|
||||
|
||||
//checking that this is not a new lock from another operation
|
||||
if (originalLockInfo.getOperationId().equals(currentLockInfo.getOperationId())) {
|
||||
forceUnlock(projectLockFile);
|
||||
} else {
|
||||
awaitUnlock(projectId, lockFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
protected void forceUnlock(Path projectLockFile) {
|
||||
// something went wrong and lock is invalid
|
||||
log.warn("File has not been unlocked within the expected period, force unlock");
|
||||
try {
|
||||
Files.deleteIfExists(projectLockFile);
|
||||
} catch (IOException e) {
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
/**
|
||||
@return
|
||||
- null if lock not exist;
|
||||
- empty lock info if the lock has just been created and the information has not yet been written;
|
||||
- lock info
|
||||
*/
|
||||
private RMLockInfo readLockInfo(String projectId, Path projectLockFile) throws DBException {
|
||||
if (Files.notExists(projectLockFile)) {
|
||||
return null;
|
||||
}
|
||||
try (Reader reader = Files.newBufferedReader(projectLockFile, StandardCharsets.UTF_8)) {
|
||||
return gson.fromJson(reader, RMLockInfo.class);
|
||||
} catch (IOException e) {
|
||||
if (!isLocked(projectLockFile)) {
|
||||
return null;
|
||||
}
|
||||
log.warn("Failed to read lock file info, but lock file still exist: " + projectLockFile);
|
||||
return RMLockInfo.emptyLock(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
private Path getProjectLockFilePath(String projectId) {
|
||||
return lockFolderPath.resolve(projectId).resolve(projectId + LOCK_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
private void createLockFolderIfNeeded() throws IOException {
|
||||
synchronized (RMFileLockController.class) {
|
||||
if (Files.notExists(lockFolderPath)) {
|
||||
Files.createDirectories(lockFolderPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2023 DBeaver Corp and others
|
||||
*
|
||||
* 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 io.cloudbeaver.model.rm.lock;
|
||||
|
||||
import org.jkiss.dbeaver.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Resource Manager resource lock
|
||||
*/
|
||||
public class RMLock implements AutoCloseable {
|
||||
private static final Log log = Log.getLog(RMLock.class);
|
||||
|
||||
private final Path lockFilePath;
|
||||
|
||||
public RMLock(Path lockFilePath) {
|
||||
this.lockFilePath = lockFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock resource and remove .lock file
|
||||
*/
|
||||
public void unlock() {
|
||||
try {
|
||||
Files.deleteIfExists(lockFilePath);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to unlock file: " + lockFilePath, e);
|
||||
if (Files.exists(lockFilePath)) {
|
||||
// file still locket, try to unlock again
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return path to the lock file
|
||||
*/
|
||||
protected Path getLockFilePath() {
|
||||
return lockFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2023 DBeaver Corp and others
|
||||
*
|
||||
* 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 io.cloudbeaver.model.rm.lock;
|
||||
|
||||
public class RMLockInfo {
|
||||
private final String applicationId;
|
||||
private final String projectId;
|
||||
private final String operationId;
|
||||
private final String operationName;
|
||||
private final long operationStartTime;
|
||||
|
||||
private RMLockInfo(
|
||||
String applicationId,
|
||||
String projectId,
|
||||
String operationId,
|
||||
String operationName,
|
||||
long operationStartTime
|
||||
) {
|
||||
this.applicationId = applicationId;
|
||||
this.projectId = projectId;
|
||||
this.operationId = operationId;
|
||||
this.operationName = operationName;
|
||||
this.operationStartTime = operationStartTime;
|
||||
}
|
||||
|
||||
static RMLockInfo emptyLock(
|
||||
String projectId
|
||||
) {
|
||||
return new RMLockInfo(
|
||||
"",
|
||||
projectId,
|
||||
"",
|
||||
"",
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public boolean isBlank() {
|
||||
return operationId.isEmpty();
|
||||
}
|
||||
|
||||
public String getApplicationId() {
|
||||
return applicationId;
|
||||
}
|
||||
|
||||
public String getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public String getOperationId() {
|
||||
return operationId;
|
||||
}
|
||||
|
||||
public String getOperationName() {
|
||||
return operationName;
|
||||
}
|
||||
|
||||
public long getOperationStartTime() {
|
||||
return operationStartTime;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String applicationId;
|
||||
private final String projectId;
|
||||
private final String operationId;
|
||||
private String operationName;
|
||||
private long operationStartTime;
|
||||
|
||||
public Builder(String projectId, String operationId) {
|
||||
this.projectId = projectId;
|
||||
this.operationId = operationId;
|
||||
}
|
||||
|
||||
|
||||
public Builder setApplicationId(String applicationId) {
|
||||
this.applicationId = applicationId;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public Builder setOperationName(String operationName) {
|
||||
this.operationName = operationName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setOperationStartTime(long operationStartTime) {
|
||||
this.operationStartTime = operationStartTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RMLockInfo build() {
|
||||
return new RMLockInfo(applicationId, projectId, operationId, operationName, operationStartTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,7 +519,8 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat
|
||||
}
|
||||
|
||||
@Override
|
||||
public RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, @NotNull DBPWorkspace workspace) {
|
||||
public RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider,
|
||||
@NotNull DBPWorkspace workspace) throws DBException {
|
||||
return LocalResourceController.builder(credentialsProvider, workspace, this::getSecurityController).build();
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -89,9 +89,9 @@ public class WebServiceNavigator implements DBWServiceNavigator {
|
||||
nodeChildren = DBNUtils.getNodeChildrenFiltered(monitor, rootNode, true);
|
||||
} else {
|
||||
DBNNode parentNode = navigatorModel.getNodeByPath(monitor, parentPath);
|
||||
if (parentNode == null) {
|
||||
throw new DBWebException("Node '" + parentPath + "' not found");
|
||||
}
|
||||
if (parentNode == null) {
|
||||
throw new DBWebException("Node '" + parentPath + "' not found");
|
||||
}
|
||||
if (!parentNode.hasChildren(false)) {
|
||||
return EMPTY_NODE_LIST;
|
||||
}
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2023 DBeaver Corp and others
|
||||
*
|
||||
* 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 io.cloudbeaver.model.rm.lock;
|
||||
|
||||
import io.cloudbeaver.test.platform.CEServerTestSuite;
|
||||
import org.jkiss.dbeaver.Log;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class RMLockTest {
|
||||
private static final Log log = Log.getLog(RMLockTest.class);
|
||||
private final String project1 = "s_fakeProject1";
|
||||
private final String project2 = "s_fakeProject2";
|
||||
|
||||
|
||||
@Test
|
||||
public void testProjectAccessUsingSeveralControllers() throws Throwable {
|
||||
var lockController1 = new TestLockController(CEServerTestSuite.getTestApp(), 1);
|
||||
|
||||
CountDownLatch thread1CDL = new CountDownLatch(1);
|
||||
CountDownLatch thread2CDL = new CountDownLatch(1);
|
||||
CountDownLatch globalCountDown = new CountDownLatch(2);
|
||||
|
||||
AtomicBoolean isLockedByThread1 = new AtomicBoolean(false);
|
||||
AtomicReference<Throwable> exceptionReference = new AtomicReference<>();
|
||||
var thread1 = new Thread(() -> {
|
||||
try (var lock = lockController1.lockProject(project1, "testThatProjectLocked1")) {
|
||||
isLockedByThread1.set(true);
|
||||
thread2CDL.countDown();
|
||||
thread1CDL.await(1, TimeUnit.MINUTES);
|
||||
} catch (Throwable e) {
|
||||
log.error(e);
|
||||
exceptionReference.set(e);
|
||||
} finally {
|
||||
isLockedByThread1.set(false);
|
||||
globalCountDown.countDown();
|
||||
}
|
||||
});
|
||||
thread1.start();
|
||||
|
||||
int atLeastWaitCalls = 1;
|
||||
var lockController2 = Mockito.spy(new TestLockController(CEServerTestSuite.getTestApp(), 100));
|
||||
Mockito.doAnswer(new Answer() {
|
||||
private int count = 0;
|
||||
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
count++;
|
||||
if (count == atLeastWaitCalls) {
|
||||
//unlock thread1
|
||||
thread1CDL.countDown();
|
||||
}
|
||||
return invocationOnMock.callRealMethod();
|
||||
}
|
||||
}).when(lockController2).awaitingUnlock(Mockito.any(), Mockito.any());
|
||||
var thread2 = new Thread(() -> {
|
||||
try {
|
||||
thread2CDL.await(1, TimeUnit.MINUTES);
|
||||
Assert.assertTrue("Thread 1 not running", isLockedByThread1.get());
|
||||
Assert.assertTrue("Project not locked", lockController2.isProjectLocked(project1));
|
||||
try (var lock = lockController2.lockProject(project1, "testThatProjectLocked2")) {
|
||||
Assert.assertFalse("Project locked by thread1, but thread2 has access to project", isLockedByThread1.get());
|
||||
//that we were really waiting for the file and the lock was not removed earlier
|
||||
Mockito.verify(lockController2, Mockito.atLeast(atLeastWaitCalls)).awaitingUnlock(Mockito.any(), Mockito.any());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.error(e);
|
||||
exceptionReference.set(e);
|
||||
} finally {
|
||||
globalCountDown.countDown();
|
||||
}
|
||||
});
|
||||
thread2.start();
|
||||
globalCountDown.await(1, TimeUnit.MINUTES);
|
||||
if (exceptionReference.get() != null) {
|
||||
throw exceptionReference.get();
|
||||
}
|
||||
Assert.assertFalse(lockController2.isProjectLocked(project1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessToDifferentProjects() throws Throwable {
|
||||
var lockController1 = new TestLockController(CEServerTestSuite.getTestApp(), 1);
|
||||
|
||||
CountDownLatch thread1CDL = new CountDownLatch(1);
|
||||
CountDownLatch thread2CDL = new CountDownLatch(1);
|
||||
CountDownLatch globalCountDown = new CountDownLatch(2);
|
||||
|
||||
AtomicBoolean isLockedByThread1 = new AtomicBoolean(false);
|
||||
AtomicBoolean isLockedByThread2 = new AtomicBoolean(false);
|
||||
AtomicReference<Throwable> exceptionReference = new AtomicReference<>();
|
||||
var thread1 = new Thread(() -> {
|
||||
try (var lock = lockController1.lockProject(project1, "testAccessToDifferentProjects1")) {
|
||||
isLockedByThread1.set(true);
|
||||
thread1CDL.await(1, TimeUnit.MINUTES);
|
||||
Assert.assertTrue("Project2 not locked by thread2", isLockedByThread2.get());
|
||||
thread2CDL.countDown();
|
||||
} catch (Throwable e) {
|
||||
log.error(e);
|
||||
exceptionReference.set(e);
|
||||
} finally {
|
||||
isLockedByThread1.set(false);
|
||||
globalCountDown.countDown();
|
||||
}
|
||||
});
|
||||
thread1.start();
|
||||
|
||||
var lockController2 = new TestLockController(CEServerTestSuite.getTestApp(), 1);
|
||||
var thread2 = new Thread(() -> {
|
||||
try {
|
||||
try (var lock = lockController2.lockProject(project2, "testAccessToDifferentProjects2")) {
|
||||
Assert.assertTrue("Project1 not locket by thread1", isLockedByThread1.get());
|
||||
isLockedByThread2.set(true);
|
||||
thread1CDL.countDown();
|
||||
thread2CDL.await();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.error(e);
|
||||
exceptionReference.set(e);
|
||||
} finally {
|
||||
isLockedByThread2.set(false);
|
||||
globalCountDown.countDown();
|
||||
}
|
||||
});
|
||||
thread2.start();
|
||||
globalCountDown.await(1, TimeUnit.MINUTES);
|
||||
if (exceptionReference.get() != null) {
|
||||
throw exceptionReference.get();
|
||||
}
|
||||
|
||||
Assert.assertFalse(lockController2.isProjectLocked(project1));
|
||||
Assert.assertFalse(lockController2.isProjectLocked(project2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForceUnlock() throws Throwable {
|
||||
var lockController1 = new TestLockController(CEServerTestSuite.getTestApp(), 1);
|
||||
|
||||
CountDownLatch thread1CDL = new CountDownLatch(1);
|
||||
CountDownLatch thread2CDL = new CountDownLatch(1);
|
||||
CountDownLatch globalCountDown = new CountDownLatch(2);
|
||||
|
||||
AtomicBoolean isLockedByThread1 = new AtomicBoolean(false);
|
||||
AtomicReference<Throwable> exceptionReference = new AtomicReference<>();
|
||||
var thread1 = new Thread(() -> {
|
||||
try (var lock = lockController1.lockProject(project1, "testForceUnlock1")) {
|
||||
isLockedByThread1.set(true);
|
||||
thread1CDL.await(1, TimeUnit.MINUTES);
|
||||
} catch (Throwable e) {
|
||||
log.error(e);
|
||||
exceptionReference.set(e);
|
||||
} finally {
|
||||
isLockedByThread1.set(false);
|
||||
globalCountDown.countDown();
|
||||
}
|
||||
});
|
||||
thread1.start();
|
||||
|
||||
var lockController2 = Mockito.spy(new TestLockController(CEServerTestSuite.getTestApp(), 100));
|
||||
var thread2 = new Thread(() -> {
|
||||
try {
|
||||
try (var lock = lockController2.lockProject(project1, "testForceUnlock2")) {
|
||||
Assert.assertTrue("Project1 not locket by thread1", isLockedByThread1.get());
|
||||
Mockito.verify(lockController2, Mockito.atLeast(5)).isLocked(Mockito.any());
|
||||
thread1CDL.countDown();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.error(e);
|
||||
exceptionReference.set(e);
|
||||
} finally {
|
||||
globalCountDown.countDown();
|
||||
}
|
||||
});
|
||||
thread2.start();
|
||||
globalCountDown.await(1, TimeUnit.MINUTES);
|
||||
if (exceptionReference.get() != null) {
|
||||
throw exceptionReference.get();
|
||||
}
|
||||
Assert.assertFalse(lockController2.isProjectLocked(project1));
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* DBeaver - Universal Database Manager
|
||||
* Copyright (C) 2010-2023 DBeaver Corp and others
|
||||
*
|
||||
* 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 io.cloudbeaver.model.rm.lock;
|
||||
|
||||
import io.cloudbeaver.model.app.WebApplication;
|
||||
import org.jkiss.dbeaver.DBException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class TestLockController extends RMFileLockController {
|
||||
public TestLockController(WebApplication application) throws DBException {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public TestLockController(WebApplication application, int maxTimeout) throws DBException {
|
||||
super(application, maxTimeout);
|
||||
}
|
||||
|
||||
//avoid mockito access method error
|
||||
@Override
|
||||
public void awaitingUnlock(String projectId, Path projectLockFile) throws InterruptedException, DBException {
|
||||
super.awaitingUnlock(projectId, projectLockFile);
|
||||
}
|
||||
|
||||
//avoid mockito access method error
|
||||
@Override
|
||||
public boolean isLocked(Path lockFilePath) {
|
||||
return super.isLocked(lockFilePath);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -17,6 +17,7 @@
|
||||
|
||||
package io.cloudbeaver.test.platform;
|
||||
|
||||
import io.cloudbeaver.model.rm.lock.RMLockTest;
|
||||
import io.cloudbeaver.server.CBApplication;
|
||||
import io.cloudbeaver.utils.WebTestUtils;
|
||||
import org.junit.AfterClass;
|
||||
@@ -29,7 +30,14 @@ import java.net.http.HttpClient;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@RunWith(Suite.class)
|
||||
@Suite.SuiteClasses({PlatformTest.class, AuthenticationTest.class, ResourceManagerTest.class })
|
||||
@Suite.SuiteClasses(
|
||||
{
|
||||
PlatformTest.class,
|
||||
AuthenticationTest.class,
|
||||
ResourceManagerTest.class,
|
||||
RMLockTest.class
|
||||
}
|
||||
)
|
||||
public class CEServerTestSuite {
|
||||
|
||||
public static final String GQL_API_URL = "http://localhost:18978/api/gql";
|
||||
@@ -80,6 +88,10 @@ public class CEServerTestSuite {
|
||||
testApp.stop();
|
||||
}
|
||||
|
||||
public static CBApplication getTestApp() {
|
||||
return testApp;
|
||||
}
|
||||
|
||||
public static HttpClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user