This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 3cd08b666 feat(bindings/nodejs): add retry layer (#3484)
3cd08b666 is described below
commit 3cd08b666c67e61da1272ee90a119b4b012c7f29
Author: Suyan <[email protected]>
AuthorDate: Tue Nov 14 20:54:14 2023 +0800
feat(bindings/nodejs): add retry layer (#3484)
* feat(bindings/nodejs): add retry layer
Signed-off-by: suyanhanx <[email protected]>
* Try
Signed-off-by: Xuanwo <[email protected]>
* Remove compat-mode
Signed-off-by: Xuanwo <[email protected]>
* polish API
Signed-off-by: Xuanwo <[email protected]>
* fill
Signed-off-by: suyanhanx <[email protected]>
* polish tests
Signed-off-by: suyanhanx <[email protected]>
* fmt
Signed-off-by: suyanhanx <[email protected]>
* revert test job & remove useless clippy allow
Signed-off-by: suyanhanx <[email protected]>
* add some doc
Signed-off-by: suyanhanx <[email protected]>
* commit type file
Signed-off-by: suyanhanx <[email protected]>
---------
Signed-off-by: suyanhanx <[email protected]>
Signed-off-by: Xuanwo <[email protected]>
Co-authored-by: Xuanwo <[email protected]>
---
.github/workflows/bindings_nodejs.yml | 9 +-
bindings/nodejs/generated.js | 4 +-
bindings/nodejs/index.d.ts | 78 +++++++++++++++
bindings/nodejs/index.js | 5 +-
bindings/nodejs/src/lib.rs | 138 ++++++++++++++++++++++++++-
bindings/nodejs/tests/suites/async.suite.mjs | 2 +-
bindings/nodejs/tests/suites/index.mjs | 10 +-
bindings/nodejs/tests/suites/sync.suite.mjs | 2 +-
fixtures/alluxio/docker-compose-alluxio.yml | 6 +-
9 files changed, 240 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/bindings_nodejs.yml
b/.github/workflows/bindings_nodejs.yml
index c9f176800..6891109f8 100644
--- a/.github/workflows/bindings_nodejs.yml
+++ b/.github/workflows/bindings_nodejs.yml
@@ -48,8 +48,6 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: ./.github/actions/setup
- with:
- need-nextest: true
- name: Setup node
uses: actions/setup-node@v4
with:
@@ -65,11 +63,14 @@ jobs:
- name: Check format
run: yarn run prettier --check .
+ - name: Build
+ run: yarn build
+
- name: Check diff
run: git diff --exit-code
- - name: Unit test
- run: cargo nextest run --no-fail-fast
+ - name: Test
+ run: cargo test --no-fail-fast
linux:
runs-on: ubuntu-latest
diff --git a/bindings/nodejs/generated.js b/bindings/nodejs/generated.js
index 915128b86..2b40ec25e 100644
--- a/bindings/nodejs/generated.js
+++ b/bindings/nodejs/generated.js
@@ -271,10 +271,12 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
-const { Operator, Entry, Metadata, Lister, BlockingLister } = nativeBinding
+const { Operator, Entry, Metadata, Lister, BlockingLister, Layer, RetryLayer }
= nativeBinding
module.exports.Operator = Operator
module.exports.Entry = Entry
module.exports.Metadata = Metadata
module.exports.Lister = Lister
module.exports.BlockingLister = BlockingLister
+module.exports.Layer = Layer
+module.exports.RetryLayer = RetryLayer
diff --git a/bindings/nodejs/index.d.ts b/bindings/nodejs/index.d.ts
index 112ece450..0d0455b0b 100644
--- a/bindings/nodejs/index.d.ts
+++ b/bindings/nodejs/index.d.ts
@@ -22,6 +22,12 @@
/* auto-generated by NAPI-RS */
+export class ExternalObject<T> {
+ readonly '': {
+ readonly '': unique symbol
+ [K: symbol]: T
+ }
+}
export interface PresignedRequest {
/** HTTP method of this request. */
method: string
@@ -394,6 +400,8 @@ export class Operator {
* ```
*/
presignStat(path: string, expires: number): Promise<PresignedRequest>
+ /** Add a layer to this operator. */
+ layer(layer: ExternalObject<Layer>): this
}
export class Entry {
/** Return the path of this entry. */
@@ -435,3 +443,73 @@ export class Lister {
export class BlockingLister {
next(): Entry | null
}
+/** A public layer wrapper */
+export class Layer { }
+/**
+ * Retry layer
+ *
+ * Add retry for temporary failed operations.
+ *
+ * # Notes
+ *
+ * This layer will retry failed operations when [`Error::is_temporary`]
+ * returns true. If operation still failed, this layer will set error to
+ * `Persistent` which means error has been retried.
+ *
+ * `write` and `blocking_write` don't support retry so far, visit [this
issue](https://github.com/apache/incubator-opendal/issues/1223) for more
details.
+ *
+ * # Examples
+ *
+ * ```javascript
+ * const op = new Operator("file", { root: "/tmp" })
+ *
+ * const retry = new RetryLayer();
+ * retry.max_times = 3;
+ * retry.jitter = true;
+ *
+ * op.layer(retry.build());
+ *```
+ */
+export class RetryLayer {
+ constructor()
+ /**
+ * Set jitter of current backoff.
+ *
+ * If jitter is enabled, ExponentialBackoff will add a random jitter in `[0,
min_delay)
+ * to current delay.
+ */
+ set jitter(v: boolean)
+ /**
+ * Set max_times of current backoff.
+ *
+ * Backoff will return `None` if max times is reaching.
+ */
+ set maxTimes(v: number)
+ /**
+ * Set factor of current backoff.
+ *
+ * # Panics
+ *
+ * This function will panic if input factor smaller than `1.0`.
+ */
+ set factor(v: number)
+ /**
+ * Set max_delay of current backoff.
+ *
+ * Delay will not increasing if current delay is larger than max_delay.
+ *
+ * # Notes
+ *
+ * - The unit of max_delay is millisecond.
+ */
+ set maxDelay(v: number)
+ /**
+ * Set min_delay of current backoff.
+ *
+ * # Notes
+ *
+ * - The unit of min_delay is millisecond.
+ */
+ set minDelay(v: number)
+ build(): ExternalObject<Layer>
+}
diff --git a/bindings/nodejs/index.js b/bindings/nodejs/index.js
index 1200443d5..c1af66747 100644
--- a/bindings/nodejs/index.js
+++ b/bindings/nodejs/index.js
@@ -19,6 +19,9 @@
/// <reference types="node" />
-const { Operator } = require('./generated.js')
+const { Operator, RetryLayer } = require('./generated.js')
module.exports.Operator = Operator
+module.exports.layers = {
+ RetryLayer,
+}
diff --git a/bindings/nodejs/src/lib.rs b/bindings/nodejs/src/lib.rs
index 7882be4cb..e994e1fac 100644
--- a/bindings/nodejs/src/lib.rs
+++ b/bindings/nodejs/src/lib.rs
@@ -24,7 +24,6 @@ use std::time::Duration;
use futures::TryStreamExt;
use napi::bindgen_prelude::*;
-use napi::tokio;
#[napi]
pub struct Operator(opendal::Operator);
@@ -685,6 +684,143 @@ impl PresignedRequest {
}
}
+pub trait NodeLayer: Send + Sync {
+ fn layer(&self, op: opendal::Operator) -> opendal::Operator;
+}
+
+/// A public layer wrapper
+#[napi]
+pub struct Layer {
+ inner: Box<dyn NodeLayer>,
+}
+
+#[napi]
+impl Operator {
+ /// Add a layer to this operator.
+ #[napi]
+ pub fn layer(&self, layer: External<Layer>) -> Result<Self> {
+ Ok(Self(layer.inner.layer(self.0.clone())))
+ }
+}
+
+impl NodeLayer for opendal::layers::RetryLayer {
+ fn layer(&self, op: opendal::Operator) -> opendal::Operator {
+ op.layer(self.clone())
+ }
+}
+
+/// Retry layer
+///
+/// Add retry for temporary failed operations.
+///
+/// # Notes
+///
+/// This layer will retry failed operations when [`Error::is_temporary`]
+/// returns true. If operation still failed, this layer will set error to
+/// `Persistent` which means error has been retried.
+///
+/// `write` and `blocking_write` don't support retry so far, visit [this
issue](https://github.com/apache/incubator-opendal/issues/1223) for more
details.
+///
+/// # Examples
+///
+/// ```javascript
+/// const op = new Operator("file", { root: "/tmp" })
+///
+/// const retry = new RetryLayer();
+/// retry.max_times = 3;
+/// retry.jitter = true;
+///
+/// op.layer(retry.build());
+///```
+#[derive(Default)]
+#[napi]
+pub struct RetryLayer {
+ jitter: bool,
+ max_times: Option<u32>,
+ factor: Option<f64>,
+ max_delay: Option<f64>,
+ min_delay: Option<f64>,
+}
+
+#[napi]
+impl RetryLayer {
+ #[napi(constructor)]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Set jitter of current backoff.
+ ///
+ /// If jitter is enabled, ExponentialBackoff will add a random jitter in
`[0, min_delay)
+ /// to current delay.
+ #[napi(setter)]
+ pub fn jitter(&mut self, v: bool) {
+ self.jitter = v;
+ }
+
+ /// Set max_times of current backoff.
+ ///
+ /// Backoff will return `None` if max times is reaching.
+ #[napi(setter)]
+ pub fn max_times(&mut self, v: u32) {
+ self.max_times = Some(v);
+ }
+
+ /// Set factor of current backoff.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if input factor smaller than `1.0`.
+ #[napi(setter)]
+ pub fn factor(&mut self, v: f64) {
+ self.factor = Some(v);
+ }
+
+ /// Set max_delay of current backoff.
+ ///
+ /// Delay will not increasing if current delay is larger than max_delay.
+ ///
+ /// # Notes
+ ///
+ /// - The unit of max_delay is millisecond.
+ #[napi(setter)]
+ pub fn max_delay(&mut self, v: f64) {
+ self.max_delay = Some(v);
+ }
+
+ /// Set min_delay of current backoff.
+ ///
+ /// # Notes
+ ///
+ /// - The unit of min_delay is millisecond.
+ #[napi(setter)]
+ pub fn min_delay(&mut self, v: f64) {
+ self.min_delay = Some(v);
+ }
+
+ #[napi]
+ pub fn build(&self) -> External<Layer> {
+ let mut l = opendal::layers::RetryLayer::default();
+ if self.jitter {
+ l = l.with_jitter();
+ }
+ if let Some(max_times) = self.max_times {
+ l = l.with_max_times(max_times as usize);
+ }
+ if let Some(factor) = self.factor {
+ l = l.with_factor(factor as f32);
+ }
+ if let Some(max_delay) = self.max_delay {
+ l = l.with_max_delay(Duration::from_millis(max_delay as u64));
+ }
+ if let Some(min_delay) = self.min_delay {
+ l = l.with_min_delay(Duration::from_millis(min_delay as u64));
+ }
+
+ External::new(Layer { inner: Box::new(l) })
+ }
+}
+
fn format_napi_error(err: opendal::Error) -> Error {
Error::from_reason(format!("{}", err))
}
diff --git a/bindings/nodejs/tests/suites/async.suite.mjs
b/bindings/nodejs/tests/suites/async.suite.mjs
index 8214c1b17..9a68f804e 100644
--- a/bindings/nodejs/tests/suites/async.suite.mjs
+++ b/bindings/nodejs/tests/suites/async.suite.mjs
@@ -28,7 +28,7 @@ export function run(operator) {
try {
await operator.stat(filename)
} catch (error) {
- assert.ok(error.message.includes('NotFound'))
+ assert.include(error.message, 'NotFound')
}
})
}
diff --git a/bindings/nodejs/tests/suites/index.mjs
b/bindings/nodejs/tests/suites/index.mjs
index cd8998009..a67467f28 100644
--- a/bindings/nodejs/tests/suites/index.mjs
+++ b/bindings/nodejs/tests/suites/index.mjs
@@ -18,7 +18,7 @@
*/
import { describe } from 'vitest'
-import { Operator } from '../../index.js'
+import { Operator, layers } from '../../index.js'
import { checkRandomRootEnabled, generateRandomRoot, loadConfigFromEnv } from
'../utils.mjs'
import { run as AsyncIOTestRun } from './async.suite.mjs'
@@ -36,7 +36,13 @@ export function runner(testName, scheme) {
config.root = generateRandomRoot(config.root)
}
- const operator = scheme ? new Operator(scheme, config) : null
+ let operator = scheme ? new Operator(scheme, config) : null
+
+ let retryLayer = new layers.RetryLayer()
+ retryLayer.jitter = true
+ retryLayer.maxTimes = 4
+
+ operator = operator.layer(retryLayer.build())
describe.skipIf(!operator)(testName, () => {
AsyncIOTestRun(operator)
diff --git a/bindings/nodejs/tests/suites/sync.suite.mjs
b/bindings/nodejs/tests/suites/sync.suite.mjs
index 473d121c6..5e73444d3 100644
--- a/bindings/nodejs/tests/suites/sync.suite.mjs
+++ b/bindings/nodejs/tests/suites/sync.suite.mjs
@@ -28,7 +28,7 @@ export function run(operator) {
try {
operator.statSync(filename)
} catch (error) {
- assert.ok(error.message.includes('NotFound'))
+ assert.include(error.message, 'NotFound')
}
})
}
diff --git a/fixtures/alluxio/docker-compose-alluxio.yml
b/fixtures/alluxio/docker-compose-alluxio.yml
index d725fe8c3..6fe399dcb 100644
--- a/fixtures/alluxio/docker-compose-alluxio.yml
+++ b/fixtures/alluxio/docker-compose-alluxio.yml
@@ -23,7 +23,7 @@ services:
- 19999:19999
- 19998:19998
environment:
- ALLUXIO_JAVA_OPTS: -Dalluxio.master.hostname=alluxio-master
-Dalluxio.master.mount.table.root.ufs=/opt/alluxio/underFSStorage
+ ALLUXIO_JAVA_OPTS: -Dalluxio.master.hostname=alluxio-master
-Dalluxio.master.mount.table.root.ufs=/opt/alluxio/underFSStorage
command: master
networks:
- alluxio_network
@@ -55,7 +55,7 @@ services:
- 30000:30000
shm_size: 1gb
environment:
- ALLUXIO_JAVA_OPTS: -Dalluxio.worker.ramdisk.size=1G
-Dalluxio.master.hostname=alluxio-master
-Dalluxio.worker.hostname=alluxio-worker
+ ALLUXIO_JAVA_OPTS: -Dalluxio.worker.ramdisk.size=1G
-Dalluxio.master.hostname=alluxio-master
-Dalluxio.worker.hostname=alluxio-worker
command: worker
networks:
- alluxio_network
@@ -69,4 +69,4 @@ services:
networks:
alluxio_network:
driver: bridge
-
\ No newline at end of file
+