From ea7b39dee04f6646f57c10a85f11cd5f20e366f8 Mon Sep 17 00:00:00 2001 From: anton Date: Fri, 7 Jun 2024 17:58:48 +0200 Subject: [PATCH 01/10] FAIRSPC-87: added workflow to Build and deploy docs to Github Pages and fixed table in doc --- .github/workflows/build_and_deploy_docs.yaml | 39 ++++++++++++++++++++ README.adoc | 3 +- docs/deploy.sh | 18 +++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build_and_deploy_docs.yaml create mode 100755 docs/deploy.sh diff --git a/.github/workflows/build_and_deploy_docs.yaml b/.github/workflows/build_and_deploy_docs.yaml new file mode 100644 index 000000000..8af7555c7 --- /dev/null +++ b/.github/workflows/build_and_deploy_docs.yaml @@ -0,0 +1,39 @@ +# This workflow is triggered on any PR's changes + +name: Build and deploy docs to Github Pages + +on: + push: + branches: + - bugfix/FAIRSPC-87 + + +jobs: + build-saturn: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Log details + run: | + BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + echo "Triggered on branch: $BRANCH" + + - name: Set up Ruby (required for gem installation) + uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.7' + + - name: Install Asciidoctor + run: | + gem install asciidoctor + gem install asciidoctor-pdf + gem install rouge + + - name: Run Build Docs script + run: ./docs/build.sh + + - name: Run Deploy Docs script + run: ./docs/deploy.sh + diff --git a/README.adoc b/README.adoc index 6595a1e1a..1ccbdccba 100644 --- a/README.adoc +++ b/README.adoc @@ -1900,7 +1900,8 @@ Multiple external Fairspace metadata pages can be configured simultaneously. A l | String to be used as a display name of the metadata source. | ``url`` | Fairspace instance to connect to. If the url is not specified, the metadata source will be treated as the internal one. -| *Important!* There should only be a single configuration of internal metadata (only the first one will not be ignored). + +*Important!* There should only be a single configuration of internal metadata (only the first one will not be ignored). | ``icon-name`` | Name of an icon configured in the "icons" section of values.yaml file. If the name is not specified, there will be a default icon used. |=== diff --git a/docs/deploy.sh b/docs/deploy.sh new file mode 100755 index 000000000..afbc8f169 --- /dev/null +++ b/docs/deploy.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +cd .. +git clone "https://${CI_SERVICE_ACCOUNT_USER}:${CI_SERVICE_ACCOUNT_PASSWORD}@github.com/${DOCUMENTATION_REPO}" fairspace-docs +export DOCS_DIR=$(pwd)/fairspace-docs + +cp -r ./fairspace/docs/build/* "${DOCS_DIR}/" + +pushd "${DOCS_DIR}" +if [ ! "$(git status -s)" == "" ]; then + echo "Committing changes to ${DOCUMENTATION_REPO} ..." + git add . + git commit -a -m "Update from the documentation branch of ${TRAVIS_REPO_SLUG}." + git push "https://${GITHUB_USERNAME}:${GITHUB_PASSWORD}@github.com/${DOCUMENTATION_REPO}" main +else + echo "Documentation unchanged." +fi +popd From 326fe1ff423532b7f777757175c56695799ad93d Mon Sep 17 00:00:00 2001 From: anton Date: Sat, 8 Jun 2024 00:02:57 +0200 Subject: [PATCH 02/10] FAIRSPC-87: added workflow to Build and deploy docs to Github Pages and fixed table in doc --- .github/workflows/build_and_deploy_docs.yaml | 85 +++++++++++++++++--- docs/build.sh | 34 -------- docs/deploy.sh | 18 ----- 3 files changed, 75 insertions(+), 62 deletions(-) delete mode 100755 docs/build.sh delete mode 100755 docs/deploy.sh diff --git a/.github/workflows/build_and_deploy_docs.yaml b/.github/workflows/build_and_deploy_docs.yaml index 8af7555c7..4c83ce6ad 100644 --- a/.github/workflows/build_and_deploy_docs.yaml +++ b/.github/workflows/build_and_deploy_docs.yaml @@ -1,12 +1,9 @@ -# This workflow is triggered on any PR's changes - name: Build and deploy docs to Github Pages on: push: branches: - - bugfix/FAIRSPC-87 - + - release jobs: build-saturn: @@ -23,17 +20,85 @@ jobs: - name: Set up Ruby (required for gem installation) uses: ruby/setup-ruby@v1 with: - ruby-version: '2.7' + ruby-version: '3.3.2' - - name: Install Asciidoctor + - name: Install Asciidoctor to build docs run: | gem install asciidoctor gem install asciidoctor-pdf gem install rouge - - name: Run Build Docs script - run: ./docs/build.sh + - name: Build Docs - Collect needed files + run: | + set -e + + # Initialize variables + PROJECT_FILES=( + "projects/saturn/src/main/resources/log4j2.properties" + "projects/saturn/src/main/resources/system-vocabulary.ttl" + "projects/saturn/taxonomies.ttl" + "projects/saturn/views.yaml" + "projects/saturn/vocabulary.ttl" + ) + BUILD_DIR=./docs/build + version=$(cat VERSION) + + # Create build directory + mkdir -p $BUILD_DIR/docs + + # Copy all files to the build directory + cp ./README.adoc $BUILD_DIR + sed -i -e "s/VERSION/${version}/" $BUILD_DIR/README.adoc + cp -r ./docs/images $BUILD_DIR/docs/ + for f in ${PROJECT_FILES[*]}; do + mkdir -p "$BUILD_DIR/$(dirname "$f")" + cp "$f" "$BUILD_DIR/"$(dirname "$f")"" + done - - name: Run Deploy Docs script - run: ./docs/deploy.sh + - name: Build Docs - Generate PDF and HTML + run: | + set -e + + BUILD_DIR=./docs/build + + asciidoctor-pdf -a pdf-theme=./docs/pdf-theme.yml -o $BUILD_DIR/Fairspace.pdf $BUILD_DIR/README.adoc || { + echo "Error building PDF" + popd + exit 1 + } + + asciidoctor -a toc=left -D $BUILD_DIR/ -o index.html $BUILD_DIR/README.adoc || { + echo "Error building site" + popd + exit 1 + } + + rm $BUILD_DIR/README.adoc + - name: Deploy Docs (push to Github Pages, will be deployed automatically) + env: + CI_SERVICE_ACCOUNT_USER: ${{ secrets.CI_SERVICE_ACCOUNT_USER }} + CI_SERVICE_ACCOUNT_PASSWORD: ${{ secrets.FNS_PAT }} + DOCS_REPOSITORY_NAME: ${{ vars.DOCS_REPOSITORY_NAME }} + run: | + set -e + + DOCS_REPO_URL="https://${CI_SERVICE_ACCOUNT_USER}:${CI_SERVICE_ACCOUNT_PASSWORD}@github.com/thehyve/${DOCS_REPOSITORY_NAME}" + echo "Cloning documentation repository ${DOCS_REPOSITORY_NAME} ..." + git clone --branch main "${DOCS_REPO_URL}" fairspace-docs + + DOCS_DIR=$(pwd)/fairspace-docs + echo "Copying documentation to ${DOCS_DIR} ..." + cp -r ./docs/build/* "${DOCS_DIR}/" + cd "${DOCS_DIR}" + + if [ ! "$(git status -s)" == "" ]; then + echo "Committing and pushing changes to ${DOCS_REPOSITORY_NAME} ..." + git config --global user.email "${CI_SERVICE_ACCOUNT_USER}@thehyve.nl" + git config --global user.name "${CI_SERVICE_ACCOUNT_USER}" + git add . + git commit -a -m "Update documentation" + git push "${DOCS_REPO_URL}" main + else + echo "Documentation unchanged." + fi diff --git a/docs/build.sh b/docs/build.sh deleted file mode 100755 index b8356ac20..000000000 --- a/docs/build.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -PROJECT_FILES=( - "projects/saturn/src/main/resources/log4j2.properties" - "projects/saturn/src/main/resources/system-vocabulary.ttl" - "projects/saturn/taxonomies.ttl" - "projects/saturn/views.yaml" - "projects/saturn/vocabulary.ttl" -) - - -here=$(dirname "${0}") -pushd "${here}" -version=$(cat ../VERSION) -mkdir -p build/docs -cp ../README.adoc build/ -sed -i -e "s/VERSION/${version}/" build/README.adoc -cp -r images build/docs/ -for f in ${PROJECT_FILES[*]}; do - mkdir -p "build/$(dirname "$f")" - cp "../$f" "build/"$(dirname "$f")"" -done -asciidoctor-pdf -a pdf-theme=pdf-theme.yml -o build/Fairspace.pdf build/README.adoc || { - echo "Error building PDF" - popd - exit 1 -} -asciidoctor -a toc=left -D build/ -o index.html build/README.adoc || { - echo "Error building site" - popd - exit 1 -} -rm build/README.adoc -popd diff --git a/docs/deploy.sh b/docs/deploy.sh deleted file mode 100755 index afbc8f169..000000000 --- a/docs/deploy.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -cd .. -git clone "https://${CI_SERVICE_ACCOUNT_USER}:${CI_SERVICE_ACCOUNT_PASSWORD}@github.com/${DOCUMENTATION_REPO}" fairspace-docs -export DOCS_DIR=$(pwd)/fairspace-docs - -cp -r ./fairspace/docs/build/* "${DOCS_DIR}/" - -pushd "${DOCS_DIR}" -if [ ! "$(git status -s)" == "" ]; then - echo "Committing changes to ${DOCUMENTATION_REPO} ..." - git add . - git commit -a -m "Update from the documentation branch of ${TRAVIS_REPO_SLUG}." - git push "https://${GITHUB_USERNAME}:${GITHUB_PASSWORD}@github.com/${DOCUMENTATION_REPO}" main -else - echo "Documentation unchanged." -fi -popd From bb88ba93d213f31265ccef9dda0fd05145ba749a Mon Sep 17 00:00:00 2001 From: EIjo Date: Tue, 18 Jun 2024 11:59:32 +0200 Subject: [PATCH 03/10] Updated installation guide readme file --- README.adoc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index 1ccbdccba..cc4b7a1f0 100644 --- a/README.adoc +++ b/README.adoc @@ -2499,18 +2499,20 @@ DOCKER_LOGGING_DRIVER=json-file ---- To run the development version, checkout this repository, -navigate to ``projects/mercury`` and run +navigate to ``projects/mercury`` and run the following commands (``yarn install`` only has to be ran the first time running fairspace). [source, shell] ---- +yarn install yarn dev ---- + This will start a Keycloak instance for authentication at port ``5100``, the backend application named Saturn at port ``8080`` and the user interface at port ``3000``. -At first run, you need to configure the service account in Keycloak. +At first run, you need to configure the service account in Keycloak. If you cannot log in, you might need to restart fairspace by closing it and running ``yarn dev`` again. * Navigate to link:http://localhost:5100[http://localhost:5100] * Login with credentials ``keycloak``, ``keycloak`` @@ -2519,7 +2521,7 @@ At first run, you need to configure the service account in Keycloak. ** Click `Clients` in the left menu -> Select 'workspace-client' ** Choose tab `Service Account Roles` ** Click `Assign Role` -** Select `Filter by clients` from the drop down menu and search for role name `view-users`. Then click `Asign role`. +** Select `Filter by clients` from the drop down menu and search for role name `view-users`. Then click `Assign`. Now everything should be ready to start using Fairspace: From fd1c61f0ea8894dcccbc33257415e618acfa3718 Mon Sep 17 00:00:00 2001 From: anton Date: Wed, 19 Jun 2024 16:24:02 +0200 Subject: [PATCH 04/10] FAIRSPC-102: fixed view update on triple deletion (property deletion) --- .../saturn/services/views/ViewStoreClient.java | 14 +++++--------- .../saturn/services/views/ViewUpdater.java | 5 +---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java index c74d6d0df..654560944 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java @@ -203,17 +203,13 @@ public void addLabel(String id, String type, String label) throws SQLException { public int updateRows(String view, List> rows, boolean bulkInsert) throws SQLException { var viewTable = configuration.viewTables.get(view); var config = configuration.viewConfig.get(view); - // Find the columns for which there are values in the rows + // Find the columns in the rows of type different from Set var columnNames = rows.stream() - .flatMap(row -> row.entrySet().stream() - .filter(entry -> entry.getValue() != null) - .map(Map.Entry::getKey)) + .flatMap(row -> row.keySet().stream()) .distinct() .filter(columnName -> config.columns.stream() - .noneMatch(column -> - // Skip value set columns - column.name.equalsIgnoreCase(columnName) && column.type.isSet())) - .collect(Collectors.toList()); + .noneMatch(column -> column.name.equalsIgnoreCase(columnName) && column.type.isSet())) + .toList(); if (columnNames.isEmpty()) { return 0; } @@ -228,7 +224,7 @@ public int updateRows(String view, List> rows, boolean bulkI for (var row : rows) { var values = columnNames.stream() .map(columnName -> row.getOrDefault(columnName, null)) - .collect(Collectors.toList()); + .toList(); var id = (String) row.get("id"); var exists = (!bulkInsert) && rowExists(viewTable.name, id); if (exists) { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java index 39d8d8878..289f1aedc 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java @@ -176,10 +176,7 @@ public void updateSubject(Node subject) { try { for (var column : view.columns) { var objects = retrieveValues(graph, subject, column.source); - if (objects.isEmpty()) { - continue; - } - row.put(column.name, getValue(column, objects.get(0))); + row.put(column.name, objects.isEmpty() ? null : getValue(column, objects.getFirst())); } viewStoreClient.updateRows(view.name, List.of(row), false); } catch (SQLException e) { From b35c02f8470211c804b8293622bec07368505fa8 Mon Sep 17 00:00:00 2001 From: Frank <29271979+frankyhollywood@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:27:31 +0200 Subject: [PATCH 05/10] Compact a Jena database (#1535) * Compact Jena database * Compact db with usage of dataset configuration * Cleanup and PR comment resolves * Documentation for maintenance API * Added test for dataset factory --------- Co-authored-by: anton --- README.adoc | 53 +++++++++++++++- .../saturn/rdf/SaturnDatasetFactory.java | 7 ++- .../transactions/TxnIndexDatasetGraph.java | 4 ++ .../rdf/transactions/TxnLogDatasetGraph.java | 6 ++ .../services/maintenance/MaintenanceApp.java | 10 +++ .../maintenance/MaintenanceService.java | 63 ++++++++++++++++++- .../saturn/services/views/ViewUpdater.java | 3 + .../saturn/rdf/SaturnDatasetFactoryTest.java | 18 ++++++ .../maintenance/MaintenanceServiceTest.java | 4 +- 9 files changed, 159 insertions(+), 9 deletions(-) diff --git a/README.adoc b/README.adoc index cc4b7a1f0..428c0326b 100644 --- a/README.adoc +++ b/README.adoc @@ -1748,13 +1748,62 @@ Only allowed for administrators. | Service not available. This means that the application is configured not to use a view database. |=== +|=== +2+| ``POST /api/maintenance/compact`` + +2+| Compact the Jena TDB database files. + +Jena database files grow fast when using transactions. This operation will compact the database files to reduce their size. If data is inserted using many small transactions the files will be reduced to 10-20% of their original size. + +Only allowed for administrators. +2+| _Response:_ +| ``204`` +| Asynchronous task to compact Jena files has started. +| ``403`` +| Operation not allowed. The current user is not an administrator. +| ``409`` +| Reindexing is already in progress. +| ``503`` +| Service not available. This means that the application is configured not to use a view database. +|=== + + +.Example of compacting Jena using curl +[%collapsible] +==== +[source, bash] +---- +curl -X GET 'http://localhost:8080/api/maintenance/compact' +---- +==== + +|=== +2+| ``GET /api/maintenance/status`` + +2+| Get the status of maintenance tasks. + +It is not possible to run more than one maintenance task at the same time. If you start a task while another task is running, the new task will be rejected. If you want to know in advance whether a task is running, you can use this endpoint. + +A text is return + +2+| _Response:_ +| ``200`` +| Returns "active" or "inactive" +| ``403`` +| Operation not allowed. The current user is not an administrator. +| ``409`` +| Reindexing is already in progress. +| ``503`` +| Service not available. This means that the application is configured not to use a view database. +|=== + -.Example recreate index using curl +.Example of getting the maintenance status using curl [%collapsible] ==== [source, bash] ---- -curl -X POST 'http://localhost:8080/api/maintenance/reindex' +curl -X POST 'http://localhost:8080/api/maintenance/status' ---- ==== diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java index cc3834fd1..7b50f5f13 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java @@ -22,9 +22,11 @@ public class SaturnDatasetFactory { /** * Returns a dataset to work with. * We're playing Russian dolls here. - * The original TDB2 dataset graph, which in fact consists of a number of wrappers itself (Jena uses wrappers everywhere), + * The original TDB2 dataset graph, which in fact consists of a number of + * wrappers itself (Jena uses wrappers everywhere), * is wrapped with a number of wrapper classes, each adding a new feature. - * Currently it adds transaction logging and applies default vocabulary if needed. + * Currently it adds transaction logging and applies default vocabulary if + * needed. */ public static Dataset connect(Config.Jena config, ViewStoreClientFactory viewStoreClientFactory) { var restoreNeeded = isRestoreNeeded(config.datasetPath); @@ -40,6 +42,7 @@ public static Dataset connect(Config.Jena config, ViewStoreClientFactory viewSto } if (restoreNeeded) { + log.warn("Jena restore is needed, starting automatic restore."); restore(dsg, txnLog); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java index 861bd65c5..9667f06b8 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java @@ -48,6 +48,10 @@ protected void onChange(QuadAction action, Node graph, Node subject, Node predic } } + public DatasetGraph getDatasetGraph() { + return dsg; + } + @Override public void begin(TxnType type) { begin(TxnType.convert(type)); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java index 5b5876684..0f7442fb9 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java @@ -22,10 +22,12 @@ public class TxnLogDatasetGraph extends AbstractChangesAwareDatasetGraph { private final TransactionLog transactionLog; private volatile AccessToken user; + private DatasetGraph dsg; public TxnLogDatasetGraph(DatasetGraph dsg, TransactionLog transactionLog) { super(dsg); this.transactionLog = transactionLog; + this.dsg = dsg; } /** @@ -46,6 +48,10 @@ protected void onChange(QuadAction action, Node graph, Node subject, Node predic }); } + public DatasetGraph getDatasetGraph() { + return dsg; + } + @Override public void begin(TxnType type) { begin(TxnType.convert(type)); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java index 7188383e3..c0fd85681 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java @@ -3,6 +3,7 @@ import io.fairspace.saturn.services.BaseApp; import static javax.servlet.http.HttpServletResponse.*; +import static spark.Spark.get; import static spark.Spark.post; public class MaintenanceApp extends BaseApp { @@ -21,5 +22,14 @@ protected void initApp() { res.status(SC_NO_CONTENT); return ""; }); + post("/compact", (req, res) -> { + maintenanceService.compactRdfStorageTask(); + res.status(SC_NO_CONTENT); + return ""; + }); + get("/status", (req, res) -> { + res.status(SC_OK); + return maintenanceService.active() ? "active" : "inactive"; + }); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java index 20efd650b..21d5ba655 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java @@ -9,8 +9,13 @@ import lombok.NonNull; import lombok.extern.log4j.Log4j2; import org.apache.jena.query.Dataset; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.tdb2.DatabaseMgr; +import org.apache.jena.tdb2.store.DatasetGraphSwitchable; import io.fairspace.saturn.config.ConfigLoader; +import io.fairspace.saturn.rdf.transactions.TxnIndexDatasetGraph; +import io.fairspace.saturn.rdf.transactions.TxnLogDatasetGraph; import io.fairspace.saturn.services.AccessDeniedException; import io.fairspace.saturn.services.ConflictException; import io.fairspace.saturn.services.NotAvailableException; @@ -22,7 +27,7 @@ @Log4j2 public class MaintenanceService { public static final String SERVICE_NOT_AVAILABLE = "Service not available"; - public static final String REINDEXING_IS_ALREADY_IN_PROGRESS = "Reindexing is already in progress."; + public static final String MAINTENANCE_IS_IN_PROGRESS = "Maintenance is in progress."; private final ThreadPoolExecutor threadpool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); @@ -59,9 +64,10 @@ public synchronized void startRecreateIndexTask() { throw new NotAvailableException(SERVICE_NOT_AVAILABLE); } if (active()) { - log.info("Reindexing is already in progress."); - throw new ConflictException(REINDEXING_IS_ALREADY_IN_PROGRESS); + log.info(MAINTENANCE_IS_IN_PROGRESS); + throw new ConflictException(MAINTENANCE_IS_IN_PROGRESS); } + threadpool.submit(() -> { log.info("Start asynchronous reindexing task"); recreateIndex(); @@ -70,6 +76,35 @@ public synchronized void startRecreateIndexTask() { }); } + public void compactRdfStorageTask() { + if (!userService.currentUser().isAdmin()) { + throw new AccessDeniedException(); + } + if (active()) { + log.info(MAINTENANCE_IS_IN_PROGRESS); + throw new ConflictException(MAINTENANCE_IS_IN_PROGRESS); + } + + threadpool.submit(() -> { + log.info("Compacting RDF storage started"); + try { + var ds = unwrap(dataset.asDatasetGraph()); + if (ds == null) { + log.warn("Compacting RDF storage is not supported for this storage type"); + return; + } + DatabaseMgr.compact(ds, true); + } catch (Exception e) { + log.error("Error compacting RDF storage", e); + throw new RuntimeException("Error compacting RDF storage", e); + } + log.info("Compacting RDF storage finished"); + }); + } + + /** + * Only use this method in a secure and synchonisized way, see 'recreateIndex()' + */ public void recreateIndex() { try (var viewStoreClient = viewStoreClientFactory.build(); var viewUpdater = new ViewUpdater(viewStoreClient, dataset.asDatasetGraph())) { @@ -84,4 +119,26 @@ public void recreateIndex() { throw new RuntimeException("Failed to recreate index", e); } } + + /** + * Unwrap the dataset graph to the underlying dataset graph that supports + * compacting. + * + * @param dsg the dataset graph + * @return the underlying dataset graph that supports compacting + */ + public static DatasetGraph unwrap(DatasetGraph dsg) { + if (dsg == null || dsg instanceof DatasetGraphSwitchable) { + return dsg; + } + if (dsg instanceof TxnLogDatasetGraph) { + return unwrap(((TxnLogDatasetGraph) dsg).getDatasetGraph()); + } + + if (dsg instanceof TxnIndexDatasetGraph) { + return unwrap(((TxnIndexDatasetGraph) dsg).getDatasetGraph()); + } + + return null; + } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java index 289f1aedc..d665682d3 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java @@ -237,6 +237,9 @@ public void updateSubject(Node subject) { log.debug("Updating subject of type {} took {}ms", type.getLocalName(), new Date().getTime() - start); } + /** + * Only use this method in a secure and synchonisized way, see 'MaintenanceService.recreateIndex()' + */ public void recreateIndexForView(ViewStoreClient viewStoreClient, ViewsConfig.View view) throws SQLException { // Clear database tables for view log.info("Recreating index for view {} started", view.name); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java index 852541beb..8fd86ed75 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java @@ -3,14 +3,21 @@ import java.io.File; import java.io.IOException; +import org.apache.jena.tdb2.store.DatasetGraphSwitchable; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import io.fairspace.saturn.services.maintenance.MaintenanceService; +import io.fairspace.saturn.services.views.ViewStoreClientFactory; + +import static io.fairspace.saturn.config.ConfigLoader.CONFIG; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; @RunWith(MockitoJUnitRunner.class) public class SaturnDatasetFactoryTest { @@ -42,4 +49,15 @@ public void testIsRestoreIfNoDataDirectoryIsPresent() throws IOException { new File(datasetPath, "lost+found").mkdirs(); assertTrue(SaturnDatasetFactory.isRestoreNeeded(datasetPath)); } + + @Test + public void testUnwrappingDatasetGraphIsOfRightType() { + // give + var viewStoreClientFactory = mock(ViewStoreClientFactory.class); + var ds = SaturnDatasetFactory.connect(CONFIG.jena, viewStoreClientFactory); + + var dataSetGraph = MaintenanceService.unwrap(ds.asDatasetGraph()); + + assertTrue(dataSetGraph instanceof DatasetGraphSwitchable); + } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java index 9f0b13350..9d0aec3c1 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java @@ -13,7 +13,7 @@ import io.fairspace.saturn.services.views.ViewService; import io.fairspace.saturn.services.views.ViewStoreClientFactory; -import static io.fairspace.saturn.services.maintenance.MaintenanceService.REINDEXING_IS_ALREADY_IN_PROGRESS; +import static io.fairspace.saturn.services.maintenance.MaintenanceService.MAINTENANCE_IS_IN_PROGRESS; import static io.fairspace.saturn.services.maintenance.MaintenanceService.SERVICE_NOT_AVAILABLE; import static org.junit.Assert.assertThrows; @@ -68,7 +68,7 @@ public void testReindexingIsNotAllowedWhenActive() { doReturn(true).when(sut).active(); // when/then - assertThrows(REINDEXING_IS_ALREADY_IN_PROGRESS, ConflictException.class, sut::startRecreateIndexTask); + assertThrows(MAINTENANCE_IS_IN_PROGRESS, ConflictException.class, sut::startRecreateIndexTask); } @Test From 120315342863caa75391e4b6c3d40d374e15e6e0 Mon Sep 17 00:00:00 2001 From: anton Date: Wed, 10 Jul 2024 14:45:52 +0200 Subject: [PATCH 06/10] Change the way materialized views are updated using concurrently --- README.adoc | 2 +- .../views/MaterializedViewService.java | 151 ++++++++++++++---- 2 files changed, 125 insertions(+), 28 deletions(-) diff --git a/README.adoc b/README.adoc index 428c0326b..01119e855 100644 --- a/README.adoc +++ b/README.adoc @@ -3320,7 +3320,7 @@ To address this issue, materialized views are used for enhanced performance. There are two types of materialized views: one for denormalized data, which includes the view ID and attribute data of Set/TermSet types, and another for joined views. For each joined view, there is one corresponding join materialized view (as specified in the views.yaml config). -Materialized views are refreshed during database reindexing and metadata updates, provided that the doViewsUpdate flag is set to true in the metadata endpoints. +Materialized views are refreshed during database reindexing, on Saturn initialization stage and metadata updates, provided that the doViewsUpdate flag is set to true in the metadata endpoints. The refresh is performed concurrently what allows for the system to be available during the update providing the old version of data until the new one is ready. ==== Extra file storage diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java index 2766cc7ab..c4bba535a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java @@ -17,9 +17,11 @@ @Slf4j public class MaterializedViewService { - public static final String INDEX_POSTFIX = "_idx"; - private final DataSource dataSource; + private static final String INDEX_POSTFIX = "_idx"; + private static final String UNIQUE_INDEX_POSTFIX = "_unique_idx"; + private static final int FIRST_ROW_IDX = 1; + private final DataSource dataSource; private final ViewStoreClient.ViewStoreConfiguration configuration; private final int maxJoinItems; @@ -37,47 +39,115 @@ public void createOrUpdateAllMaterializedViews() { createOrUpdateJoinMaterializedView(view, connection); } } catch (SQLException e) { - log.error("Materialized view update failed", e); + log.error("Materialized view create/update failed", e); throw new RuntimeException(e); } } - public void createOrUpdateViewMaterializedView(ViewsConfig.View view, Connection connection) throws SQLException { + private void createOrUpdateViewMaterializedView(ViewsConfig.View view, Connection connection) throws SQLException { var setColumns = view.columns.stream().filter(column -> column.type.isSet()).toList(); if (!setColumns.isEmpty()) { String viewName = view.name.toLowerCase(); var mvName = "mv_%s".formatted(viewName); - log.info("{} refresh has started", mvName); - dropMaterializedViewIfExists(mvName, connection); - createViewMaterializedView(mvName, view, setColumns, connection); - createMaterializedViewIndex(viewName + INDEX_POSTFIX, mvName, viewName + "id", connection); - log.info("{} refresh has finished successfully", mvName); + + log.info("View materialized view {} create/update has started", mvName); + // all checks and changes to be done in one transaction + connection.setAutoCommit(false); + try { + List columns = collectViewColumns(view); + if (doesMaterializedViewExist(mvName, connection)) { + // 'refresh' requires unique index based on all columns, so we have to check if it exists + createMaterializedViewUniqueIndexIfNotExist( + viewName + UNIQUE_INDEX_POSTFIX, mvName, columns, connection); + refreshMaterializedView(mvName, connection); + } else { + createViewMaterializedView(mvName, view, setColumns, connection); + createMaterializedViewIndex(viewName + INDEX_POSTFIX, mvName, viewName + "id", connection); + createMaterializedViewUniqueIndexIfNotExist( + viewName + UNIQUE_INDEX_POSTFIX, mvName, columns, connection); + } + connection.commit(); + log.info("View materialized view {} create/update has finished successfully", mvName); + } catch (SQLException e) { + connection.rollback(); + throw e; + } finally { + connection.setAutoCommit(true); + } } } - public void createOrUpdateJoinMaterializedView(ViewsConfig.View view, Connection connection) throws SQLException { + private void createOrUpdateJoinMaterializedView(ViewsConfig.View view, Connection connection) throws SQLException { for (ViewsConfig.View.JoinView joinView : view.join) { String viewName = view.name.toLowerCase(); String joinViewName = joinView.view.toLowerCase(); var mvName = "mv_%s_join_%s".formatted(viewName, joinViewName); - log.info("{} refresh has started", mvName); - dropMaterializedViewIfExists(mvName, connection); - createJoinMaterializedViews(view, joinView, connection); - createMaterializedViewIndex(mvName + "_" + viewName + INDEX_POSTFIX, mvName, viewName + "_id", connection); - createMaterializedViewIndex( - mvName + "_" + joinViewName + INDEX_POSTFIX, mvName, joinViewName + "_id", connection); - log.info("{} refresh has finished successfully", mvName); + + log.info("Join materialized view {} create/update has started", mvName); + // all checks and changes to be done in one transaction + connection.setAutoCommit(false); + try { + List columns = collectJoinColumns(view, joinView); + if (doesMaterializedViewExist(mvName, connection)) { + // 'refresh' requires unique index based on all columns, so we have to check if it exists + createMaterializedViewUniqueIndexIfNotExist( + mvName + UNIQUE_INDEX_POSTFIX, mvName, columns, connection); + refreshMaterializedView(mvName, connection); + } else { + createJoinMaterializedViews(view, joinView, connection); + createMaterializedViewIndex( + mvName + "_" + viewName + INDEX_POSTFIX, mvName, viewName + "_id", connection); + createMaterializedViewIndex( + mvName + "_" + joinViewName + INDEX_POSTFIX, mvName, joinViewName + "_id", connection); + createMaterializedViewUniqueIndexIfNotExist( + mvName + UNIQUE_INDEX_POSTFIX, mvName, columns, connection); + } + connection.commit(); + log.info("Join materialized view {} create/update has finished successfully", mvName); + } catch (SQLException e) { + connection.rollback(); + throw e; + } finally { + connection.setAutoCommit(true); + } } } - private void createMaterializedViewIndex( - String indexName, String materializedViewName, String columnName, Connection connection) - throws SQLException { - var query = "CREATE INDEX %s on %s (%s)".formatted(indexName, materializedViewName, columnName); + private void refreshMaterializedView(String mvName, Connection connection) throws SQLException { + var query = "REFRESH MATERIALIZED VIEW CONCURRENTLY %s".formatted(mvName); try (var ps = connection.prepareStatement(query)) { ps.execute(); - connection.commit(); + } + } + + private void createMaterializedViewUniqueIndexIfNotExist( + String idxName, String mvName, List columns, Connection connection) throws SQLException { + if (!doesIndexExist(mvName, idxName, connection)) { + var joinedColumns = String.join(", ", columns); + var query = "CREATE UNIQUE INDEX %s ON %s (%s)".formatted(idxName, mvName, joinedColumns); + try (var ps = connection.prepareStatement(query)) { + ps.execute(); + } + } + } + + private boolean doesMaterializedViewExist(String viewName, Connection connection) throws SQLException { + var query = "SELECT EXISTS (SELECT 1 FROM pg_matviews WHERE matviewname = '%s')".formatted(viewName); + try (var ps = connection.prepareStatement(query)) { + var rs = ps.executeQuery(); + rs.next(); + return rs.getBoolean(FIRST_ROW_IDX); + } + } + + private boolean doesIndexExist(String mvName, String idxName, Connection connection) throws SQLException { + var query = "SELECT EXISTS (SELECT * FROM pg_indexes WHERE tablename = '%s' AND indexname = '%s')" + .formatted(mvName, idxName); + try (var ps = connection.prepareStatement(query)) { + var rs = ps.executeQuery(); + rs.next(); + return rs.getBoolean(FIRST_ROW_IDX); } } @@ -126,6 +196,15 @@ private void createViewMaterializedView( try (var ps = connection.prepareStatement(queryBuilder.toString())) { ps.execute(); + } + } + + private void createMaterializedViewIndex( + String indexName, String materializedViewName, String columnName, Connection connection) + throws SQLException { + var query = "CREATE INDEX %s on %s (%s)".formatted(indexName, materializedViewName, columnName); + try (var ps = connection.prepareStatement(query)) { + ps.execute(); connection.commit(); } } @@ -241,11 +320,29 @@ private void createJoinMaterializedViews( } } - private void dropMaterializedViewIfExists(String viewName, Connection connection) throws SQLException { - var query = "DROP MATERIALIZED VIEW IF EXISTS %s CASCADE".formatted(viewName); - try (var ps = connection.prepareStatement(query)) { - ps.execute(); - connection.commit(); + private List collectViewColumns(ViewsConfig.View view) { + List columns = view.columns.stream() + .filter(column -> column.type.isSet()) + .map(column -> column.name.toLowerCase()) + .toList(); + columns = new ArrayList<>(columns); + columns.add(view.name.toLowerCase() + "id"); + return columns; + } + + private List collectJoinColumns(ViewsConfig.View view, ViewsConfig.View.JoinView joinView) { + var viewTableName = view.name.toLowerCase(); + var joinedTable = configuration.viewTables.get(joinView.view).name.toLowerCase(); + var viewIdColumn = viewTableName + "_id"; + var joinIdColumn = joinedTable + "_id"; + var joinLabelColumn = joinedTable + "_label"; + var columns = new ArrayList<>(List.of(viewIdColumn, joinIdColumn, joinLabelColumn)); + for (int i = 0; i < joinView.include.size(); i++) { + var attr = joinView.include.get(i).toLowerCase(); + if (!"id".equalsIgnoreCase(attr)) { + columns.add(joinedTable + "_" + attr); + } } + return columns; } } From 8105a172302136880f8a979f49de4154b61871f0 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 12 Jul 2024 23:03:24 +0200 Subject: [PATCH 07/10] Update README.adoc for compact Jena example --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 01119e855..8e195d789 100644 --- a/README.adoc +++ b/README.adoc @@ -1773,7 +1773,7 @@ Only allowed for administrators. ==== [source, bash] ---- -curl -X GET 'http://localhost:8080/api/maintenance/compact' +curl -X POST 'http://localhost:8080/api/maintenance/compact' ---- ==== From 71333299e94c4ad7c698fa0a6f8ddca099a17525 Mon Sep 17 00:00:00 2001 From: anton Date: Mon, 15 Jul 2024 18:40:47 +0200 Subject: [PATCH 08/10] Added env var to control materialized views refresh on Saturn start --- README.adoc | 2 +- charts/fairspace/templates/project/configmap-saturn.yaml | 1 + projects/saturn/application.yaml | 1 + .../src/main/java/io/fairspace/saturn/config/Config.java | 1 + .../saturn/services/views/ViewStoreClientFactory.java | 8 +++++--- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.adoc b/README.adoc index 8e195d789..cfd08f9f4 100644 --- a/README.adoc +++ b/README.adoc @@ -3320,7 +3320,7 @@ To address this issue, materialized views are used for enhanced performance. There are two types of materialized views: one for denormalized data, which includes the view ID and attribute data of Set/TermSet types, and another for joined views. For each joined view, there is one corresponding join materialized view (as specified in the views.yaml config). -Materialized views are refreshed during database reindexing, on Saturn initialization stage and metadata updates, provided that the doViewsUpdate flag is set to true in the metadata endpoints. The refresh is performed concurrently what allows for the system to be available during the update providing the old version of data until the new one is ready. +Materialized views are refreshed during database reindexing, on Saturn initialization stage and metadata updates, provided that the doViewsUpdate flag is set to true in the metadata endpoints. The refresh is performed concurrently what allows for the system to be available during the update providing the old version of data until the new one is ready. To skip materialized views refresh on Saturn initialization stage, update the Saturn ConfigMap setting false value to `viewDatabase.mvRefreshOnStartRequired`. ==== Extra file storage diff --git a/charts/fairspace/templates/project/configmap-saturn.yaml b/charts/fairspace/templates/project/configmap-saturn.yaml index da2609cde..514ad10d9 100644 --- a/charts/fairspace/templates/project/configmap-saturn.yaml +++ b/charts/fairspace/templates/project/configmap-saturn.yaml @@ -31,6 +31,7 @@ data: blobStorePath: "/data/saturn/files/blobs" viewDatabase: enabled: true + mvRefreshOnStartRequired: true features: {{ toYaml .Values.fairspace.features | indent 6 }} {{ if has "ExtraStorage" .Values.fairspace.features }} diff --git a/projects/saturn/application.yaml b/projects/saturn/application.yaml index c08cd1cd5..9dbebe53e 100644 --- a/projects/saturn/application.yaml +++ b/projects/saturn/application.yaml @@ -55,6 +55,7 @@ viewDatabase: autoCommit: false maxPoolSize: 50 connectionTimeout: 1000 + mvRefreshOnStartRequired: true search: pageRequestTimeout: 10000 countRequestTimeout: 60000 diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/Config.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/Config.java index d770ca93a..7eb428b60 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/Config.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/Config.java @@ -115,6 +115,7 @@ public static class ViewDatabase { public int maxPoolSize = 50; public long connectionTimeout = 1000; public boolean autoCommit = false; + public boolean mvRefreshOnStartRequired = true; } public static class ExtraStorage { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java index 8ffdcce31..31bfc2a92 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java @@ -18,7 +18,6 @@ import com.zaxxer.hikari.HikariDataSource; import lombok.Builder; import lombok.Data; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import io.fairspace.saturn.config.Config; @@ -33,7 +32,6 @@ @Slf4j public class ViewStoreClientFactory { - @Getter private final MaterializedViewService materializedViewService; public ViewStoreClient build() throws SQLException { @@ -82,7 +80,11 @@ public ViewStoreClientFactory(ViewsConfig viewsConfig, Config.ViewDatabase viewD createOrUpdateView(view); } materializedViewService = new MaterializedViewService(dataSource, configuration, search.maxJoinItems); - materializedViewService.createOrUpdateAllMaterializedViews(); + if (viewDatabase.mvRefreshOnStartRequired) { + materializedViewService.createOrUpdateAllMaterializedViews(); + } else { + log.warn("Skipping materialized view refresh on start"); + } } public Connection getConnection() throws SQLException { From b9414eb2cc934dd4631121355b3779f49b380acc Mon Sep 17 00:00:00 2001 From: anton Date: Tue, 16 Jul 2024 13:17:54 +0200 Subject: [PATCH 09/10] updated README.adoc for reindex and compact --- README.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index cfd08f9f4..ea9ece9e0 100644 --- a/README.adoc +++ b/README.adoc @@ -1743,7 +1743,7 @@ Only allowed for administrators. | ``403`` | Operation not allowed. The current user is not an administrator. | ``409`` -| Reindexing is already in progress. +| Maintenance (reindexing or compacting) is already in progress. | ``503`` | Service not available. This means that the application is configured not to use a view database. |=== @@ -1762,7 +1762,7 @@ Only allowed for administrators. | ``403`` | Operation not allowed. The current user is not an administrator. | ``409`` -| Reindexing is already in progress. +| Maintenance (reindexing or compacting) is already in progress. | ``503`` | Service not available. This means that the application is configured not to use a view database. |=== From 770287947c5e684a6567176eceef55337151cd90 Mon Sep 17 00:00:00 2001 From: ewelinagr Date: Tue, 23 Jul 2024 10:30:09 +0200 Subject: [PATCH 10/10] Prepare release 1.0.1. --- README.adoc | 8 ++++---- VERSION | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.adoc b/README.adoc index ea9ece9e0..5db4c9ffb 100644 --- a/README.adoc +++ b/README.adoc @@ -2685,7 +2685,7 @@ Create DNS records for the ``keycloak.example.com``, ``fairspace.example.com`` a # List available fairspace chart versions ~/bin/helm/helm search repo --versions fairspace/fairspace # Fetch the fairspace chart -~/bin/helm/helm pull fairspace/fairspace --version 1.0.0 +~/bin/helm/helm pull fairspace/fairspace --version 1.0.1 ---- ===== Deploy Keycloak @@ -2773,7 +2773,7 @@ Create a new deployment (called _release_ in helm terminology) and install the Fairspace chart: [source, shell] ---- -~/bin/helm/helm install fairspace-new fairspace/fairspace --version 1.0.0 --namespace fairspace-new \ +~/bin/helm/helm install fairspace-new fairspace/fairspace --version 1.0.1 --namespace fairspace-new \ -f /path/to/values.yaml --set-file saturn.vocabulary=/path/to/vocabulary.ttl --set-file saturn.views=/path/to/views.yaml ---- You can pass values files with ``-f`` and provide a file for a specified @@ -2870,7 +2870,7 @@ Additionally, to include custom icons for `fairspace.icons` option, you need to [source, shell] ---- -~/bin/helm/helm install fairspace-new fairspace/fairspace --version 1.0.0 --namespace fairspace-new \ +~/bin/helm/helm install fairspace-new fairspace/fairspace --version 1.0.1 --namespace fairspace-new \ -f /path/to/values.yaml --set-file saturn.vocabulary=/path/to/vocabulary.ttl --set-file saturn.views=/path/to/views.yaml --set-file svgicons.extra-icon=/path/to/extra-icon.svg ---- @@ -2926,7 +2926,7 @@ jupyterhub: To update a deployment using a new chart: [source, shell] ---- -~/bin/helm/helm upgrade fairspace-new fairspace-1.0.0.tgz +~/bin/helm/helm upgrade fairspace-new fairspace-1.0.1.tgz ---- With ``helm upgrade`` you can also pass new values files with ``-f`` and pass files with ``--set-file`` as for ``helm install``. diff --git a/VERSION b/VERSION index 3eefcb9dd..7dea76edb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.0.1