[ZEPPELIN-6579] Make notebook tree reload safe for concurrent note operations

### What is this PR for?
`NoteManager` locates a note through two separate pieces of state: `notesInfo` maps a note id to its path, and `root` holds the folder tree that the path is walked against. A lookup uses both in sequence, so the two have to agree.

`reloadNotes()` replaced them one at a time:

```java
public void reloadNotes() throws IOException {
  this.root = new Folder("/", notebookRepo, noteCache, zConf);  // (1) tree becomes empty
  this.trash = this.root.getOrCreateFolder(TRASH_FOLDER);
  init();                                                      // (2) new mapping, (3) refill tree
}
```

Neither field is `volatile` and nothing is held while they are swapped, so a concurrent `processNote()` can observe a mapping and a tree that belong to different generations:

| time | reloading thread | note request thread | state |
|---|---|---|---|
| t1 | installs an empty tree | | mapping: old (complete) / tree: **empty** |
| t2 | | `notesInfo.containsKey(noteId)` passes | the id is still in the old mapping |
| t3 | | walks the path in the tree, finds nothing | **throws** |
| t4 | installs the new mapping | | |
| t5 | refills the tree, one note at a time | | notes not inserted yet still fail |

The guard in `processNote()` only checks `notesInfo`, so it passes and the failure surfaces one line later in `getNoteNode()`:

```
java.io.IOException: Can not find note: /E2E_TEST_FOLDER/TestNotebook_...
  at org.apache.zeppelin.notebook.NoteManager.getNoteNode
  at org.apache.zeppelin.notebook.NoteManager.processNote
  at org.apache.zeppelin.rest.NotebookRestApi.updateParagraph
```

`IOException` is not mapped to a specific status, so `WebApplicationExceptionMapper` turns it into **HTTP 500** for a note that was never removed. Everything that goes through `processNote()` is affected: reading a note, updating a paragraph, creating, deleting and moving notes, and listing the notebook.

This PR holds the tree, the trash folder and the mapping in one immutable `NoteTree` and publishes it with a single `volatile` write. `buildNoteTree()` fills the new tree locally and returns it; only then is it assigned. The tree-walking helpers (`getNoteNode`, `getFolder`, `getOrCreateFolder`, `isNotePathAvailable`) take the tree as a parameter, and callers that need both pieces of state read the reference once, so a lookup resolves the mapping and the tree against the same generation. Those helpers are `static` so that the compiler prevents them from reaching back to the field.

### Scope and related issues
**#5325** (`[ZEPPELIN-5858]`) is open against the same class and restructures `removeNote`, `moveNote` and `moveFolder` with `synchronized (this)`. It targets a different race (two mutators duplicating a note) and its monitor does not cover `reloadNotes()`, so neither change subsumes the other. Whichever merges second will need a rebase.

### What type of PR is it?
Bug Fix

### Todos
* [x] - Build the new tree, trash folder and mapping in `buildNoteTree()` before publishing them
* [x] - Hold the three in an immutable `NoteTree` published through a single `volatile` write
* [x] - Pass the tree into the tree-walking helpers so one lookup uses one generation
* [x] - Add a regression test that reloads while other threads read notes
* [x] - Confirm the test fails without the fix and passes with it

### What is the Jira issue?
* [ZEPPELIN-6579](https://issues.apache.org/jira/browse/ZEPPELIN-6579)

### How should this be tested?
New test `NoteManagerTest#testConcurrentReloadAndProcessNote`: it saves 50 notes, then runs `reloadNotes()` in a loop on one thread while four threads keep calling `processNote()` for every note, and asserts that no lookup fails or returns nothing.

```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 11)
./mvnw package -pl zeppelin-server --am -Dtest=NoteManagerTest -DfailIfNoTests=false
```

Result with the fix: `Tests run: 7, Failures: 0, Errors: 0`.

Reverting only the production change makes the new test fail on every reader thread with `java.io.IOException: Can not find note: /prod/note_0` thrown from `NoteManager.getNoteNode` via `NoteManager.processNote`, which is the stack from the ticket; with the fix it passes.

Also run, to cover the callers of the reload path:

```bash
./mvnw package -pl zeppelin-server --am \
  -Dtest='NotebookTest#testReloadAllNotes+testReloadAndSetInterpreter' -DfailIfNoTests=false
```

Result: `Tests run: 2, Failures: 0, Errors: 0`.

Not verified locally: the full `NotebookTest` and `NotebookServerTest` classes, which start real remote interpreter processes and time out in my environment, and `NotebookRepoSyncTest`. Those are left to CI.

### Screenshots (if appropriate)
N/A

### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No


Closes #5357 from big-cir/ZEPPELIN-6579.

Signed-off-by: Jongyoul Lee <jongyoul@gmail.com>
2 files changed
tree: 3deefcc37c7b5aa2125753643f1735410e43a0e0
  1. .github/
  2. .husky/
  3. .mvn/
  4. angular/
  5. bigquery/
  6. bin/
  7. build-tools/
  8. cassandra/
  9. conf/
  10. dev/
  11. docs/
  12. elasticsearch/
  13. examples/
  14. file/
  15. flink/
  16. flink-cmd/
  17. groovy/
  18. hbase/
  19. helium-dev/
  20. influxdb/
  21. java/
  22. jdbc/
  23. k8s/
  24. licenses/
  25. livy/
  26. markdown/
  27. mongodb/
  28. neo4j/
  29. notebook/
  30. python/
  31. scripts/
  32. shell/
  33. spark/
  34. spark-submit/
  35. sparql/
  36. testing/
  37. zeppelin-client/
  38. zeppelin-client-examples/
  39. zeppelin-common/
  40. zeppelin-distribution/
  41. zeppelin-examples/
  42. zeppelin-integration/
  43. zeppelin-interpreter/
  44. zeppelin-interpreter-integration/
  45. zeppelin-interpreter-parent/
  46. zeppelin-interpreter-shaded/
  47. zeppelin-jupyter/
  48. zeppelin-jupyter-interpreter/
  49. zeppelin-jupyter-interpreter-shaded/
  50. zeppelin-plugins/
  51. zeppelin-server/
  52. zeppelin-test/
  53. zeppelin-web/
  54. zeppelin-web-angular/
  55. .asf.yaml
  56. .gitattributes
  57. .gitignore
  58. AGENTS.md
  59. Dockerfile
  60. LICENSE
  61. mvnw
  62. mvnw.cmd
  63. NOTICE
  64. pom.xml
  65. README.md
  66. Roadmap.md
  67. SECURITY-README.md
  68. SECURITY.md
  69. STYLE.md
  70. THREAT_MODEL.md
README.md

Apache Zeppelin

Documentation: User Guide
Mailing Lists: User and Dev mailing list
Continuous Integration: core frontend rat
Contributing: Contribution Guide
Issue Tracker: Jira
License: Apache 2.0

Zeppelin, a web-based notebook that enables interactive data analytics. You can make beautiful data-driven, interactive and collaborative documents with SQL, Scala and more.

Core features:

  • Web based notebook style editor.
  • Built-in Apache Spark support

To know more about Zeppelin, visit our web site https://zeppelin.apache.org

Getting Started

Install binary package

Please go to install to install Apache Zeppelin from binary package.

Build from source

Please check Build from source to build Zeppelin from source.