diff --git a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/StatisticsManager.scala b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/StatisticsManager.scala index 8ae0419f0a3..c34595cf3bd 100644 --- a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/StatisticsManager.scala +++ b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/worker/managers/StatisticsManager.scala @@ -30,11 +30,11 @@ import org.apache.texera.amber.engine.architecture.worker.statistics.{ import scala.collection.mutable class StatisticsManager { - // DataProcessor + // Plain maps (no withDefaultValue) so they survive Kryo round-trip. private val inputStatistics: mutable.Map[PortIdentity, (Long, Long)] = - mutable.Map.empty.withDefaultValue((0L, 0L)) + mutable.Map.empty private val outputStatistics: mutable.Map[PortIdentity, (Long, Long)] = - mutable.Map.empty.withDefaultValue((0L, 0L)) + mutable.Map.empty private var dataProcessingTime: Long = 0L private var totalExecutionTime: Long = 0L private var workerStartTime: Long = 0L @@ -82,8 +82,10 @@ class StatisticsManager { */ def increaseInputStatistics(portId: PortIdentity, size: Long): Unit = { require(size >= 0, "Tuple size must be non-negative") - val (count, totalSize) = inputStatistics(portId) - inputStatistics.update(portId, (count + 1, totalSize + size)) + inputStatistics.updateWith(portId) { + case Some((count, totalSize)) => Some((count + 1, totalSize + size)) + case None => Some((1L, size)) + } } /** @@ -93,8 +95,10 @@ class StatisticsManager { */ def increaseOutputStatistics(portId: PortIdentity, size: Long): Unit = { require(size >= 0, "Tuple size must be non-negative") - val (count, totalSize) = outputStatistics(portId) - outputStatistics.update(portId, (count + 1, totalSize + size)) + outputStatistics.updateWith(portId) { + case Some((count, totalSize)) => Some((count + 1, totalSize + size)) + case None => Some((1L, size)) + } } /** diff --git a/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala b/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala index af9a26286d4..c49f63e9394 100644 --- a/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala +++ b/amber/src/main/scala/org/apache/texera/web/service/WorkflowEmailNotifier.scala @@ -107,7 +107,7 @@ class WorkflowEmailNotifier( private def createDashboardUrl(): String = { val host = sessionUri.getHost val port = sessionUri.getPort - val path = s"/dashboard/user/workspace/$workflowId" + val path = s"/user/workspace/$workflowId" if (port == -1 || port == 80 || port == 443) { s"http://$host$path" } else { diff --git a/amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/managers/WorkerManagersSpec.scala b/amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/managers/WorkerManagersSpec.scala index 3fbff39148c..1932823f5dc 100644 --- a/amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/managers/WorkerManagersSpec.scala +++ b/amber/src/test/scala/org/apache/texera/amber/engine/architecture/worker/managers/WorkerManagersSpec.scala @@ -76,11 +76,15 @@ class WorkerManagersSpec extends AnyFlatSpec { val sm = new StatisticsManager() sm.increaseOutputStatistics(PortIdentity(0), 30) sm.increaseOutputStatistics(PortIdentity(0), 70) - assert(sm.getOutputTupleCount == 2L) - val out = sm.getStatistics(nullExec).outputTupleMetrics - assert(out.size == 1) - assert(out.head.tupleMetrics.count == 2L) - assert(out.head.tupleMetrics.size == 100L) + sm.increaseOutputStatistics(PortIdentity(1), 25) + assert(sm.getOutputTupleCount == 3L) + val byPort = sm + .getStatistics(nullExec) + .outputTupleMetrics + .map(m => m.portId -> (m.tupleMetrics.count, m.tupleMetrics.size)) + .toMap + assert(byPort(PortIdentity(0)) == (2L, 100L)) + assert(byPort(PortIdentity(1)) == (1L, 25L)) } it should "reject negative tuple sizes" in { diff --git a/amber/src/test/scala/org/apache/texera/amber/engine/faulttolerance/CheckpointSpec.scala b/amber/src/test/scala/org/apache/texera/amber/engine/faulttolerance/CheckpointSpec.scala index fbc7e8044df..3d207fd23b3 100644 --- a/amber/src/test/scala/org/apache/texera/amber/engine/faulttolerance/CheckpointSpec.scala +++ b/amber/src/test/scala/org/apache/texera/amber/engine/faulttolerance/CheckpointSpec.scala @@ -63,7 +63,7 @@ class CheckpointSpec extends AnyFlatSpecLike with BeforeAndAfterAll { system.actorOf(Props[SingleNodeListener](), "cluster-info") } - "Default controller state" should "be serializable" in { + "Default controller state" should "round-trip through CheckpointState" in { val cp = new ControllerProcessor( workflow.context, @@ -73,9 +73,11 @@ class CheckpointSpec extends AnyFlatSpecLike with BeforeAndAfterAll { ) val chkpt = new CheckpointState() chkpt.save(CP_STATE_KEY, cp) + val restored: ControllerProcessor = chkpt.load(CP_STATE_KEY) + assert(restored.actorId == cp.actorId) } - "Default worker state" should "be serializable" in { + "Default worker state" should "round-trip through CheckpointState" in { val dp = new DataProcessor( SELF, msg => {}, @@ -83,6 +85,8 @@ class CheckpointSpec extends AnyFlatSpecLike with BeforeAndAfterAll { ) val chkpt = new CheckpointState() chkpt.save(DP_STATE_KEY, dp) + val restored: DataProcessor = chkpt.load(DP_STATE_KEY) + assert(restored.actorId == dp.actorId) } "CheckpointState" should "fail loudly on an unknown key" in { diff --git a/frontend/git-version.js b/frontend/build-version.js similarity index 82% rename from frontend/git-version.js rename to frontend/build-version.js index 54cfdf8d3f5..97a3bbd1c84 100644 --- a/frontend/git-version.js +++ b/frontend/build-version.js @@ -17,17 +17,12 @@ * under the License. */ -const { gitDescribeSync } = require("git-describe"); +const { generate } = require("build-number-generator"); const { version } = require("./package.json"); const { resolve, relative } = require("path"); const { writeFileSync, existsSync, mkdirSync } = require("fs-extra"); -const gitInfo = gitDescribeSync({ - dirtyMark: false, - dirtySemver: false, -}); - -gitInfo.version = version; +const buildNumber = generate(version); if (!existsSync(__dirname + "/src/environments")) { mkdirSync(__dirname + "/src/environments"); @@ -37,10 +32,13 @@ writeFileSync( file, `// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ -export const Version = ${JSON.stringify(gitInfo, null, 4)}; +export const Version = { + "buildNumber": ${JSON.stringify(buildNumber)}, + "version": ${JSON.stringify(version)} +}; /* tslint:enable */ `, { encoding: "utf-8" } ); -console.log(`Wrote version info ${gitInfo.raw} to ${relative(resolve(__dirname, ".."), file)}`); +console.log(`Wrote build number ${buildNumber} to ${relative(resolve(__dirname, ".."), file)}`); diff --git a/frontend/package.json b/frontend/package.json index 481e720d976..6075a8dec84 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,7 @@ "eslint:fix": "yarn eslint --fix ./src", "format:fix": "yarn prettier-eslint --write \"src/**/*.{ts,js,html,scss,less,json}\"", "format:ci": "yarn prettier-eslint --list-different \"src/**/*.{ts,js,html,scss,less,json}\" && yarn eslint ./src", - "postinstall": "node git-version.js" + "postinstall": "node build-version.js" }, "private": true, "dependencies": { @@ -122,12 +122,12 @@ "@vitest/browser": "4.1.5", "@vitest/browser-playwright": "4.1.5", "@vitest/coverage-v8": "4.1.5", + "build-number-generator": "3.1.0", "concurrently": "7.4.0", "eslint": "8.57.0", "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "fs-extra": "10.0.1", - "git-describe": "4.1.0", "jsdom": "25.0.1", "nodecat": "2.0.0", "nx": "22.7.0", diff --git a/frontend/src/app/app-routing.constant.ts b/frontend/src/app/app-routing.constant.ts index 4181df8a954..d3aca2c823f 100644 --- a/frontend/src/app/app-routing.constant.ts +++ b/frontend/src/app/app-routing.constant.ts @@ -17,7 +17,7 @@ * under the License. */ -export const DASHBOARD = "/dashboard"; +export const DASHBOARD = ""; export const DASHBOARD_HOME = `${DASHBOARD}/home`; export const DASHBOARD_ABOUT = `${DASHBOARD}/about`; diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 179caf5c088..4e3e68016d8 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -17,8 +17,8 @@ * under the License. */ -import { inject, NgModule } from "@angular/core"; -import { CanActivateFn, Router, RouterModule, Routes } from "@angular/router"; +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; import { DashboardComponent } from "./dashboard/component/dashboard.component"; import { UserWorkflowComponent } from "./dashboard/component/user/user-workflow/user-workflow.component"; import { UserQuotaComponent } from "./dashboard/component/user/user-quota/user-quota.component"; @@ -38,28 +38,21 @@ import { DatasetDetailComponent } from "./dashboard/component/user/user-dataset/ import { UserDatasetComponent } from "./dashboard/component/user/user-dataset/user-dataset.component"; import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component"; import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component"; -import { DASHBOARD_ABOUT, DASHBOARD_USER_WORKFLOW } from "./app-routing.constant"; +import { DASHBOARD_USER_WORKFLOW } from "./app-routing.constant"; import { HubSearchResultComponent } from "./hub/component/hub-search-result/hub-search-result.component"; import { AdminSettingsComponent } from "./dashboard/component/admin/settings/admin-settings.component"; -import { GuiConfigService } from "./common/service/gui-config.service"; - -const rootRedirectGuard: CanActivateFn = () => { - const config = inject(GuiConfigService); - const router = inject(Router); - try { - return router.parseUrl(DASHBOARD_ABOUT); - } catch { - // config not loaded yet, swallow the error and let the app handle it - } - return true; -}; const routes: Routes = []; routes.push({ - path: "dashboard", + path: "", component: DashboardComponent, children: [ + { + path: "", + redirectTo: "about", + pathMatch: "full", + }, { path: "home", component: LandingPageComponent, @@ -174,14 +167,6 @@ routes.push({ ], }); -// default route renders the workspace editor directly; if userSystem is enabled at runtime, -// AppComponent will navigate to DASHBOARD_ABOUT instead. -routes.push({ - path: "", - component: WorkspaceComponent, - canActivate: [rootRedirectGuard], -}); - // redirect all other paths to index. routes.push({ path: "**", diff --git a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html index 41a2ceb9656..907ffa8d727 100644 --- a/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html +++ b/frontend/src/app/dashboard/component/admin/execution/admin-execution.component.html @@ -100,7 +100,7 @@