opendal 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +31 -36
  3. data/Cargo.toml +1 -1
  4. data/DEPENDENCIES.rust.tsv +25 -35
  5. data/README.md +1 -1
  6. data/core/CHANGELOG.md +22 -0
  7. data/core/Cargo.lock +145 -189
  8. data/core/Cargo.toml +95 -95
  9. data/core/DEPENDENCIES.rust.tsv +259 -290
  10. data/core/LICENSE +16 -0
  11. data/core/benches/vs_fs/Cargo.toml +1 -1
  12. data/core/benches/vs_s3/Cargo.toml +1 -1
  13. data/core/core/CHANGELOG.md +6031 -0
  14. data/core/core/Cargo.toml +1 -1
  15. data/core/core/LICENSE +16 -0
  16. data/core/core/src/docs/mod.rs +1 -1
  17. data/core/core/src/docs/upgrade.md +8 -0
  18. data/core/core/src/lib.rs +1 -3
  19. data/core/core/src/raw/futures_util.rs +26 -21
  20. data/core/core/src/raw/http_util/error.rs +9 -0
  21. data/core/core/src/raw/ops.rs +1 -0
  22. data/core/core/src/types/http_transport/mod.rs +30 -2
  23. data/core/core/src/types/http_transport/uri.rs +166 -0
  24. data/core/core/src/types/metadata.rs +1 -0
  25. data/core/core/src/types/mod.rs +2 -0
  26. data/core/http-transports/reqwest/Cargo.toml +1 -1
  27. data/core/http-transports/reqwest/LICENSE +16 -0
  28. data/core/http-transports/reqwest/src/lib.rs +46 -4
  29. data/core/layers/async-backtrace/Cargo.toml +2 -2
  30. data/core/layers/async-backtrace/LICENSE +16 -0
  31. data/core/layers/await-tree/Cargo.toml +2 -2
  32. data/core/layers/await-tree/LICENSE +16 -0
  33. data/core/layers/capability-check/Cargo.toml +2 -2
  34. data/core/layers/capability-check/LICENSE +16 -0
  35. data/core/layers/chaos/Cargo.toml +2 -2
  36. data/core/layers/chaos/LICENSE +16 -0
  37. data/core/layers/concurrent-limit/Cargo.toml +3 -3
  38. data/core/layers/concurrent-limit/LICENSE +16 -0
  39. data/core/layers/dtrace/Cargo.toml +2 -2
  40. data/core/layers/dtrace/LICENSE +16 -0
  41. data/core/layers/fastmetrics/Cargo.toml +3 -3
  42. data/core/layers/fastmetrics/LICENSE +16 -0
  43. data/core/layers/fastrace/Cargo.toml +2 -2
  44. data/core/layers/fastrace/LICENSE +16 -0
  45. data/core/layers/foyer/Cargo.toml +2 -2
  46. data/core/layers/foyer/LICENSE +16 -0
  47. data/core/layers/hotpath/Cargo.toml +2 -2
  48. data/core/layers/hotpath/LICENSE +16 -0
  49. data/core/layers/immutable-index/Cargo.toml +2 -2
  50. data/core/layers/immutable-index/LICENSE +16 -0
  51. data/core/layers/logging/Cargo.toml +2 -2
  52. data/core/layers/logging/LICENSE +16 -0
  53. data/core/layers/metrics/Cargo.toml +3 -3
  54. data/core/layers/metrics/LICENSE +16 -0
  55. data/core/layers/mime-guess/Cargo.toml +2 -2
  56. data/core/layers/mime-guess/LICENSE +16 -0
  57. data/core/layers/observe-metrics-common/Cargo.toml +1 -1
  58. data/core/layers/observe-metrics-common/LICENSE +16 -0
  59. data/core/layers/otelmetrics/Cargo.toml +3 -3
  60. data/core/layers/otelmetrics/LICENSE +16 -0
  61. data/core/layers/oteltrace/Cargo.toml +2 -2
  62. data/core/layers/oteltrace/LICENSE +16 -0
  63. data/core/layers/prometheus/Cargo.toml +3 -3
  64. data/core/layers/prometheus/LICENSE +16 -0
  65. data/core/layers/prometheus-client/Cargo.toml +3 -3
  66. data/core/layers/prometheus-client/LICENSE +16 -0
  67. data/core/layers/retry/Cargo.toml +4 -4
  68. data/core/layers/retry/LICENSE +16 -0
  69. data/core/layers/route/Cargo.toml +3 -3
  70. data/core/layers/route/LICENSE +16 -0
  71. data/core/layers/tail-cut/Cargo.toml +2 -2
  72. data/core/layers/tail-cut/LICENSE +16 -0
  73. data/core/layers/throttle/Cargo.toml +2 -2
  74. data/core/layers/throttle/LICENSE +16 -0
  75. data/core/layers/timeout/Cargo.toml +3 -3
  76. data/core/layers/timeout/LICENSE +16 -0
  77. data/core/layers/tracing/Cargo.toml +2 -2
  78. data/core/layers/tracing/LICENSE +16 -0
  79. data/core/services/aliyun-drive/Cargo.toml +2 -2
  80. data/core/services/aliyun-drive/LICENSE +16 -0
  81. data/core/services/alluxio/Cargo.toml +1 -1
  82. data/core/services/alluxio/LICENSE +16 -0
  83. data/core/services/azblob/Cargo.toml +2 -2
  84. data/core/services/azblob/LICENSE +16 -0
  85. data/core/services/azblob/src/backend.rs +21 -7
  86. data/core/services/azblob/src/config.rs +4 -1
  87. data/core/services/azdls/Cargo.toml +3 -3
  88. data/core/services/azdls/LICENSE +16 -0
  89. data/core/services/azfile/Cargo.toml +2 -2
  90. data/core/services/azfile/LICENSE +16 -0
  91. data/core/services/azure-common/Cargo.toml +1 -1
  92. data/core/services/azure-common/LICENSE +16 -0
  93. data/core/services/b2/Cargo.toml +2 -2
  94. data/core/services/b2/LICENSE +16 -0
  95. data/core/services/cacache/Cargo.toml +1 -1
  96. data/core/services/cacache/LICENSE +16 -0
  97. data/core/services/cloudflare-kv/Cargo.toml +1 -1
  98. data/core/services/cloudflare-kv/LICENSE +16 -0
  99. data/core/services/compfs/Cargo.toml +1 -1
  100. data/core/services/compfs/LICENSE +16 -0
  101. data/core/services/compfs/src/core.rs +27 -11
  102. data/core/services/cos/Cargo.toml +1 -1
  103. data/core/services/cos/LICENSE +16 -0
  104. data/core/services/d1/Cargo.toml +1 -1
  105. data/core/services/d1/LICENSE +16 -0
  106. data/core/services/dashmap/Cargo.toml +1 -1
  107. data/core/services/dashmap/LICENSE +16 -0
  108. data/core/services/dbfs/Cargo.toml +1 -1
  109. data/core/services/dbfs/LICENSE +16 -0
  110. data/core/services/dropbox/Cargo.toml +2 -2
  111. data/core/services/dropbox/LICENSE +16 -0
  112. data/core/services/etcd/Cargo.toml +2 -2
  113. data/core/services/etcd/LICENSE +16 -0
  114. data/core/services/etcd/src/core.rs +3 -3
  115. data/core/services/foundationdb/Cargo.toml +1 -1
  116. data/core/services/foundationdb/LICENSE +16 -0
  117. data/core/services/foyer/Cargo.toml +3 -3
  118. data/core/services/foyer/LICENSE +16 -0
  119. data/core/services/fs/Cargo.toml +1 -1
  120. data/core/services/fs/LICENSE +16 -0
  121. data/core/services/fs/src/core.rs +69 -10
  122. data/core/services/ftp/Cargo.toml +2 -2
  123. data/core/services/ftp/LICENSE +16 -0
  124. data/core/services/ftp/src/core.rs +1 -1
  125. data/core/services/ftp/src/reader.rs +1 -1
  126. data/core/services/ftp/src/writer.rs +1 -1
  127. data/core/services/gcs/Cargo.toml +3 -2
  128. data/core/services/gcs/LICENSE +16 -0
  129. data/core/services/gcs/src/composer.rs +11 -21
  130. data/core/services/gcs/src/core.rs +116 -2
  131. data/core/services/gcs-grpc/Cargo.toml +1 -1
  132. data/core/services/gcs-grpc/proto/google/storage/v2/storage.proto +17 -18
  133. data/core/services/gdrive/Cargo.toml +3 -2
  134. data/core/services/gdrive/LICENSE +16 -0
  135. data/core/services/gdrive/src/docs.md +2 -0
  136. data/core/services/gdrive/src/path_index.rs +109 -18
  137. data/core/services/ghac/Cargo.toml +2 -2
  138. data/core/services/ghac/LICENSE +16 -0
  139. data/core/services/github/Cargo.toml +1 -1
  140. data/core/services/github/LICENSE +16 -0
  141. data/core/services/goosefs/Cargo.toml +13 -4
  142. data/core/services/goosefs/LICENSE +16 -0
  143. data/core/services/goosefs/README.md +11 -0
  144. data/core/services/goosefs/src/backend.rs +376 -49
  145. data/core/services/goosefs/src/config.rs +18 -10
  146. data/core/services/goosefs/src/core.rs +1 -1
  147. data/core/services/goosefs/src/docs.md +72 -3
  148. data/core/services/goosefs/src/reader.rs +22 -89
  149. data/core/services/goosefs/tests/master_addr_resolution.rs +103 -0
  150. data/core/services/gridfs/Cargo.toml +2 -2
  151. data/core/services/gridfs/LICENSE +16 -0
  152. data/core/services/hdfs/Cargo.toml +1 -1
  153. data/core/services/hdfs/LICENSE +16 -0
  154. data/core/services/hdfs-native/Cargo.toml +1 -1
  155. data/core/services/hdfs-native/LICENSE +16 -0
  156. data/core/services/hf/Cargo.toml +3 -3
  157. data/core/services/hf/LICENSE +16 -0
  158. data/core/services/hf/src/backend.rs +77 -9
  159. data/core/services/hf/src/config.rs +58 -0
  160. data/core/services/hf/src/core.rs +695 -318
  161. data/core/services/hf/src/docs.md +29 -0
  162. data/core/services/hf/src/lister.rs +3 -3
  163. data/core/services/hf/src/reader.rs +128 -143
  164. data/core/services/http/Cargo.toml +3 -3
  165. data/core/services/http/LICENSE +16 -0
  166. data/core/services/ipfs/Cargo.toml +1 -1
  167. data/core/services/ipfs/LICENSE +16 -0
  168. data/core/services/ipmfs/Cargo.toml +1 -1
  169. data/core/services/ipmfs/LICENSE +16 -0
  170. data/core/services/koofr/Cargo.toml +2 -2
  171. data/core/services/koofr/LICENSE +16 -0
  172. data/core/services/lakefs/Cargo.toml +1 -1
  173. data/core/services/lakefs/LICENSE +16 -0
  174. data/core/services/memcached/Cargo.toml +2 -2
  175. data/core/services/memcached/LICENSE +16 -0
  176. data/core/services/memcached/src/core.rs +3 -3
  177. data/core/services/mini_moka/Cargo.toml +1 -1
  178. data/core/services/mini_moka/LICENSE +16 -0
  179. data/core/services/moka/Cargo.toml +1 -1
  180. data/core/services/moka/LICENSE +16 -0
  181. data/core/services/mongodb/Cargo.toml +2 -2
  182. data/core/services/mongodb/LICENSE +16 -0
  183. data/core/services/monoiofs/Cargo.toml +2 -2
  184. data/core/services/monoiofs/LICENSE +16 -0
  185. data/core/services/monoiofs/src/core.rs +27 -11
  186. data/core/services/mysql/Cargo.toml +2 -2
  187. data/core/services/mysql/LICENSE +16 -0
  188. data/core/services/obs/Cargo.toml +1 -1
  189. data/core/services/obs/LICENSE +16 -0
  190. data/core/services/onedrive/Cargo.toml +2 -2
  191. data/core/services/onedrive/LICENSE +16 -0
  192. data/core/services/opfs/Cargo.toml +1 -1
  193. data/core/services/opfs/LICENSE +16 -0
  194. data/core/services/oss/Cargo.toml +1 -1
  195. data/core/services/oss/LICENSE +16 -0
  196. data/core/services/pcloud/Cargo.toml +1 -1
  197. data/core/services/pcloud/LICENSE +16 -0
  198. data/core/services/persy/Cargo.toml +1 -1
  199. data/core/services/persy/LICENSE +16 -0
  200. data/core/services/postgresql/Cargo.toml +2 -2
  201. data/core/services/postgresql/LICENSE +16 -0
  202. data/core/services/redb/Cargo.toml +1 -1
  203. data/core/services/redb/LICENSE +16 -0
  204. data/core/services/redis/Cargo.toml +2 -2
  205. data/core/services/redis/LICENSE +16 -0
  206. data/core/services/redis/src/core.rs +1 -1
  207. data/core/services/rocksdb/Cargo.toml +1 -1
  208. data/core/services/rocksdb/LICENSE +16 -0
  209. data/core/services/s3/Cargo.toml +1 -1
  210. data/core/services/s3/LICENSE +16 -0
  211. data/core/services/seafile/Cargo.toml +4 -2
  212. data/core/services/seafile/LICENSE +16 -0
  213. data/core/services/seafile/src/backend.rs +2 -3
  214. data/core/services/seafile/src/core.rs +189 -86
  215. data/core/services/seafile/src/docs.md +2 -0
  216. data/core/services/sftp/Cargo.toml +2 -2
  217. data/core/services/sftp/LICENSE +16 -0
  218. data/core/services/sftp/src/core.rs +1 -1
  219. data/core/services/sftp/src/reader.rs +1 -1
  220. data/core/services/sled/Cargo.toml +1 -1
  221. data/core/services/sled/LICENSE +16 -0
  222. data/core/services/sqlite/Cargo.toml +2 -2
  223. data/core/services/sqlite/LICENSE +16 -0
  224. data/core/services/surrealdb/Cargo.toml +2 -2
  225. data/core/services/surrealdb/LICENSE +16 -0
  226. data/core/services/swift/Cargo.toml +1 -1
  227. data/core/services/swift/LICENSE +16 -0
  228. data/core/services/tikv/Cargo.toml +2 -2
  229. data/core/services/tikv/LICENSE +16 -0
  230. data/core/services/tos/Cargo.toml +1 -1
  231. data/core/services/tos/LICENSE +16 -0
  232. data/core/services/upyun/Cargo.toml +1 -1
  233. data/core/services/upyun/LICENSE +16 -0
  234. data/core/services/vercel-artifacts/Cargo.toml +1 -1
  235. data/core/services/vercel-artifacts/LICENSE +16 -0
  236. data/core/services/vercel-blob/Cargo.toml +1 -1
  237. data/core/services/vercel-blob/LICENSE +16 -0
  238. data/core/services/webdav/Cargo.toml +1 -2
  239. data/core/services/webdav/LICENSE +16 -0
  240. data/core/services/webhdfs/Cargo.toml +2 -2
  241. data/core/services/webhdfs/LICENSE +16 -0
  242. data/core/services/yandex-disk/Cargo.toml +1 -1
  243. data/core/services/yandex-disk/LICENSE +16 -0
  244. data/core/src/lib.rs +1 -3
  245. data/core/testkit/Cargo.toml +4 -4
  246. data/core/testkit/LICENSE +16 -0
  247. data/core/upgrade.md +8 -0
  248. metadata +5 -2
@@ -64,13 +64,11 @@ impl GoosefsBuilder {
64
64
  /// Single master: `"10.0.0.1:9200"`
65
65
  /// HA (comma-separated): `"10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200"`
66
66
  ///
67
- /// When provided here it **overrides** any address discovered from
68
- /// `goosefs-site.properties` or `GOOSEFS_MASTER_ADDR`. When left unset
69
- /// the builder falls back to the SDK's auto-discovery chain
70
- /// (defaults → properties → env) — see
71
- /// [goosefs-sdk `docs/CLIENT_CONFIGURATION.md`](https://github.com/Tencent/tencent-goosefs-rust-sdk/blob/main/docs/CLIENT_CONFIGURATION.md)
72
- /// §1. `build()` fails with `ConfigInvalid` only if **no** source
73
- /// supplies a master address.
67
+ /// This is the lowest-priority source: `build()` uses it only when
68
+ /// neither `GOOSEFS_MASTER_ADDR` nor `goosefs-site.properties` declares a
69
+ /// master address, and fails with `ConfigInvalid` when no source supplies
70
+ /// one. See [`crate::GoosefsConfig::master_addr`] for the full resolution
71
+ /// order and its rationale.
74
72
  pub fn master_addr(mut self, addr: &str) -> Self {
75
73
  if !addr.is_empty() {
76
74
  self.config.master_addr = Some(addr.to_string());
@@ -124,6 +122,160 @@ impl GoosefsBuilder {
124
122
  }
125
123
  }
126
124
 
125
+ /// The source that supplied the master addresses of an auto-loaded SDK config.
126
+ ///
127
+ /// `goosefs_sdk::config::GoosefsConfig::from_properties_auto()` always returns
128
+ /// a master address: when neither `GOOSEFS_MASTER_ADDR` nor
129
+ /// `goosefs-site.properties` declares one, it keeps the SDK default
130
+ /// `127.0.0.1:9200`. The loaded value therefore cannot tell "a real source
131
+ /// configured this" from "nothing configured this", and the builder has to
132
+ /// ask which source actually spoke.
133
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
134
+ enum MasterAddrSource {
135
+ /// `GOOSEFS_MASTER_ADDR` supplied the addresses.
136
+ Env,
137
+ /// `goosefs-site.properties` declared `goosefs.master.rpc.addresses` or
138
+ /// `goosefs.master.hostname`.
139
+ SiteProperties,
140
+ /// Neither spoke; the loaded addresses are the SDK defaults.
141
+ Unset,
142
+ }
143
+
144
+ /// Apply the master-address resolution order to an auto-loaded SDK config.
145
+ ///
146
+ /// `goosefs-site.properties` carries a deployment's whole HA master list,
147
+ /// which a URI authority such as `goosefs://host:9200/path` cannot express, so
148
+ /// a site file that declares masters outranks `explicit` (the `master_addr`
149
+ /// config key, whether it arrived from the builder, the option map, or the URI
150
+ /// authority). `GOOSEFS_MASTER_ADDR` stays on top as the per-process override:
151
+ ///
152
+ /// ```text
153
+ /// GOOSEFS_MASTER_ADDR > goosefs-site.properties > master_addr
154
+ /// ```
155
+ ///
156
+ /// `from_properties_auto()` already resolved the first two sources, so
157
+ /// `explicit` is written into `config` only when neither spoke. Returns
158
+ /// `ConfigInvalid` when `explicit` holds no address after trimming, and when
159
+ /// no source at all supplies one.
160
+ fn apply_master_addr_precedence(
161
+ config: &mut goosefs_sdk::config::GoosefsConfig,
162
+ explicit: Option<&str>,
163
+ ) -> Result<()> {
164
+ let explicit_addrs = match explicit {
165
+ Some(master_addr) => {
166
+ let addrs: Vec<String> = master_addr
167
+ .split(',')
168
+ .map(|s| s.trim().to_string())
169
+ .filter(|s| !s.is_empty())
170
+ .collect();
171
+ if addrs.is_empty() {
172
+ return Err(Error::new(
173
+ ErrorKind::ConfigInvalid,
174
+ "master_addr is empty after trimming",
175
+ )
176
+ .with_operation("Builder::build")
177
+ .with_context("service", GOOSEFS_SCHEME));
178
+ }
179
+ Some(addrs)
180
+ }
181
+ None => None,
182
+ };
183
+
184
+ match detect_master_addr_source() {
185
+ MasterAddrSource::Unset => {
186
+ let Some(addrs) = explicit_addrs else {
187
+ return Err(Error::new(
188
+ ErrorKind::ConfigInvalid,
189
+ "master_addr is not configured: set it via GoosefsBuilder::master_addr(...), \
190
+ the `master_addr` config key, the GOOSEFS_MASTER_ADDR env var, \
191
+ or `goosefs.master.hostname`/`goosefs.master.rpc.addresses` in goosefs-site.properties",
192
+ )
193
+ .with_operation("Builder::build")
194
+ .with_context("service", GOOSEFS_SCHEME));
195
+ };
196
+
197
+ config.master_addr = addrs[0].clone();
198
+ config.master_addrs = if addrs.len() > 1 { addrs } else { Vec::new() };
199
+ }
200
+ source => {
201
+ if let Some(addrs) = explicit_addrs {
202
+ debug!(
203
+ "GoosefsBuilder ignores master_addr {addrs:?}: {source:?} supplies {} (addrs={:?})",
204
+ config.master_addr, config.master_addrs
205
+ );
206
+ }
207
+ }
208
+ }
209
+
210
+ Ok(())
211
+ }
212
+
213
+ /// Report which source supplied the master addresses that
214
+ /// `from_properties_auto()` loaded.
215
+ fn detect_master_addr_source() -> MasterAddrSource {
216
+ // Probe `GOOSEFS_MASTER_ADDR` through the SDK's own parser so that the
217
+ // `gfs://h1:9200,h2:9200/root` URI form and the bare comma list are
218
+ // recognised exactly as `from_properties_auto()` recognises them. Blanking
219
+ // the address first makes an unset, empty, or malformed value observable:
220
+ // in those cases `apply_env()` leaves the field untouched.
221
+ let probe = goosefs_sdk::config::GoosefsConfig {
222
+ master_addr: String::new(),
223
+ master_addrs: Vec::new(),
224
+ ..Default::default()
225
+ }
226
+ .apply_env();
227
+ if !probe.master_addr.is_empty() || !probe.master_addrs.is_empty() {
228
+ return MasterAddrSource::Env;
229
+ }
230
+
231
+ // The SDK searches `$GOOSEFS_CONFIG_FILE`, `$GOOSEFS_CONF_DIR`,
232
+ // `$GOOSEFS_HOME/conf`, `~/.goosefs`, and `/etc/goosefs` for
233
+ // `goosefs-site.properties`. An unreadable file is left to
234
+ // `from_properties_auto()`, which already failed the build above.
235
+ if let Some(path) = goosefs_sdk::config::discover_config_file()
236
+ && let Ok(content) = std::fs::read_to_string(&path)
237
+ && site_properties_declare_master(&content)
238
+ {
239
+ return MasterAddrSource::SiteProperties;
240
+ }
241
+
242
+ MasterAddrSource::Unset
243
+ }
244
+
245
+ /// Report whether `goosefs-site.properties` declares a master address.
246
+ ///
247
+ /// The SDK's properties parser is private, so key lookup repeats its
248
+ /// documented Java `Properties.load()` rules here: `#` and `!` start a comment
249
+ /// line, the first `=` (else the first `:`) separates key from value, and the
250
+ /// last assignment of a key wins. `goosefs.master.rpc.addresses` shadows
251
+ /// `goosefs.master.hostname` whenever it is present, matching the order in
252
+ /// which the SDK consults the two keys.
253
+ fn site_properties_declare_master(content: &str) -> bool {
254
+ let mut addresses = None;
255
+ let mut hostname = None;
256
+
257
+ for line in content.lines() {
258
+ let line = line.trim();
259
+ if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
260
+ continue;
261
+ }
262
+ let Some(sep) = line.find('=').or_else(|| line.find(':')) else {
263
+ continue;
264
+ };
265
+
266
+ match line[..sep].trim() {
267
+ "goosefs.master.rpc.addresses" => addresses = Some(line[sep + 1..].trim()),
268
+ "goosefs.master.hostname" => hostname = Some(line[sep + 1..].trim()),
269
+ _ => {}
270
+ }
271
+ }
272
+
273
+ match addresses {
274
+ Some(addresses) => addresses.split(',').any(|addr| !addr.trim().is_empty()),
275
+ None => hostname.is_some_and(|hostname| !hostname.is_empty()),
276
+ }
277
+ }
278
+
127
279
  impl Builder for GoosefsBuilder {
128
280
  type Config = GoosefsConfig;
129
281
 
@@ -156,8 +308,9 @@ impl Builder for GoosefsBuilder {
156
308
  // * properties file present but malformed → hard-fail
157
309
  // (broken config must not be silently dropped)
158
310
  //
159
- // Builder-explicit fields (Step 2) have the final say, overriding
160
- // anything discovered from properties / env.
311
+ // Builder-explicit fields overlay this config afterwards. The master
312
+ // address is the one exception: Step 2 keeps the properties/env value
313
+ // on top, see `apply_master_addr_precedence`.
161
314
  let mut goosefs_config = goosefs_sdk::config::GoosefsConfig::from_properties_auto()
162
315
  .map_err(|e| {
163
316
  Error::new(
@@ -171,50 +324,15 @@ impl Builder for GoosefsBuilder {
171
324
  // Root always comes from OpenDAL (it's an OpenDAL-layer concept).
172
325
  goosefs_config.root = root.clone();
173
326
 
174
- // ── Step 2: overlay builder-explicit fields (authoritative) ───────────
175
- if let Some(ref master_addr) = self.config.master_addr {
176
- let addrs: Vec<String> = master_addr
177
- .split(',')
178
- .map(|s| s.trim().to_string())
179
- .filter(|s| !s.is_empty())
180
- .collect();
181
-
182
- if addrs.is_empty() {
183
- return Err(Error::new(
184
- ErrorKind::ConfigInvalid,
185
- "master_addr is empty after trimming",
186
- )
187
- .with_operation("Builder::build")
188
- .with_context("service", GOOSEFS_SCHEME));
189
- }
190
-
191
- if addrs.len() == 1 {
192
- goosefs_config.master_addr = addrs[0].clone();
193
- goosefs_config.master_addrs = Vec::new();
194
- } else {
195
- goosefs_config.master_addr = addrs[0].clone();
196
- goosefs_config.master_addrs = addrs;
197
- }
198
- }
199
-
200
- // After properties/env auto-load + builder overlay, we must still
201
- // have at least one usable master address. If we don't, fail fast
202
- // with a message that points the user at all three sources.
203
- if goosefs_config.master_addr.is_empty() && goosefs_config.master_addrs.is_empty() {
204
- return Err(Error::new(
205
- ErrorKind::ConfigInvalid,
206
- "master_addr is not configured: set it via GoosefsBuilder::master_addr(...), \
207
- the `master_addr` config key, the GOOSEFS_MASTER_ADDR env var, \
208
- or `goosefs.master.hostname`/`goosefs.master.rpc.addresses` in goosefs-site.properties",
209
- )
210
- .with_operation("Builder::build")
211
- .with_context("service", GOOSEFS_SCHEME));
212
- }
327
+ // ── Step 2: resolve the master address ────────────────────────────────
328
+ apply_master_addr_precedence(&mut goosefs_config, self.config.master_addr.as_deref())?;
213
329
  debug!(
214
330
  "GoosefsBuilder use master_addr {} (addrs={:?})",
215
331
  goosefs_config.master_addr, goosefs_config.master_addrs
216
332
  );
217
333
 
334
+ // ── Step 3: overlay the remaining builder-explicit fields ─────────────
335
+
218
336
  if let Some(block_size) = self.config.block_size {
219
337
  goosefs_config.block_size = block_size;
220
338
  }
@@ -258,7 +376,7 @@ impl Builder for GoosefsBuilder {
258
376
  goosefs_config = goosefs_config.with_auth_username(auth_username);
259
377
  }
260
378
 
261
- // ── Step 3: validate the final merged config ──────────────────────────
379
+ // ── Step 4: validate the final merged config ──────────────────────────
262
380
  goosefs_config.validate().map_err(|e| {
263
381
  Error::new(
264
382
  ErrorKind::ConfigInvalid,
@@ -411,9 +529,215 @@ impl Service for GoosefsBackend {
411
529
  #[cfg(test)]
412
530
  mod tests {
413
531
  use super::*;
532
+ use goosefs_sdk::config::ENV_CONF_DIR;
533
+ use goosefs_sdk::config::ENV_CONFIG_FILE;
534
+ use goosefs_sdk::config::ENV_HOME;
535
+ use goosefs_sdk::config::ENV_MASTER_ADDR;
536
+ use std::path::PathBuf;
537
+ use std::sync::Mutex;
538
+
539
+ /// `GOOSEFS_*` vars are process-global; serialize every test that
540
+ /// exercises master-address resolution through them.
541
+ static ENV_LOCK: Mutex<()> = Mutex::new(());
542
+
543
+ const SITE_HA: &str =
544
+ "goosefs.master.rpc.addresses=172.31.5.10:9200,172.31.5.2:9200,172.31.5.11:9200\n";
545
+ const SITE_NO_MASTER: &str = "goosefs.user.block.size.bytes.default=4MB\n";
546
+
547
+ /// Write a `goosefs-site.properties` fixture, point `$GOOSEFS_CONFIG_FILE`
548
+ /// at it, and clear the other resolution inputs.
549
+ ///
550
+ /// A file is written even for the "no master keys" cases so that discovery
551
+ /// stops at `$GOOSEFS_CONFIG_FILE` instead of reaching a `~/.goosefs` or
552
+ /// `/etc/goosefs` file that happens to exist on the host.
553
+ fn site_properties(name: &str, content: &str) -> PathBuf {
554
+ let path = std::env::temp_dir().join(format!(
555
+ "opendal_goosefs_{}_{name}_site.properties",
556
+ std::process::id()
557
+ ));
558
+ std::fs::write(&path, content).expect("write goosefs-site.properties");
559
+
560
+ unsafe {
561
+ std::env::set_var(ENV_CONFIG_FILE, &path);
562
+ std::env::remove_var(ENV_CONF_DIR);
563
+ std::env::remove_var(ENV_HOME);
564
+ std::env::remove_var(ENV_MASTER_ADDR);
565
+ }
566
+
567
+ path
568
+ }
569
+
570
+ fn clear_site_properties(path: &PathBuf) {
571
+ unsafe {
572
+ std::env::remove_var(ENV_CONFIG_FILE);
573
+ std::env::remove_var(ENV_MASTER_ADDR);
574
+ }
575
+ let _ = std::fs::remove_file(path);
576
+ }
577
+
578
+ /// Resolve master addresses exactly as `build()` does: auto-load, then
579
+ /// apply the precedence.
580
+ fn resolve(explicit: Option<&str>) -> Result<(String, Vec<String>)> {
581
+ let mut config = goosefs_sdk::config::GoosefsConfig::from_properties_auto()
582
+ .expect("auto-load must succeed");
583
+ apply_master_addr_precedence(&mut config, explicit)?;
584
+ Ok((config.master_addr, config.master_addrs))
585
+ }
586
+
587
+ /// A site file that declares `goosefs.master.rpc.addresses` supplies the
588
+ /// HA list even when the URI authority carries an unreachable address.
589
+ ///
590
+ /// This is the reported failure: with `GOOSEFS_CONFIG_FILE` pointing at a
591
+ /// file that lists the real masters, the client still dialed the dummy
592
+ /// address from `goosefs://192.0.2.5:9999/...`.
593
+ #[test]
594
+ fn site_properties_outrank_uri_authority() {
595
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
596
+ let path = site_properties("site_over_uri", SITE_HA);
597
+
598
+ let (master_addr, master_addrs) =
599
+ resolve(Some("192.0.2.5:9999")).expect("resolution must succeed");
600
+
601
+ assert_eq!(master_addr, "172.31.5.10:9200");
602
+ assert_eq!(
603
+ master_addrs,
604
+ vec![
605
+ "172.31.5.10:9200".to_string(),
606
+ "172.31.5.2:9200".to_string(),
607
+ "172.31.5.11:9200".to_string(),
608
+ ]
609
+ );
610
+
611
+ clear_site_properties(&path);
612
+ }
613
+
614
+ /// Without a URI authority or `GOOSEFS_MASTER_ADDR`, a declared site file
615
+ /// still supplies the masters — `goosefs:///path` must not be rejected for
616
+ /// a missing host.
617
+ #[test]
618
+ fn site_properties_resolve_without_explicit_addr() {
619
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
620
+ let path = site_properties("site_only", SITE_HA);
621
+
622
+ let (master_addr, master_addrs) = resolve(None).expect("resolution must succeed");
623
+
624
+ assert_eq!(master_addr, "172.31.5.10:9200");
625
+ assert_eq!(master_addrs.len(), 3);
626
+
627
+ clear_site_properties(&path);
628
+ }
629
+
630
+ /// `GOOSEFS_MASTER_ADDR` is the per-process override and outranks both the
631
+ /// site file and the URI authority.
632
+ #[test]
633
+ fn env_master_addr_outranks_site_properties_and_uri_authority() {
634
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
635
+ let path = site_properties("env_over_site", SITE_HA);
636
+ unsafe { std::env::set_var(ENV_MASTER_ADDR, "10.0.0.7:9200") };
637
+
638
+ let (master_addr, master_addrs) =
639
+ resolve(Some("192.0.2.5:9999")).expect("resolution must succeed");
640
+
641
+ assert_eq!(master_addr, "10.0.0.7:9200");
642
+ assert!(
643
+ master_addrs.is_empty(),
644
+ "a single env address must not populate the HA list, got {master_addrs:?}"
645
+ );
646
+
647
+ clear_site_properties(&path);
648
+ }
649
+
650
+ /// A site file without master keys leaves `master_addr` in charge, so a
651
+ /// URI authority or option-map address is used as before.
652
+ #[test]
653
+ fn explicit_addr_applies_when_no_source_declares_masters() {
654
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
655
+ let path = site_properties("explicit_only", SITE_NO_MASTER);
656
+
657
+ let (master_addr, master_addrs) =
658
+ resolve(Some("10.0.0.1:9200,10.0.0.2:9200")).expect("resolution must succeed");
659
+
660
+ assert_eq!(master_addr, "10.0.0.1:9200");
661
+ assert_eq!(
662
+ master_addrs,
663
+ vec!["10.0.0.1:9200".to_string(), "10.0.0.2:9200".to_string()]
664
+ );
665
+
666
+ clear_site_properties(&path);
667
+ }
668
+
669
+ /// When no source declares a master address, `build()` must fail instead
670
+ /// of silently dialing the SDK default `127.0.0.1:9200`.
671
+ #[test]
672
+ fn missing_master_addr_fails() {
673
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
674
+ let path = site_properties("missing_master", SITE_NO_MASTER);
675
+
676
+ let err = resolve(None).expect_err("resolution must fail without any master address");
677
+ assert_eq!(err.kind(), ErrorKind::ConfigInvalid);
678
+ assert!(
679
+ err.to_string().contains("master_addr is not configured"),
680
+ "unexpected error message: {err}"
681
+ );
682
+
683
+ clear_site_properties(&path);
684
+ }
685
+
686
+ /// `GOOSEFS_MASTER_ADDR` also accepts the SDK's `gfs://` URI form, and
687
+ /// resolution must recognise it as a real source rather than falling
688
+ /// through to `master_addr`.
689
+ #[test]
690
+ fn env_master_addr_accepts_gfs_uri_form() {
691
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
692
+ let path = site_properties("env_gfs_uri", SITE_NO_MASTER);
693
+ unsafe { std::env::set_var(ENV_MASTER_ADDR, "gfs://10.0.0.1:9200,10.0.0.2:9200/lance") };
694
+
695
+ let (master_addr, master_addrs) =
696
+ resolve(Some("192.0.2.5:9999")).expect("resolution must succeed");
697
+
698
+ assert_eq!(master_addr, "10.0.0.1:9200");
699
+ assert_eq!(master_addrs.len(), 2);
700
+
701
+ clear_site_properties(&path);
702
+ }
703
+
704
+ #[test]
705
+ fn site_properties_declare_master_reads_both_keys() {
706
+ assert!(site_properties_declare_master(
707
+ "goosefs.master.rpc.addresses=10.0.0.1:9200,10.0.0.2:9200\n"
708
+ ));
709
+ assert!(site_properties_declare_master(
710
+ "goosefs.master.hostname = master-1\ngoosefs.master.rpc.port=9200\n"
711
+ ));
712
+ // Java `Properties.load()` also accepts `:` as the separator.
713
+ assert!(site_properties_declare_master(
714
+ "goosefs.master.hostname:master-1\n"
715
+ ));
716
+ }
717
+
718
+ #[test]
719
+ fn site_properties_declare_master_rejects_absent_and_blank_keys() {
720
+ assert!(!site_properties_declare_master(SITE_NO_MASTER));
721
+ assert!(!site_properties_declare_master(
722
+ "# goosefs.master.rpc.addresses=10.0.0.1:9200\n! goosefs.master.hostname=master-1\n"
723
+ ));
724
+ assert!(!site_properties_declare_master(
725
+ "goosefs.master.hostname=\n"
726
+ ));
727
+ assert!(!site_properties_declare_master(
728
+ "goosefs.master.rpc.addresses= , ,\n"
729
+ ));
730
+ // The SDK consults `addresses` first and never falls back to
731
+ // `hostname` once the key is present, so a blank list declares
732
+ // nothing here either.
733
+ assert!(!site_properties_declare_master(
734
+ "goosefs.master.rpc.addresses=\ngoosefs.master.hostname=master-1\n"
735
+ ));
736
+ }
414
737
 
415
738
  #[test]
416
739
  fn test_builder_build() {
740
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
417
741
  let builder = GoosefsBuilder::default()
418
742
  .root("/data")
419
743
  .master_addr("127.0.0.1:9200")
@@ -423,6 +747,7 @@ mod tests {
423
747
 
424
748
  #[test]
425
749
  fn test_builder_ha() {
750
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
426
751
  let builder = GoosefsBuilder::default()
427
752
  .root("/data")
428
753
  .master_addr("10.0.0.1:9200,10.0.0.2:9200,10.0.0.3:9200")
@@ -437,6 +762,7 @@ mod tests {
437
762
  /// before any auto-load value can rescue it.
438
763
  #[test]
439
764
  fn test_builder_blank_master_addr_fails() {
765
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
440
766
  let err = GoosefsBuilder::default()
441
767
  .root("/data")
442
768
  .master_addr(" , , ")
@@ -451,6 +777,7 @@ mod tests {
451
777
 
452
778
  #[test]
453
779
  fn test_capability_rename_with_if_not_exists() {
780
+ let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
454
781
  let backend = GoosefsBuilder::default()
455
782
  .root("/data")
456
783
  .master_addr("127.0.0.1:9200")
@@ -46,12 +46,18 @@ pub struct GoosefsConfig {
46
46
  ///
47
47
  /// Resolution precedence at `build()` time (highest → lowest), following
48
48
  /// [goosefs-sdk `docs/CLIENT_CONFIGURATION.md`](https://github.com/Tencent/tencent-goosefs-rust-sdk/blob/main/docs/CLIENT_CONFIGURATION.md) §1:
49
- /// 1. This field (when set on the builder / OpenDAL config map)
50
- /// 2. `GOOSEFS_MASTER_ADDR` environment variable
51
- /// 3. `goosefs.master.rpc.addresses` / `goosefs.master.hostname` in
49
+ /// 1. `GOOSEFS_MASTER_ADDR` environment variable
50
+ /// 2. `goosefs.master.rpc.addresses` / `goosefs.master.hostname` in
52
51
  /// `goosefs-site.properties`
52
+ /// 3. This field (from the builder, the OpenDAL config map, or the URI
53
+ /// authority of `goosefs://host:port/path`)
53
54
  ///
54
- /// `build()` fails with `ConfigInvalid` only when **none** of the above
55
+ /// A site file that declares masters therefore outranks this field: the
56
+ /// file carries the deployment's whole HA master list, which a single URI
57
+ /// authority cannot express. Set `GOOSEFS_MASTER_ADDR` to override a
58
+ /// deployed site file for one process.
59
+ ///
60
+ /// `build()` fails with `ConfigInvalid` when **none** of the above
55
61
  /// supplies a master address.
56
62
  pub master_addr: Option<String>,
57
63
 
@@ -164,12 +170,14 @@ mod tests {
164
170
 
165
171
  /// HA note: since URIs don't allow commas in `host`, HA mode is
166
172
  /// expressed through the extra-option/env-var pathway, not through the
167
- /// URI authority. When both are present the URI authority **wins** —
168
- /// this is the deliberate contract of `from_uri` (it ensures a user who
169
- /// typed `goosefs://host:port/` can't be silently overridden by a stale
170
- /// env var). The full HA list therefore reaches `build()` only when the
171
- /// caller goes through `from_iter` / `Operator::via_iter` without the
172
- /// URI authority, which is covered by [`from_iter_picks_up_every_known_field`].
173
+ /// URI authority. When both are present the URI authority **wins** the
174
+ /// `master_addr` slot — a user who typed `goosefs://host:port/` must not
175
+ /// be silently redirected by a stale option map. The full HA list
176
+ /// therefore reaches `build()` only when the caller goes through
177
+ /// `from_iter` / `Operator::via_iter` without the URI authority, which is
178
+ /// covered by [`from_iter_picks_up_every_known_field`], or through
179
+ /// `goosefs-site.properties` / `GOOSEFS_MASTER_ADDR`, which outrank
180
+ /// `master_addr` inside `build()`.
173
181
  #[test]
174
182
  fn from_uri_authority_overrides_extra_master_addr() {
175
183
  let uri = OperatorUri::new(
@@ -19,12 +19,12 @@ use std::fmt::Debug;
19
19
  use std::sync::Arc;
20
20
  use std::sync::atomic::{AtomicU32, Ordering};
21
21
 
22
+ use asyncband::rwlock::RwLock;
22
23
  use goosefs_sdk::client::MasterClient;
23
24
  use goosefs_sdk::config::GoosefsConfig as ClientConfig;
24
25
  use goosefs_sdk::context::FileSystemContext;
25
26
  use goosefs_sdk::io::{GoosefsFileReader, GoosefsFileWriter};
26
27
  use goosefs_sdk::proto::grpc::file::FileInfo;
27
- use tokio::sync::RwLock;
28
28
 
29
29
  use opendal_core::raw::*;
30
30
  use opendal_core::*;
@@ -23,6 +23,8 @@ which means it connects directly to GooseFS Master (port 9200) and
23
23
  Worker (port 9203) without requiring a Proxy component.
24
24
 
25
25
  Features:
26
+ - **Read metadata**: Streams expose full-file metadata from the SDK reader before
27
+ consuming the body. Bounded reads do not issue a separate stat to obtain it.
26
28
  - **HA support**: Comma-separated master addresses for automatic Primary Master discovery.
27
29
  - **Block-level I/O**: Data reads/writes go through block-level gRPC bidirectional streaming.
28
30
  - **Consistent hash routing**: Worker selection uses consistent hashing on block IDs.
@@ -32,6 +34,9 @@ Features:
32
34
  - **Rename parent directories**: `rename` creates a missing destination parent only
33
35
  when Master reports it missing. Write-via-temp publish does not call
34
36
  `CreateDirectory` for a parent that `CreateFile(recursive)` already created.
37
+ - **Optional client caches**: `goosefs-sdk` 0.2.1 keeps metadata and page caches
38
+ behind Cargo features so the default build stays a gRPC client. Enable them
39
+ on this crate; see [Optional client caches](#optional-client-caches).
35
40
 
36
41
  ## Configuration
37
42
 
@@ -39,6 +44,71 @@ Use [`crate::GoosefsConfig`] for serializable configuration and this builder's
39
44
  methods for direct construction. The field and method documentation defines
40
45
  accepted values, defaults, and environment interaction.
41
46
 
47
+ ### Master address resolution
48
+
49
+ `build()` resolves the master addresses from three sources, highest priority
50
+ first:
51
+
52
+ 1. the `GOOSEFS_MASTER_ADDR` environment variable, either a comma-separated
53
+ list or the SDK's `gfs://h1:9200,h2:9200/root` URI form;
54
+ 2. `goosefs.master.rpc.addresses` or `goosefs.master.hostname` in
55
+ `goosefs-site.properties`, discovered through `$GOOSEFS_CONFIG_FILE`,
56
+ `$GOOSEFS_CONF_DIR`, `$GOOSEFS_HOME/conf`, `~/.goosefs`, and `/etc/goosefs`.
57
+ Use `$GOOSEFS_CONFIG_FILE` to point at a file outside those directories;
58
+ 3. the `master_addr` config key, which also receives the URI authority of
59
+ `goosefs://host:port/path`.
60
+
61
+ A site file that declares masters outranks `master_addr` because the file
62
+ carries a deployment's whole HA master list, which a single URI authority
63
+ cannot express. Set `GOOSEFS_MASTER_ADDR` to override a deployed site file for
64
+ one process. `build()` fails with `ConfigInvalid` when no source supplies an
65
+ address; it never falls back to `127.0.0.1:9200`.
66
+
67
+ | `goosefs-site.properties` | `GOOSEFS_MASTER_ADDR` | `master_addr` / URI authority | Master addresses used |
68
+ | --- | --- | --- | --- |
69
+ | declares masters | set | any | `GOOSEFS_MASTER_ADDR` |
70
+ | declares masters | unset | any or absent | site file |
71
+ | absent, or no master keys | set | any | `GOOSEFS_MASTER_ADDR` |
72
+ | absent, or no master keys | unset | set | `master_addr` |
73
+ | absent, or no master keys | unset | absent | none — `ConfigInvalid` |
74
+
75
+ ### Optional client caches
76
+
77
+ `goosefs-sdk` 0.2.1 compiles the client metadata cache and the disk-backed page
78
+ cache only when their Cargo features are enabled. The default
79
+ `services-goosefs` / `opendal-service-goosefs` build stays a gRPC client.
80
+
81
+ Enable a cache on this crate. Applications that use the `opendal` facade still
82
+ add `opendal-service-goosefs` as a direct dependency so Cargo unifies the
83
+ features into the same build:
84
+
85
+ | Feature | Compiles |
86
+ | --- | --- |
87
+ | `metadata-cache` | Process-local status / listing LRU |
88
+ | `page-cache` | Disk-backed page cache via `tokio::fs` |
89
+ | `page-cache-io-uring` | Page cache plus the Linux io_uring backend |
90
+
91
+ ```shell
92
+ cargo add opendal --features services-goosefs
93
+ cargo add opendal-service-goosefs --features metadata-cache,page-cache-io-uring
94
+ ```
95
+
96
+ `page-cache-io-uring` already enables `page-cache`. On non-Linux targets the
97
+ SDK still builds the portable `tokio::fs` store; io_uring is Linux-only.
98
+
99
+ Compiling a cache does not replace runtime configuration. `build()` loads
100
+ `goosefs-site.properties` and `GOOSEFS_*` environment variables through
101
+ `GoosefsConfig::from_properties_auto()`:
102
+
103
+ - **Metadata cache**: on by default once `metadata-cache` is compiled. Set
104
+ `GOOSEFS_METADATA_CACHE_ENABLED=false` to disable it.
105
+ - **Page cache**: off by default. Set `GOOSEFS_USER_CLIENT_CACHE_ENABLED=true`
106
+ and configure cache directories through
107
+ `goosefs.user.client.cache.dirs` / `GOOSEFS_USER_CLIENT_CACHE_DIRS`.
108
+
109
+ See [goosefs-sdk client configuration](https://github.com/Tencent/tencent-goosefs-rust-sdk/blob/main/docs/CLIENT_CONFIGURATION.md)
110
+ for the full cache knob list.
111
+
42
112
  ## Example
43
113
 
44
114
  ### Via Builder
@@ -140,8 +210,7 @@ This service is covered by all three OpenDAL test layers:
140
210
  `create_dir`). Start the fixture and point the harness at it:
141
211
 
142
212
  ```shell
143
- # Start a single-container GooseFS (master + worker + job_master +
144
- # job_worker + table_master)
213
+ # Start a single-container GooseFS (master + worker; see start-default.sh)
145
214
  docker compose -f fixtures/goosefs/docker-compose-goosefs.yml up -d --wait
146
215
 
147
216
  OPENDAL_TEST=goosefs \
@@ -158,7 +227,7 @@ This service is covered by all three OpenDAL test layers:
158
227
  3. **GitHub CI** — `.github/services/goosefs/goosefs/action.yml` is picked up
159
228
  automatically by `.github/scripts/test_behavior/plan.py::provided_cases()`
160
229
  and runs the fixture + behavior suite on every PR. The fixture image
161
- (`goosefs.tencentcloudcr.com/goosefs/repo:v2.1.0`) is public so no secrets
230
+ (`ghcr.io/tencent/tencent-goosefs-rust-sdk/goosefs:v2.0.0`) is public so no secrets
162
231
  are needed.
163
232
 
164
233
  The fixture also exposes a `distributed` compose profile (separate master /