Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/ci-build-manual-full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,16 @@ jobs:
GROBID_VERSION=${{ steps.version.outputs.value }}
tags: |
lfoppiano/grobid:latest-full,lfoppiano/grobid:${{ github.event.inputs.custom_tag }}
cache-from: type=gha,scope=full
cache-to: type=gha,mode=max,scope=full
# Registry cache rather than type=gha. The GitHub Actions cache is capped at
# 10GB per repository with LRU eviction, and mode=max on this image exports far
# more than that -- the torch/CUDA wheels alone are ~5GB and the embeddings lmdb
# ~2GB -- so entries were being evicted between runs and the "cached" layers
# rebuilt anyway. Docker Hub has no such ceiling, and the push happens over the
# login already done above.
#
# ignore-error keeps a failed cache export from failing the build itself.
cache-from: type=registry,ref=lfoppiano/grobid:buildcache
cache-to: type=registry,ref=lfoppiano/grobid:buildcache,mode=max,ignore-error=true
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Docker Image Summary
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/ci-build-unstable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@ jobs:
- name: Check code formatting
run: ./gradlew spotlessCheck
continue-on-error: false
# jacocoTestReport is deliberately not invoked here: it writes per-subproject reports
# that nothing downstream reads. Coveralls is fed by the aggregate `codeCoverageReport`
# task, which the `coveralls` step below derives from the test execution data.
- name: Build and test (with coverage)
if: matrix.os == 'ubuntu-latest'
run: ./gradlew assemble test --info --stacktrace

# Every other leg skips the jacoco agent: its output is never published, and the agent
# instruments classes as they load in the test JVM.
- name: Build and test
run: ./gradlew assemble test jacocoTestReport --info --stacktrace
if: matrix.os != 'ubuntu-latest'
run: ./gradlew assemble test -PskipCoverage --info --stacktrace

- name: Run Coveralls Gradle task
if: matrix.os == 'ubuntu-latest'
Expand Down
62 changes: 44 additions & 18 deletions Dockerfile.delft
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ RUN unzip -o /opt/grobid-source/grobid-home/build/distributions/grobid-home-*.zi
chmod -R 755 /opt/grobid/grobid-home/pdfalto
RUN rm -rf grobid-source

# -----------------------------------------------
# JDK, as a bind-mount source for the JEP build
# -----------------------------------------------

# Same image the builder starts from, so it costs no extra pull, but -- unlike `builder` --
# its content does not move with the grobid source, which keeps the JEP layer cacheable.
FROM eclipse-temurin:21.0.11_10-jdk-noble AS jdk

# -------------------
# build runtime image
# -------------------
Expand Down Expand Up @@ -109,21 +117,44 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \

WORKDIR /opt/grobid

COPY --from=builder /opt/grobid .
# Everything from here to the "COPY --from=builder /opt/grobid ." below is ordered
# stable-first, on purpose. The Java distribution changes with every grobid commit, so it
# goes last: the DeLFT install, the JEP build and the glove preload (~2GB downloaded, then
# loaded into lmdb) all sit above it and stay cached across ordinary source changes. Only a
# version bump, a base-image change or a resources-registry edit rebuilds them.

# link the data directory to /data
# the current working directory will most likely be /opt/grobid
# (must precede the embeddings preload: the registry writes the lmdb under ./data)
RUN mkdir -p /data \
&& ln -s /data /opt/grobid/data \
&& ln -s /data ./data

# install DeLFT
RUN pip3 install --no-cache-dir delft==0.4.6

# Build JEP with temporary access to full JDK (needs javac + JNI headers).
# The bind mount is only visible during this RUN step — zero overhead in the final image.
RUN --mount=from=builder,source=/opt/java/openjdk,target=/opt/java/openjdk-jdk \
# Mounted from the `jdk` stage rather than from `builder`, so this layer does not depend on
# a stage that is rebuilt on every commit.
RUN --mount=from=jdk,source=/opt/java/openjdk,target=/opt/java/openjdk-jdk \
JAVA_HOME=/opt/java/openjdk-jdk pip3 install --no-cache-dir jep==4.3.1

# link the data directory to /data
# the current working directory will most likely be /opt/grobid
RUN mkdir -p /data \
&& ln -s /data /opt/grobid/data \
&& ln -s /data ./data
# preload embeddings, for GROBID all the RNN models use glove-840B (default for the script), ELMo is currently not loaded
# to be done: mechanism to download GROBID fine-tuned models based on SciBERT if selected (but not good enough for the moment)
# Only these two files are taken from the builder, and BuildKit keys the layer on their
# content, so the preload is rebuilt when the registry changes -- not on every commit.
COPY --from=builder /opt/grobid-source/grobid-home/scripts/preload_embeddings.py .
COPY --from=builder /opt/grobid-source/grobid-home/config/resources-registry.json .
# The lmdb permissions are set here rather than in the chgrp/chmod step at the end of the
# file: a recursive chmod in a later layer copies up every file it touches, which would put
# a second copy of the multi-GB embeddings database in the image.
RUN python3 preload_embeddings.py --registry ./resources-registry.json && \
ln -s /opt/grobid /opt/delft && \
chgrp -R 0 /data && chmod -R g=u /data

RUN mkdir delft && \
cp ./resources-registry.json delft/

# disable python warnings (and fix logging)
ENV PYTHONWARNINGS="ignore"
Expand All @@ -142,19 +173,14 @@ ENTRYPOINT ["/tini", "-s", "--"]

WORKDIR /opt/grobid

# preload embeddings, for GROBID all the RNN models use glove-840B (default for the script), ELMo is currently not loaded
# to be done: mechanism to download GROBID fine-tuned models based on SciBERT if selected (but not good enough for the moment)
COPY --from=builder /opt/grobid-source/grobid-home/scripts/preload_embeddings.py .
COPY --from=builder /opt/grobid-source/grobid-home/config/resources-registry.json .
RUN python3 preload_embeddings.py --registry ./resources-registry.json && \
ln -s /opt/grobid /opt/delft

RUN mkdir delft && \
cp ./resources-registry.json delft/
# The Java distribution, last: it changes with every grobid commit, so nothing cacheable
# should sit below it.
COPY --from=builder /opt/grobid .

# Set the group to avoid running the docker image in a rootless environment
RUN chgrp -R 0 /opt/grobid /data /tmp && \
chmod -R g=u /opt/grobid /data /tmp
# (/data is handled above, in the layer that fills it)
RUN chgrp -R 0 /opt/grobid /tmp && \
chmod -R g=u /opt/grobid /tmp

CMD ["./grobid-service/bin/grobid-service"]

Expand Down
37 changes: 35 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,23 @@ subprojects {
useJUnitPlatform()

testLogging.showStandardStreams = true
// enable for having separate test executor for different tests
forkEvery = 1

// One JVM per test task, not one per test class.
//
// This was forkEvery = 1, which spawns a fresh JVM for every test class -- 109 of
// them, run strictly one at a time (maxParallelForks is 1, and
// org.gradle.workers.max=1 in gradle.properties would cap it regardless). Each
// start pays JVM boot, the 1g heap below, the Mockito -javaagent, and
// java.library.path native library loading.
//
// Measured locally with `test --rerun-tasks -PskipCoverage`, same machine:
// 9m20s at forkEvery = 1 against 4m07s here, running an identical 812 tests across
// 109 classes with no failures. The difference is almost entirely process startup.
//
// The tests do share a JVM now, so anything depending on per-class isolation --
// static state in GrobidProperties, or a native library loaded through
// LibraryLoader -- would surface here rather than being papered over.
forkEvery = 0
maxHeapSize = "1024m"

def javaLibraryPath = getJavaLibraryPath()
Expand Down Expand Up @@ -737,6 +752,24 @@ project(":grobid-trainer") {

/** JACOCO **/

// Coverage instrumentation is opt-out, for CI legs that do not publish it.
//
// The jacoco agent attaches to every forked test JVM and instruments classes as they load.
// Only one CI leg feeds coveralls; on the others that cost is paid and thrown away.
//
// configureEach (rather than a plain withType) so this resolves after the subprojects have
// applied the jacoco plugin, and findByType so projects without it are simply skipped.
if (providers.gradleProperty('skipCoverage').present) {
allprojects {
tasks.withType(Test).configureEach {
def jacocoExtension = it.extensions.findByType(JacocoTaskExtension)
if (jacocoExtension != null) {
jacocoExtension.enabled = false
}
}
}
}

tasks.register("codeCoverageReport", JacocoReport) {
// If a subproject applies the 'jacoco' plugin, add the result it to the report
subprojects { subproject ->
Expand Down
Loading