Skip to content

refactor!: move the S3 storage backend to the fess-storage-s3 plugin - #3428

Merged
marevol merged 2 commits into
masterfrom
feat/extract-s3-storage
Sep 10, 2026
Merged

marevol merged 2 commits into
masterfrom
feat/extract-s3-storage

Conversation

@marevol

@marevol marevol commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Merge order. This needs codelibs/fess-crawler#204 merged and its snapshot published first, and CI here cannot pass until then — see "Why CI is red" below. It also pairs with the new fess-storage-s3 plugin repository. Reviewable now; not mergeable yet.

Moves the S3 storage backend out of the distribution, the way #3424 moved Google Cloud Storage. The AWS SDK for S3 is 30 jars, 8,799,602 bytes that every installation carried, on its own release cadence, for a backend most installations never configure.

What moves and what stays

S3StorageClient becomes org.codelibs.fess.storage.s3.S3StorageClient in the plugin, so fess_storage.xml declares no component of its own. The file and the fess.xml include stay: fess_storage++.xml merges into it, and without a base file the plugins would have nowhere to contribute to. The plugin registers both s3StorageClient and s3_compatStorageClient, which is what keeps storage.type=auto working for MinIO and other S3-compatible endpoints once installed.

Staying in core: StorageType, StorageClientFactory's storage.type-to-component-name mapping and its endpoint detection, and the four s3: literals in ProtocolHelper — those are string comparisons with no class behind them, and dropping them would break /go/ links, the crawling wizard and file name decoding.

Keeping the jars out of the war is the exclusion on fess-crawler-lasta, not the removal of the direct declaration: fess-crawler still compiles S3Client and the s3: URL handler against the SDK.

The netty-nio-client exclusion the direct declaration carried is not repeated, because it had nothing to remove — awssdk:s3 2.54.2 uses apache5-client for its synchronous transport and does not depend on netty-nio-client at any scope.

Dependency measurement

mvn dependency:list -DincludeScope=runtime before and after: 286 runtime artifacts to 256. Exactly 30 removals, no additions, and no version changes, so nothing that stays behind fell back to a different version.

crawler.file.protocols

Loses s3 in all three places that spell it out — fess_config.properties, the two FessConfig javadoc comments, and the compiled defaultMap. Missing that last one is what #3410 had to fix for the removed storage protocol. The plugin adds the protocol back through ProtocolHelper.addFileProtocol when installed.

Tests

StorageClientFactoryTest asserted that every type the distribution serves has a component, which inverts here. test_noTypeIsServedByCore walks StorageType.values() plus auto and requires hasComponent to be false for each; test_app.xml includes the shipped fess_storage.xml, so re-adding a client to core — and with it an SDK to the war — fails a test rather than only showing up in the artifact. test_detectStorageType_mapsEndpointsToTypes is new and pins the detection table. The prototype assertion is dropped because core has no component left to assert it against; that contract belongs to the plugins.

The second commit annotates StorageTypeTest and StorageItemTest, which are written in the JUnit 3 style — public void test_* with no annotation — and so contributed no tests at all: neither appears in any of the 462 surefire reports of a full run. Annotating them runs 17 tests, which pass as written. It matters here because StorageType is one of the two things core still contributes toward reaching a plugin, and the test pinning it was pinning nothing. 27 test classes in this repository have the same shape, roughly 65 methods; only the two in the storage package are touched.

mvn test7696 tests, 0 failures.

Runtime verification

A LastaDi container was booted on a plain JVM with fess_storage.xml, crawler/client.xml and crawler/mimetype.xml, on a class path built the way Fess builds one: WEB-INF/classes, then the war's runtime jars from this branch, then the plugin jar.

With the plugin (and fess-crawler#204): s3StorageClient and s3_compatStorageClient both resolve to the plugin's class, both are prototypes, and clientFactory maps s3://bucket/key to org.codelibs.fess.crawler.client.s3.S3Client — the class from fess-crawler, registered by the plugin. file: unaffected.

Without the plugin — the state of an installation that upgrades and does not install it — the container still initializes, so the webapp starts. hasComponent is false for s3StorageClient, s3_compatStorageClient and gcsStorageClient; clientFactory builds; getClient("s3://bucket/k") returns null; file: still resolves. That is the designed degradation, and #3427 is what makes it visible rather than silent.

Storage operations were then exercised for real against MinIO on that same class path — the war's jars with no AWS SDK, plus the plugin jar. ensureBucketExists, isAvailable, uploadObject, listObjects at two prefix levels, downloadObject, setObjectTags/getObjectTags and deleteObject: 10 checks, all passing. Worth noting what that resolves: the SDK's HTTP service implementation loads from the plugin jar while httpclient5 loads from the war at 5.6.1, which is older than the 5.6.4 apache5-client asks for. That pairing is not new — the war ships the same combination today — and it works.

Why CI is red

master's fess-crawler-lasta still declares s3Client in crawler/client.xml, and LastaDi resolves a class attribute when it parses the XML, not when the component is first used. With the SDK excluded, booting Fess therefore fails at container init:

DiXmlParseFailureException: Failed to parse the dependency XML.
[Dependency XML] crawler/client.xml
Caused by: java.lang.NoClassDefFoundError: software/amazon/awssdk/core/ResponseInputStream

Reproduced on a plain JVM, and it is the CI's "Run Fess" step that will hit it. The unit suite does not, because test_app.xml does not include crawler/client.xml — which is exactly why the merge order is a requirement.

The AWS SDK for S3 is 30 jars the distribution carried for every installation,
on its own release cadence, to serve a backend most installations never
configure. It ships in the fess-storage-s3 plugin now, alongside the S3
crawler client registration, the way fess-storage-gcs already ships the Google
Cloud Storage SDK.

S3StorageClient moves to org.codelibs.fess.storage.s3 in that plugin, so
fess_storage.xml declares no component of its own. The file and the fess.xml
include stay: fess_storage++.xml merges into it, and without a base file the
plugins would have nowhere to contribute to. The plugin registers both
s3StorageClient and s3_compatStorageClient, which is what keeps
storage.type=auto working for MinIO and other S3-compatible endpoints once it
is installed.

Keeping the AWS jars out of the war is the exclusion on fess-crawler-lasta,
not the removal of the direct declaration: fess-crawler still compiles
S3Client and the s3: URL handler against the SDK. The netty-nio-client
exclusion the direct declaration carried is not repeated because it had
nothing to remove -- awssdk:s3 2.54.2 uses apache5-client for its synchronous
transport and does not depend on netty-nio-client at any scope.

Measured with mvn dependency:list -DincludeScope=runtime before and after:
286 runtime artifacts to 256. Exactly 30 removals, no additions, and no
version changes, so nothing that stays behind fell back to a different
version. The 30 jars are 8,799,602 bytes.

crawler.file.protocols loses s3 from its shipped default in all three places
that spell it out: fess_config.properties, the two FessConfig javadoc
comments, and the compiled defaultMap. Missing that last one is what #3410
had to fix for the removed storage protocol. The plugin adds the protocol back
through ProtocolHelper.addFileProtocol when it is installed. The s3: literals
in ProtocolHelper itself stay: they are string comparisons with no class
reference behind them, and dropping them would break /go/ links, the crawling
wizard and file name decoding.

StorageClientFactory keeps the storage.type-to-component-name mapping and the
endpoint detection, which is now all core contributes toward reaching a
plugin. Its exception names both plugins instead of only fess-storage-gcs.

StorageClientFactoryTest asserted that every type the distribution serves has
a component, which inverts: test_noTypeIsServedByCore now walks
StorageType.values() and auto, and requires hasComponent to be false for each.
test_app.xml includes the shipped fess_storage.xml, so re-adding a client to
core -- and with it an SDK to the war -- fails a test rather than only showing
up in the artifact. test_detectStorageType_mapsEndpointsToTypes is new and
fixes the detection table directly. The prototype assertion is dropped here
because core has no component left to assert it against; that contract belongs
to the plugins now.

Pairs with the fess-crawler change that stops registering s3Client, which has
to be merged and published first: crawler/client.xml still naming the
component while the SDK is gone makes every CrawlerClientFactory fail to
build, and the unit suite does not catch it because test_app.xml does not
include crawler/client.xml.
StorageTypeTest and StorageItemTest are written in the JUnit 3 style the suite
used before: public void test_* on a UnitFessTestCase subclass, with no
annotation. UnitFessTestCase runs on JUnit 5 through utflute, which discovers
tests by annotation, so neither class contributed a single test -- they are
absent from all 462 surefire reports of a full mvn test run.

Annotating them makes 17 tests run, and they pass as written.

This matters here because StorageType is one of the two things core still
contributes toward reaching a storage plugin, now that the clients themselves
ship as fess-storage-*: StorageTypeTest is what pins its constants, and it was
pinning nothing.

27 test classes in this repository have the same shape, roughly 65 test
methods; only the two in the storage package are annotated here.
@marevol marevol self-assigned this Sep 10, 2026
@marevol marevol added the task label Sep 10, 2026
@marevol marevol added this to the 15.9.0 milestone Sep 10, 2026
marevol added a commit to codelibs/fess-parent that referenced this pull request Sep 10, 2026
…lished (#80)

These two repositories are how one repository in this family sees another's
change, and Maven's default updatePolicy for a snapshot is daily. A build whose
local repository already resolved a -SNAPSHOT earlier the same day never asks
again, so a CI job that caches ~/.m2 cannot see a sibling's snapshot published
minutes ago. updatePolicy always removes that window.

Measured, not assumed. codelibs/fess-crawler#204 merged at 02:48:54 and its
snapshot deployed at 02:49:31. codelibs/fess#3428, which depends on it, was
re-run at 02:59:29 -- ten minutes after the fix was published -- and failed with

  DiXmlParseFailureException: crawler/client.xml included by app.xml
  Caused by: NoClassDefFoundError: software/amazon/awssdk/auth/credentials/AwsCredentials

because it built against the previous fess-crawler-lasta. The job log contains
no request to maven.codelibs.org at all: not one "Downloading from
codelibs.org.snapshot" line, while "Cache restored from key: Linux-maven-..."
sits at the top. The published jar was correct the whole time.

Reproduced and fixed locally on the same shape of cache. Before, the resolved
fess-crawler-lasta-15.9.0-SNAPSHOT.jar declared one s3Client component; after
installing this pom and resolving once, it declares none and names the plugin
that took over, and the timestamped build the fix landed in appears in ~/.m2.
The Fess branch's DI container then built clientFactory and mapped
s3://bucket/key, using nothing but published artifacts.

Every consumer inherits this, which is the point: the plugin repositories'
CI installs this pom from source and then resolves org.codelibs.fess:fess from
these repositories, so they are in the same window whenever core publishes.

The cost is one metadata request per snapshot dependency per build. Offline
builds skip it, and a release build has no snapshot dependencies to check.
@marevol
marevol merged commit b525bc6 into master Sep 10, 2026
2 of 5 checks passed
@marevol
marevol deleted the feat/extract-s3-storage branch September 16, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant