Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,31 @@
* limitations under the License.
*/

package org.apache.fluss.annotation.docs;

import org.apache.fluss.annotation.Internal;
package org.apache.fluss.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** Annotation used to override the default value string in the documentation. */
/** Annotations for generating configuration documentation. */
@Internal
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigOverrideDefault {
String value();
public @interface Documentation {

/** Annotation to mark the section a config option belongs to. */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Section {
String value();
}

/**
* Annotation to override the default value in documentation. Useful for dynamic defaults like
* system paths or timezones.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OverrideDefault {
String value();
}
}

This file was deleted.

293 changes: 250 additions & 43 deletions fluss-common/src/main/java/org/apache/fluss/config/ConfigOptions.java

Large diffs are not rendered by default.

90 changes: 77 additions & 13 deletions fluss-docgen/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,94 @@
# Fluss Documentation Generator
# Apache Fluss Documentation Generator

This module contains utilities to automatically generate documentation parts from the Fluss source code. This ensures that the documentation stays in sync with the actual implementation and default values.
This module contains utilities to automatically generate documentation from the
Fluss source code, ensuring that the documentation stays in sync with the actual
implementation, default values, and configuration types.

## Configuration Options Generator

The `ConfigOptionsDocGenerator` scans the `ConfigOptions` class and generates an MDX partial file containing categorized tables of all available configuration settings.
The `ConfigOptionsDocGenerator` scans the `ConfigOptions` class and generates an
MDX partial file containing categorized documentation of all available settings.

### How it works
1. It uses reflection to find all `ConfigOption` fields in the `ConfigOptions` class.
2. It groups options into sections based on the `@ConfigSection` annotation or key prefixes.
3. It handles special default value formatting via `@ConfigOverrideDefault`.
4. It outputs an MDX partial file (`_partial_config.mdx`) to the `website/docs/_configs/` directory for direct import into the Docusaurus site.

### Running the Generator
1. **MDX Partial Strategy**: Produces a reusable `_partial_config.mdx` file that
Docusaurus treats as a React component, embeddable in any documentation page.

2. **Logical Grouping**: Uses the `@Documentation.Section` annotation to categorize
options into sections (e.g., `Client`, `Server`, `ZooKeeper`) that match the
existing Fluss documentation hierarchy. Options without a `@Documentation.Section`
annotation are intentionally excluded from the generated output.

3. **Environment-Agnostic Defaults**: Uses the `@Documentation.OverrideDefault`
annotation to replace system-specific values (such as local `/tmp` paths or
system timezone strings) with documentation-friendly placeholders.

4. **Type Awareness**: Automatically extracts and formats complex types such as
`Duration`, `MemorySize`, `Boolean`, and enum types. Sub-second `Duration`
values are formatted as milliseconds (e.g., `100 ms`). Near-infinite values
for `Duration` and `MemorySize` are rendered as `infinite`.

5. **JSX Safety**: Handles character escaping for symbols like `{`, `}`, and `<`,
and strips Markdown link syntax to prevent React hydration errors during the
website build.

6. **Deprecated Options**: Options annotated with `@Deprecated` are rendered with
a visible deprecation notice and still appear in the documentation for reference.

To update the configuration documentation, run the following command from the project root:
7. **Output**: Produces `website/docs/_configs/_partial_config.mdx` with unique
section anchors (e.g., `{#key-name}`) for deep linking.

### Running the Generator

To regenerate the configuration documentation, run the following command from the
project root:
```bash
./mvnw compile -pl fluss-docgen -am
```

## Integration with Website
The updated file will be written to `website/docs/_configs/_partial_config.mdx`.

### Keeping Documentation in Sync

When adding or modifying a `ConfigOption` in `ConfigOptions.java`, follow these steps
to ensure it appears correctly in the documentation:

The generated file is stored in `website/docs/_configs/_partial_config.mdx`. To display these tables in the documentation, use the MDX import syntax in any `.md` or `.mdx` file:
1. **Assign a section** by adding `@Documentation.Section("SectionName")` above the
field. Options without this annotation are excluded from the generated output.

2. **Override system-dependent defaults** by adding `@Documentation.OverrideDefault("value")`
above the field. This is required for options whose default value is derived at
runtime (e.g., `System.getProperty("java.io.tmpdir")`, `ZoneId.systemDefault()`,
`Long.MAX_VALUE`, `Integer.MAX_VALUE`).

3. **Add a description** via `.withDescription(...)` on the `ConfigOption` builder.
Options with no description will render `(No description provided.)` in the docs.

4. **Re-run the generator** using the command above to regenerate `_partial_config.mdx`.

Example of a well-annotated option:
```java
@Documentation.Section("Client")
@Documentation.OverrideDefault("/tmp/fluss")
public static final ConfigOption<String> CLIENT_SCANNER_IO_TMP_DIR =
key("client.scanner.io.tmpdir")
.stringType()
.defaultValue(System.getProperty("java.io.tmpdir") + "/fluss")
.withDescription(
"Local directory used by the client for storing data files "
+ "(such as kv snapshot and log segment files) temporarily.");
```

## Integration with Website

The generated file is stored at `website/docs/_configs/_partial_config.mdx`.
To display the documentation in any `.md` or `.mdx` page, use the MDX import syntax:
```markdown
import PartialConfig from '../_configs/_partial_config.mdx';
<PartialConfig></PartialConfig>
```

<PartialConfig />
```

> **Note**: Adjust the relative import path based on the location of the importing
> file within the `website/docs/` directory. For example, a file located at
> `website/docs/maintenance/configuration.md` uses `../_configs/_partial_config.mdx`.
7 changes: 4 additions & 3 deletions fluss-docgen/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@
<version>3.1.0</version>
<executions>
<execution>
<phase>compile</phase> <goals>
<goal>java</goal>
</goals>
<phase>compile</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,80 @@
import org.apache.fluss.config.MemorySize;

import java.time.Duration;
import java.util.Collection;
import java.util.stream.Collectors;

/** Utility class for formatting configuration options into human-readable documentation. */
public class ConfigDocUtils {

/**
* Formats the default value of a {@link ConfigOption} for documentation purposes.
*
* @param option The configuration option to format.
* @return A string representation of the default value.
*/
/** The threshold for considering a duration or memory size as infinite. */
private static final long INFINITE_THRESHOLD = 9223372036L;

public static String formatDefaultValue(ConfigOption<?> option) {
Object value = option.defaultValue();

if (value == null) {
return "none";
}

// Handle Duration: Convert ISO-8601 (PT15M) to human-readable (15 min).
if (value instanceof Duration) {
Duration d = (Duration) value;
long seconds = d.getSeconds(); // Use Java 8 compatible method.
return formatDuration((Duration) value);
}

if (seconds == 0) {
return "0 s";
}
if (seconds >= 3600 && seconds % 3600 == 0) {
return (seconds / 3600) + " hours";
}
if (seconds >= 60 && seconds % 60 == 0) {
return (seconds / 60) + " min";
if (value instanceof MemorySize) {
MemorySize mem = (MemorySize) value;
// Handle max values to avoid showing raw bytes
if (mem.getBytes() >= Long.MAX_VALUE || mem.getBytes() < 0) {
return "infinite";
}
return seconds + " s";
return value.toString().toLowerCase();
}

// Handle MemorySize: Uses internal toString() for human-readable units (e.g., 64 mb).
if (value instanceof MemorySize) {
return value.toString();
if (value instanceof Collection) {
Collection<?> col = (Collection<?>) value;
if (col.isEmpty()) {
return "none";
}
return "[" + col.stream().map(String::valueOf).collect(Collectors.joining(", ")) + "]";
}

// Handle Strings: Specifically check for empty values.
if (value instanceof String && ((String) value).isEmpty()) {
return "(empty)";
}

return value.toString();
return String.valueOf(value);
}

private static String formatDuration(Duration d) {
long seconds = d.getSeconds();
int nanos = d.getNano();

if (seconds >= INFINITE_THRESHOLD || seconds < 0) {
return "infinite";
}
if (seconds == 0 && nanos == 0) {
return "0 s";
}
// Handle sub-second durations
if (seconds == 0) {
long millis = nanos / 1_000_000;
if (millis > 0 && nanos % 1_000_000 == 0) {
return millis + " ms";
}
return nanos + " ns";
}
if (seconds >= 86400 && seconds % 86400 == 0) {
long days = seconds / 86400;
return days + (days == 1 ? " day" : " days");
}
if (seconds >= 3600 && seconds % 3600 == 0) {
long hours = seconds / 3600;
return hours + (hours == 1 ? " hour" : " hours");
}
if (seconds >= 60 && seconds % 60 == 0) {
long mins = seconds / 60;
return mins + " min";
}
return seconds + " s";
}
}
Loading
Loading