This is an automated email from the ASF dual-hosted git repository. chanholee pushed a commit to branch branch-0.12 in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/branch-0.12 by this push: new 690a92befb [ZEPPELIN-6216] Show clear message when job manager is disabled instead of infinite loading 690a92befb is described below commit 690a92befbb6457101fca98528dd01ad09c413fa Author: Yeonhee Hayden Kim <devye...@gmail.com> AuthorDate: Thu Aug 28 21:11:26 2025 +0900 [ZEPPELIN-6216] Show clear message when job manager is disabled instead of infinite loading > This PR is a rebased version of [#5022](https://github.com/apache/zeppelin/pull/5022) > The previous PR was automatically closed because the branch was deleted, so I recreated the branch and opened this new PR. ### What is this PR for? This PR fixes the issue that occurs when `zeppelin.jobmanager.enable` is set to `false`. In this case, the server sends no response, causing the Job Manager page in both classic and new UIs to show an infinite loading indicator. This behavior confuses users and gives the impression that the page is broken. To improve user experience, this PR introduces explicit server-side handling that returns an explicit "not allowed" response when the Job Manager is disabled via configuration. Additionally, both classic and new UIs have been updated to properly handle this response and display a user-friendly message: **"Job Manager is disabled in the current configuration."** This change ensures users are informed about the disabled state instead of encountering an endless loading screen. ### What type of PR is it? Improvement ### Todos * [x] Implement server-side forbidden response when Job Manager is disabled * [x] Add `JOB_MANAGER_DISABLED` WebSocket message and corresponding handling in both Classic and New UI * [x] Add unit and integration tests covering the new behavior **Note:** * Although REST API tests have been implemented, the current frontend (both Classic and New UI) interacts with the Job Manager primarily through WebSocket communication. Therefore, the functional verification and actual handling of the "Job Manager disabled" scenario have been focused on the WebSocket path. The REST API tests are prepared to ensure future compatibility if the REST endpoints are used. * In the user-request handling method, a ForbiddenException triggers sending a clear error message to the client so the UI can inform the user that the Job Manager is disabled. However, during broadcast events where no direct client request is involved, the exception is logged at debug level and silently skipped to avoid unnecessary noise or client interruptions. ### What is the Jira issue? [[ZEPPELIN-6216]](https://issues.apache.org/jira/browse/ZEPPELIN-6216) ### How should this be tested? * Automated tests have been added to cover the new behavior when the Job Manager is disabled: * Unit tests in `JobManagerServiceTest` verify that forbidden exceptions are thrown. * REST API tests in `NotebookRestApiTest` check that HTTP 403 responses with appropriate messages are returned. * WebSocket and server event handling tests in `NotebookServerTest` ensure no unexpected exceptions occur and proper notification messages are sent. * Manual testing steps: 1. Set `zeppelin.jobmanager.enable` to `false` via `ZeppelinConfiguration`. 2. Access the Job Manager page in both classic and new UI. 3. Confirm that instead of infinite loading, a clear message stating **"Job Manager is disabled in the current configuration."** is displayed. ### Screenshots (if appropriate) * AS-IS (New UI) <img width="862" height="362" alt="AS-IS NEW UI" src="https://github.com/user-attachments/assets/e2a1288f-5952-4ea0-a31b-a267f49f171e" /> * AS-IS (Classic UI) <img width="864" height="424" alt="AS-IS CLASSIC UI" src="https://github.com/user-attachments/assets/bc111aff-e95c-4427-a266-af7f9f600407" /> * TO-BE (New UI) <img width="870" height="305" alt="TO-BE NEW UI" src="https://github.com/user-attachments/assets/0555e5dc-2ee8-4000-b1eb-c7d2259e96b1" /> * TO-BE (Classic UI) <img width="875" height="275" alt="TO-BE CLASSIC UI" src="https://github.com/user-attachments/assets/e0a87e98-fde8-467b-9df3-d9ada39deb36" /> ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? No * Does this needs documentation? No Closes #5035 from devyeony/feature/ZEPPELIN-6216. Signed-off-by: ChanHo Lee <chanho...@apache.org> (cherry picked from commit fbd94da456288b50b1290e233ada41879e9e62aa) Signed-off-by: ChanHo Lee <chanho...@apache.org> --- .../java/org/apache/zeppelin/common/Message.java | 1 + .../org/apache/zeppelin/rest/NotebookRestApi.java | 11 ++ .../apache/zeppelin/service/JobManagerService.java | 26 ++++- .../exception/JobManagerForbiddenException.java | 35 ++++++ .../org/apache/zeppelin/socket/NotebookServer.java | 24 +++- .../apache/zeppelin/rest/NotebookRestApiTest.java | 45 ++++++++ .../zeppelin/service/JobManagerServiceTest.java | 114 +++++++++++++++++++ .../apache/zeppelin/socket/NotebookServerTest.java | 121 +++++++++++++++++++++ .../interfaces/message-data-type-map.interface.ts | 3 +- .../src/interfaces/message-job.interface.ts | 4 + .../src/interfaces/message-operator.interface.ts | 6 + .../job-manager/job-manager.component.html | 36 +++--- .../workspace/job-manager/job-manager.component.ts | 12 +- .../workspace/job-manager/job-manager.module.ts | 4 +- .../src/app/jobmanager/jobmanager.component.js | 10 +- zeppelin-web/src/app/jobmanager/jobmanager.html | 50 +++++---- .../websocket/websocket-event.factory.js | 2 + 17 files changed, 458 insertions(+), 46 deletions(-) diff --git a/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java b/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java index c561e745d0..82c5b54f88 100644 --- a/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java +++ b/zeppelin-common/src/main/java/org/apache/zeppelin/common/Message.java @@ -176,6 +176,7 @@ public class Message implements JsonSerializable { LIST_NOTE_JOBS, // [c-s] get note job management information LIST_UPDATE_NOTE_JOBS, // [c-s] get job management information for until unixtime UNSUBSCRIBE_UPDATE_NOTE_JOBS, // [c-s] unsubscribe job information for job management + JOB_MANAGER_DISABLED, // [s-c] send when job manager is disabled // @param unixTime GET_INTERPRETER_BINDINGS, // [c-s] get interpreter bindings SAVE_INTERPRETER_BINDINGS, // [c-s] save interpreter bindings diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java index db5275f76f..c0b4a115c7 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java @@ -59,6 +59,7 @@ import org.apache.zeppelin.service.AuthenticationService; import org.apache.zeppelin.service.JobManagerService; import org.apache.zeppelin.service.NotebookService; import org.apache.zeppelin.service.ServiceContext; +import org.apache.zeppelin.service.exception.JobManagerForbiddenException; import org.apache.zeppelin.socket.NotebookServer; import org.apache.zeppelin.user.AuthenticationInfo; import org.quartz.CronExpression; @@ -221,6 +222,14 @@ public class NotebookRestApi extends AbstractRestApi { } } + private void checkIfJobManagerIsEnabled() { + try { + jobManagerService.checkIfJobManagerIsEnabled(); + } catch (JobManagerForbiddenException e) { + throw new ForbiddenException(e.getMessage()); + } + } + /** * Get notebook capabilities. */ @@ -1159,6 +1168,7 @@ public class NotebookRestApi extends AbstractRestApi { @ZeppelinApi public Response getJobListforNote() throws IOException, IllegalArgumentException { LOGGER.info("Get note jobs for job manager"); + checkIfJobManagerIsEnabled(); List<JobManagerService.NoteJobInfo> noteJobs = jobManagerService .getNoteJobInfoByUnixTime(0, getServiceContext(), new RestServiceCallback<>()); Map<String, Object> response = new HashMap<>(); @@ -1182,6 +1192,7 @@ public class NotebookRestApi extends AbstractRestApi { public Response getUpdatedJobListforNote(@PathParam("lastUpdateUnixtime") long lastUpdateUnixTime) throws IOException, IllegalArgumentException { LOGGER.info("Get updated note jobs lastUpdateTime {}", lastUpdateUnixTime); + checkIfJobManagerIsEnabled(); List<JobManagerService.NoteJobInfo> noteJobs = jobManagerService.getNoteJobInfoByUnixTime(lastUpdateUnixTime, getServiceContext(), new RestServiceCallback<>()); diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/service/JobManagerService.java b/zeppelin-server/src/main/java/org/apache/zeppelin/service/JobManagerService.java index 6a06d06680..6b65ef58d6 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/service/JobManagerService.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/service/JobManagerService.java @@ -18,6 +18,7 @@ package org.apache.zeppelin.service; import jakarta.inject.Inject; +import java.util.Collections; import org.apache.commons.lang3.StringUtils; import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.notebook.AuthorizationService; @@ -26,6 +27,7 @@ import org.apache.zeppelin.notebook.NoteInfo; import org.apache.zeppelin.notebook.Notebook; import org.apache.zeppelin.notebook.Paragraph; import org.apache.zeppelin.scheduler.Job; +import org.apache.zeppelin.service.exception.JobManagerForbiddenException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,12 +57,26 @@ public class JobManagerService { this.zConf = zConf; } + public void checkIfJobManagerIsEnabled() throws JobManagerForbiddenException { + if (!zConf.isJobManagerEnabled()) { + throw new JobManagerForbiddenException(); + } + } + + private boolean isJobManagerDisabled(ServiceContext context, ServiceCallback<?> callback) throws IOException { + if (!zConf.isJobManagerEnabled()) { + callback.onFailure(new JobManagerForbiddenException(), context); + return true; + } + return false; + } + public List<NoteJobInfo> getNoteJobInfo(String noteId, ServiceContext context, ServiceCallback<List<NoteJobInfo>> callback) throws IOException { - if (!zConf.isJobManagerEnabled()) { - return new ArrayList<>(); + if (isJobManagerDisabled(context, callback)) { + return Collections.emptyList(); } return notebook.processNote(noteId, @@ -83,8 +99,8 @@ public class JobManagerService { ServiceContext context, ServiceCallback<List<NoteJobInfo>> callback) throws IOException { - if (!zConf.isJobManagerEnabled()) { - return new ArrayList<>(); + if (isJobManagerDisabled(context, callback)) { + return Collections.emptyList(); } List<NoteJobInfo> notesJobInfo = new LinkedList<>(); @@ -103,7 +119,7 @@ public class JobManagerService { public void removeNoteJobInfo(String noteId, ServiceContext context, ServiceCallback<List<NoteJobInfo>> callback) throws IOException { - if (!zConf.isJobManagerEnabled()) { + if (isJobManagerDisabled(context, callback)) { return; } List<NoteJobInfo> notesJobInfo = new ArrayList<>(); diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/service/exception/JobManagerForbiddenException.java b/zeppelin-server/src/main/java/org/apache/zeppelin/service/exception/JobManagerForbiddenException.java new file mode 100644 index 0000000000..a70da14eb9 --- /dev/null +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/service/exception/JobManagerForbiddenException.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zeppelin.service.exception; + +/** + * Runtime exception thrown when the job manager is disabled. + */ +public class JobManagerForbiddenException extends Exception { + + private static final long serialVersionUID = -8872599278254399427L; + public static final String MESSAGE = "Job Manager is disabled in the current configuration."; + + public JobManagerForbiddenException() { + super(MESSAGE); + } + + public JobManagerForbiddenException(String message) { + super(message); + } +} diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index 8edb096e3f..d0d0af683c 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -91,6 +91,7 @@ import org.apache.zeppelin.service.JobManagerService; import org.apache.zeppelin.service.NotebookService; import org.apache.zeppelin.service.ServiceContext; import org.apache.zeppelin.service.SimpleServiceCallback; +import org.apache.zeppelin.service.exception.JobManagerForbiddenException; import org.apache.zeppelin.ticket.TicketContainer; import org.apache.zeppelin.types.InterpreterSettingsList; import org.apache.zeppelin.user.AuthenticationInfo; @@ -563,7 +564,13 @@ public class NotebookServer implements AngularObjectRegistryListener, @Override public void onFailure(Exception ex, ServiceContext context) throws IOException { - LOGGER.warn(ex.getMessage()); + if (ex instanceof JobManagerForbiddenException) { + LOGGER.info("Job Manager is disabled. Rejecting request from user: {}", + context.getAutheInfo().getUser()); + conn.send(serializeMessage(new Message(OP.JOB_MANAGER_DISABLED).put("errorMessage", ex.getMessage()))); + } else { + LOGGER.warn(ex.getMessage()); + } } }); } @@ -585,7 +592,11 @@ public class NotebookServer implements AngularObjectRegistryListener, @Override public void onFailure(Exception ex, ServiceContext context) throws IOException { - LOGGER.warn(ex.getMessage()); + if (ex instanceof JobManagerForbiddenException) { + LOGGER.debug(ex.getMessage()); + } else { + LOGGER.warn(ex.getMessage()); + } } }); } @@ -1932,6 +1943,15 @@ public class NotebookServer implements AngularObjectRegistryListener, connectionManager.broadcast(JobManagerServiceType.JOB_MANAGER_PAGE.getKey(), new Message(OP.LIST_UPDATE_NOTE_JOBS).put("noteRunningJobs", response)); } + + @Override + public void onFailure(Exception ex, ServiceContext context) throws IOException { + if (ex instanceof JobManagerForbiddenException) { + LOGGER.debug(ex.getMessage()); + } else { + super.onFailure(ex, context); + } + } } @Override diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java index 5a97b3db81..5cf4adbf5b 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/NotebookRestApiTest.java @@ -19,6 +19,8 @@ package org.apache.zeppelin.rest; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import jakarta.ws.rs.core.Response; +import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.interpreter.InterpreterSetting; import org.apache.zeppelin.interpreter.InterpreterSettingManager; import org.apache.zeppelin.notebook.Notebook; @@ -1187,4 +1189,47 @@ class NotebookRestApiTest extends AbstractTestRestApi { } } } + + @Test + void testGetJobList_whenJobManagerDisabled() throws IOException { + assertJobManagerDisabledResponse("/notebook/jobmanager/"); + } + + @Test + void testGetUpdatedJobList_whenJobManagerDisabled() throws IOException { + assertJobManagerDisabledResponse("/notebook/jobmanager/12345/"); + } + + private void assertJobManagerDisabledResponse(String url) throws IOException { + boolean originalFlag = disableJobManagerAndBackupFlag(); + String expectedErrorMessage = "Job Manager is disabled in the current configuration."; + + try (CloseableHttpResponse response = httpGet(url)) { + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), + response.getStatusLine().getStatusCode(), + "Response status should be 403 Forbidden"); + + String jsonResponse = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + Map<String, Object> parsedResponse = gson.fromJson(jsonResponse, + new TypeToken<Map<String, Object>>() {}.getType()); + + assertEquals("FORBIDDEN", parsedResponse.get("status")); + assertEquals(expectedErrorMessage, parsedResponse.get("message")); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + + private boolean disableJobManagerAndBackupFlag() { + ZeppelinConfiguration zConf = zepServer.getZeppelinConfiguration(); + boolean originalFlag = zConf.isJobManagerEnabled(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_JOBMANAGER_ENABLE.getVarName(), "false"); + return originalFlag; + } + + private void restoreJobManagerFlag(boolean originalFlag) { + ZeppelinConfiguration zConf = zepServer.getZeppelinConfiguration(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_JOBMANAGER_ENABLE.getVarName(), + String.valueOf(originalFlag)); + } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/service/JobManagerServiceTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/service/JobManagerServiceTest.java new file mode 100644 index 0000000000..e8d69d604b --- /dev/null +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/service/JobManagerServiceTest.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.zeppelin.service; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.List; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.notebook.AuthorizationService; +import org.apache.zeppelin.notebook.Notebook; +import org.apache.zeppelin.service.JobManagerService.NoteJobInfo; +import org.apache.zeppelin.service.exception.JobManagerForbiddenException; +import org.apache.zeppelin.user.AuthenticationInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class JobManagerServiceTest { + + private ZeppelinConfiguration zConf; + private Notebook mockNotebook; + private AuthorizationService mockAuthorizationService; + private JobManagerService jobManagerService; + private ServiceContext serviceContext; + + @BeforeEach + public void setUp() { + zConf = mock(ZeppelinConfiguration.class); + mockNotebook = mock(Notebook.class); + mockAuthorizationService = mock(AuthorizationService.class); + jobManagerService = new JobManagerService(mockNotebook, mockAuthorizationService, zConf); + serviceContext = new ServiceContext(new AuthenticationInfo("test-user"), null); + } + + @Nested + class WhenJobManagerIsDisabled { + + @BeforeEach + void disableJobManager() { + when(zConf.isJobManagerEnabled()).thenReturn(false); + } + + @Test + void checkIfJobManagerIsEnabled_throwsException() { + assertThrows(JobManagerForbiddenException.class, () -> jobManagerService.checkIfJobManagerIsEnabled()); + } + + @Test + void getNoteJobInfo_returnsEmptyList_andCallsCallback() throws IOException { + @SuppressWarnings("unchecked") + ServiceCallback<List<NoteJobInfo>> callback = mock(ServiceCallback.class); + List<NoteJobInfo> result = jobManagerService.getNoteJobInfo( + "some_note_id", + serviceContext, + callback + ); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + verify(callback).onFailure(any(JobManagerForbiddenException.class), eq(serviceContext)); + } + + @Test + void getNoteJobInfoByUnixTime_returnsEmptyList() throws IOException { + ServiceCallback<List<NoteJobInfo>> callback = new SimpleServiceCallback<>(); + List<NoteJobInfo> result = jobManagerService.getNoteJobInfoByUnixTime( + 0, + serviceContext, + callback + ); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + void removeNoteJobInfo_doesNothing() { + ServiceCallback<List<NoteJobInfo>> callback = new SimpleServiceCallback<>(); + assertDoesNotThrow(() -> + jobManagerService.removeNoteJobInfo( + "some_note_id", + serviceContext, + callback + ) + ); + } + } + +} diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java index 3eaf74d683..d982d46a33 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java @@ -16,6 +16,7 @@ */ package org.apache.zeppelin.socket; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -129,6 +130,113 @@ class NotebookServerTest extends AbstractTestRestApi { assertFalse(notebookServer.checkOrigin("http://evillocalhost:8080")); } + @Test + void testUnicastNoteJobInfo_whenJobManagerDisabled() throws IOException { + boolean originalFlag = disableJobManagerAndBackupFlag(); + NotebookSocket conn = createWebSocket(); + ServiceContext context = new ServiceContext(anonymous, new HashSet<>()); + Message fromMessage = new Message(OP.LIST_NOTE_JOBS); + + try { + notebookServer.unicastNoteJobInfo(conn, context, fromMessage); + String expectedErrorMessage = "Job Manager is disabled in the current configuration."; + Message expectedMessage = new Message(OP.JOB_MANAGER_DISABLED).put("errorMessage", expectedErrorMessage); + verify(conn, times(1)).send(eq(notebookServer.serializeMessage(expectedMessage))); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + + @Test + void testBroadcastUpdateNoteJobInfo_whenJobManagerDisabled() { + boolean originalFlag = disableJobManagerAndBackupFlag(); + Note mockNote = mock(Note.class, RETURNS_DEEP_STUBS); + when(mockNote.getId()).thenReturn("testNoteId"); + + try { + assertDoesNotThrow(() -> { + notebookServer.broadcastUpdateNoteJobInfo(mockNote, System.currentTimeMillis()); + }, "broadcastUpdateNoteJobInfo should not throw exception when job manager is disabled"); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + + @Test + void testOnParagraphRemove_whenJobManagerDisabled() { + boolean originalFlag = disableJobManagerAndBackupFlag(); + Paragraph mockParagraph = mock(Paragraph.class, RETURNS_DEEP_STUBS); + when(mockParagraph.getNote().getId()).thenReturn("testNoteId"); + + try { + assertDoesNotThrow(() -> { + notebookServer.onParagraphRemove(mockParagraph); + }, "onParagraphRemove should not throw exception when job manager is disabled"); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + + @Test + void testOnNoteRemove_whenJobManagerDisabled() { + boolean originalFlag = disableJobManagerAndBackupFlag(); + Note mockNote = mock(Note.class, RETURNS_DEEP_STUBS); + when(mockNote.getId()).thenReturn("testNoteId"); + + try { + assertDoesNotThrow(() -> { + notebookServer.onNoteRemove(mockNote, anonymous); + }, "onNoteRemove should not throw exception when job manager is disabled"); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + + @Test + void testOnParagraphCreate_whenJobManagerDisabled() { + boolean originalFlag = disableJobManagerAndBackupFlag(); + Paragraph mockParagraph = mock(Paragraph.class, RETURNS_DEEP_STUBS); + when(mockParagraph.getNote().getId()).thenReturn("testNoteId"); + + try { + assertDoesNotThrow(() -> { + notebookServer.onParagraphCreate(mockParagraph); + }, "onParagraphCreate should not throw exception when job manager is disabled"); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + + @Test + void testOnNoteCreate_whenJobManagerDisabled() { + boolean originalFlag = disableJobManagerAndBackupFlag(); + Note mockNote = mock(Note.class, RETURNS_DEEP_STUBS); + when(mockNote.getId()).thenReturn("testNoteId"); + + try { + assertDoesNotThrow(() -> { + notebookServer.onNoteCreate(mockNote, anonymous); + }, "onNoteCreate should not throw exception when job manager is disabled"); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + + @Test + void testOnParagraphStatusChange_whenJobManagerDisabled() { + boolean originalFlag = disableJobManagerAndBackupFlag(); + Paragraph mockParagraph = mock(Paragraph.class, RETURNS_DEEP_STUBS); + when(mockParagraph.getNote().getId()).thenReturn("testNoteId"); + + try { + assertDoesNotThrow(() -> { + notebookServer.onParagraphStatusChange(mockParagraph, Status.RUNNING); + }, "onParagraphStatusChange should not throw exception when job manager is disabled"); + } finally { + restoreJobManagerFlag(originalFlag); + } + } + @Test void testCollaborativeEditing() throws IOException { if (!zepServer.getZeppelinConfiguration().isZeppelinNotebookCollaborativeModeEnable()) { @@ -860,4 +968,17 @@ class NotebookServerTest extends AbstractTestRestApi { NotebookSocket sock = mock(NotebookSocket.class); return sock; } + + private boolean disableJobManagerAndBackupFlag() { + ZeppelinConfiguration zConf = zepServer.getZeppelinConfiguration(); + boolean originalFlag = zConf.isJobManagerEnabled(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_JOBMANAGER_ENABLE.getVarName(), "false"); + return originalFlag; + } + + private void restoreJobManagerFlag(boolean originalFlag) { + ZeppelinConfiguration zConf = zepServer.getZeppelinConfiguration(); + zConf.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_JOBMANAGER_ENABLE.getVarName(), + String.valueOf(originalFlag)); + } } diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts index 731b349bac..e832a1e8f9 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-data-type-map.interface.ts @@ -76,7 +76,7 @@ import { RunParagraph } from './message-paragraph.interface'; -import { ListNoteJobs, ListUpdateNoteJobs } from './message-job.interface'; +import { JobManagerDisabled, ListNoteJobs, ListUpdateNoteJobs } from './message-job.interface'; import { InterpreterBindings, InterpreterSetting } from './message-interpreter.interface'; import { OP } from './message-operator.interface'; @@ -92,6 +92,7 @@ export interface MessageReceiveDataTypeMap { [OP.ERROR_INFO]: ErrorInfo; [OP.LIST_NOTE_JOBS]: ListNoteJobs; [OP.LIST_UPDATE_NOTE_JOBS]: ListUpdateNoteJobs; + [OP.JOB_MANAGER_DISABLED]: JobManagerDisabled; [OP.INTERPRETER_SETTINGS]: InterpreterSetting; [OP.LIST_REVISION_HISTORY]: ListRevision; [OP.INTERPRETER_BINDINGS]: InterpreterBindings; diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts index c59122b3a3..2a9a973b4e 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-job.interface.ts @@ -18,6 +18,10 @@ export interface ListUpdateNoteJobs { noteRunningJobs: NoteJobs; } +export interface JobManagerDisabled { + errorMessage: string; +} + export interface NoteJobs { lastResponseUnixTime: number; jobs: JobsItem[]; diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts index 0affb79c71..cc10753cff 100644 --- a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts +++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-operator.interface.ts @@ -360,6 +360,12 @@ export enum OP { */ UNSUBSCRIBE_UPDATE_NOTE_JOBS = 'UNSUBSCRIBE_UPDATE_NOTE_JOBS', + /** + * [s-c] + * send when job manager is disabled + */ + JOB_MANAGER_DISABLED = 'JOB_MANAGER_DISABLED', + /** * [c-s] * get interpreter bindings diff --git a/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.html b/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.html index beb4b956ab..0419cb13c0 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.html +++ b/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.html @@ -60,18 +60,28 @@ </form> </zeppelin-page-header> <div class="content"> - <nz-card *ngIf="loading; else jobs"> - <nz-skeleton [nzTitle]="false" [nzLoading]="true" [nzActive]="true"></nz-skeleton> - </nz-card> + <ng-container [ngSwitch]="status"> + <nz-card *ngSwitchCase="'loading'"> + <nz-skeleton [nzTitle]="false" [nzLoading]="true" [nzActive]="true"></nz-skeleton> + </nz-card> - <ng-template #jobs> - <zeppelin-job-manager-job - *ngFor="let item of filteredJobs" - [note]="item" - [highlight]="filterString" - (start)="onStart($event)" - (stop)="onStop($event)" - ></zeppelin-job-manager-job> - <nz-empty *ngIf="filteredJobs.length === 0" nzNotFoundContent="No Job found"></nz-empty> - </ng-template> + <ng-container *ngSwitchCase="'success'"> + <zeppelin-job-manager-job + *ngFor="let item of filteredJobs" + [note]="item" + [highlight]="filterString" + (start)="onStart($event)" + (stop)="onStop($event)" + ></zeppelin-job-manager-job> + <nz-empty *ngIf="filteredJobs.length === 0" nzNotFoundContent="No Job found"></nz-empty> + </ng-container> + + <ng-container *ngSwitchCase="'disabled'"> + <nz-alert + nzType="info" + nzMessage="Job Manager is disabled in the current configuration." + [nzShowIcon]="true" + ></nz-alert> + </ng-container> + </ng-container> </div> diff --git a/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.ts b/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.ts index 993d6182bc..feb1991166 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.component.ts @@ -16,7 +16,7 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { NzModalService } from 'ng-zorro-antd/modal'; import { MessageListener, MessageListenersManager } from '@zeppelin/core'; -import { JobsItem, JobStatus, ListNoteJobs, ListUpdateNoteJobs, OP } from '@zeppelin/sdk'; +import { JobsItem, JobManagerDisabled, JobStatus, ListNoteJobs, ListUpdateNoteJobs, OP } from '@zeppelin/sdk'; import { JobManagerService, MessageService } from '@zeppelin/services'; enum JobDateSortKeys { @@ -44,14 +44,14 @@ export class JobManagerComponent extends MessageListenersManager implements OnDe filteredJobs: JobsItem[] = []; filterString: string = ''; jobs: JobsItem[] = []; - loading = true; + status: 'loading' | 'success' | 'disabled' = 'loading'; @MessageListener(OP.LIST_NOTE_JOBS) setJobs(data: ListNoteJobs) { this.jobs = data.noteJobs.jobs.filter(j => typeof j.interpreter !== 'undefined'); const interpreters = this.jobs.map(job => job.interpreter); this.interpreters = Array.from(new Set(interpreters)); - this.loading = false; + this.status = 'success'; this.filterJobs(); } @@ -72,6 +72,12 @@ export class JobManagerComponent extends MessageListenersManager implements OnDe this.filterJobs(); } + @MessageListener(OP.JOB_MANAGER_DISABLED) + onJobManagerDisabled(data: JobManagerDisabled) { + this.status = 'disabled'; + this.cdr.markForCheck(); + } + filterJobs() { const filterData = this.form.getRawValue() as FilterForm; this.filterString = filterData.noteName; diff --git a/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.module.ts b/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.module.ts index f8b6134345..349c98d936 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.module.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/job-manager/job-manager.module.ts @@ -16,6 +16,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { IconDefinition } from '@ant-design/icons-angular'; import { ClockCircleOutline, FileOutline, FileUnknownOutline, SearchOutline } from '@ant-design/icons-angular/icons'; +import { NzAlertModule } from 'ng-zorro-antd/alert'; import { NzBadgeModule } from 'ng-zorro-antd/badge'; import { NzCardModule } from 'ng-zorro-antd/card'; import { NzHighlightModule } from 'ng-zorro-antd/core/highlight'; @@ -63,7 +64,8 @@ const icons: IconDefinition[] = [SearchOutline, FileOutline, FileUnknownOutline, NzToolTipModule, NzProgressModule, NzSkeletonModule, - NzEmptyModule + NzEmptyModule, + NzAlertModule ], providers: [{ provide: NZ_ICONS, useValue: icons }] }) diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.component.js b/zeppelin-web/src/app/jobmanager/jobmanager.component.js index c883a11eff..293f2fdb41 100644 --- a/zeppelin-web/src/app/jobmanager/jobmanager.component.js +++ b/zeppelin-web/src/app/jobmanager/jobmanager.component.js @@ -28,7 +28,7 @@ const JobDateSorter = { OLDEST_UPDATED: 'Oldest Updated', }; -function JobManagerController($scope, ngToast, JobManagerFilter, JobManagerService) { +function JobManagerController($rootScope, $scope, ngToast, JobManagerFilter, JobManagerService) { 'ngInject'; $scope.isFilterLoaded = false; @@ -53,6 +53,8 @@ function JobManagerController($scope, ngToast, JobManagerFilter, JobManagerServi maxPageCount: 5, }; + $scope.jobManagerEnabled = true; + ngToast.dismiss(); init(); @@ -135,8 +137,14 @@ function JobManagerController($scope, ngToast, JobManagerFilter, JobManagerServi JobManagerService.subscribeSetJobs($scope, setJobsCallback); JobManagerService.subscribeUpdateJobs($scope, updateJobsCallback); + const deregisterJobManagerDisabledListener = $rootScope.$on('jobManagerDisabled', function(event, data) { + $scope.isFilterLoaded = true; + $scope.jobManagerEnabled = false; + }); + $scope.$on('$destroy', function() { JobManagerService.disconnect(); + deregisterJobManagerDisabledListener(); }); } diff --git a/zeppelin-web/src/app/jobmanager/jobmanager.html b/zeppelin-web/src/app/jobmanager/jobmanager.html index 55ebb96636..c0d96f0460 100644 --- a/zeppelin-web/src/app/jobmanager/jobmanager.html +++ b/zeppelin-web/src/app/jobmanager/jobmanager.html @@ -116,28 +116,38 @@ limitations under the License. Loading... </div> </div> - <div ng-if="filteredJobs.length > 0" - ng-repeat="note in getJobsInCurrentPage(filteredJobs)" - class="paragraph-col"> - <div class="job-space box job-margin"> - <job note="note"></job> + + <div ng-if="isFilterLoaded"> + <div ng-if="!jobManagerEnabled" class="paragraph-col"> + <div class="job-space box job-margin"> + <i class="fa fa-warning"></i> Job Manager is disabled in the current configuration. + </div> </div> </div> - <div ng-if="isFilterLoaded === false && filteredJobs.length <= 0" - class="paragraph-col"> - <div class="job-space box job-margin text-center">No Job found</div> - </div> - <!-- pagination --> - <div class="job-pagination-container"> - <ul uib-pagination class="pagination-sm" - total-items="filteredJobs.length" - ng-model="pagination.currentPage" - items-per-page="pagination.itemsPerPage" - boundary-links="true" rotate="false" - max-size="pagination.maxPageCount" - previous-text="‹" next-text="›" - first-text="«" last-text="»"></ul> - </div> + <div ng-if="jobManagerEnabled"> + <div ng-if="filteredJobs.length > 0" + ng-repeat="note in getJobsInCurrentPage(filteredJobs)" + class="paragraph-col"> + <div class="job-space box job-margin"> + <job note="note"></job> + </div> + </div> + <div ng-if="isFilterLoaded === false && filteredJobs.length <= 0" + class="paragraph-col"> + <div class="job-space box job-margin text-center">No Job found</div> + </div> + <!-- pagination --> + <div class="job-pagination-container"> + <ul uib-pagination class="pagination-sm" + total-items="filteredJobs.length" + ng-model="pagination.currentPage" + items-per-page="pagination.itemsPerPage" + boundary-links="true" rotate="false" + max-size="pagination.maxPageCount" + previous-text="‹" next-text="›" + first-text="«" last-text="»"></ul> + </div> + </div> </div> diff --git a/zeppelin-web/src/components/websocket/websocket-event.factory.js b/zeppelin-web/src/components/websocket/websocket-event.factory.js index 8cbf34cd3d..ccf941b4a3 100644 --- a/zeppelin-web/src/components/websocket/websocket-event.factory.js +++ b/zeppelin-web/src/components/websocket/websocket-event.factory.js @@ -80,6 +80,8 @@ function WebsocketEventFactory($rootScope, $websocket, $location, baseUrlSrv, sa $rootScope.$emit('jobmanager:set-jobs', data.noteJobs); } else if (op === 'LIST_UPDATE_NOTE_JOBS') { $rootScope.$emit('jobmanager:update-jobs', data.noteRunningJobs); + } else if (op === 'JOB_MANAGER_DISABLED') { + $rootScope.$broadcast('jobManagerDisabled', data.errorMessage); } else if (op === 'AUTH_INFO') { let btn = []; if ($rootScope.ticket.roles === '[]') {