Skip to content

Commit

Permalink
feat: Fetched child folder information in parallel
Browse files Browse the repository at this point in the history
Closes #26
  • Loading branch information
devatherock committed Oct 19, 2024
1 parent cb07105 commit c489fec
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]
### Changed
- Used virtual threads when available
- [#26](https://github.com/devatherock/artifactory-badge/issues/26): Fetched child folder information in parallel

## [3.0.0] - 2024-10-16
### Added
Expand Down
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,20 @@ logger:
#### Environment variables
| Name | Required | Default | Description |
|--------------------------------------|--------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| ARTIFACTORY_URL | true | (None) | The JFrog artifactory URL that hosts the docker registry |
| ARTIFACTORY_API_KEY | true | (None) | API key for interacting with artifactory's REST API |
| ARTIFACTORY_EXCLUDED_FOLDERS | false | /_uploads | Subfolders to be not treated as docker tags |
| ARTIFACTORY_DATE_FORMAT | false | yyyy-MM-dd'T'HH:mm:ss.SSSXXX | Date format to parse dates in artifactory API responses |
| ARTIFACTORY_BADGE_SHIELDS_IO_ENABLED | false | true | Indicates if <a href="https://shields.io">shields.io</a> should be used to generate the badge |
| LOGGER_LEVELS_ROOT | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for all(framework and custom) code |
| LOGGER_LEVELS_IO_GITHUB_DEVATHEROCK | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for custom code |
| MICRONAUT_ENVIRONMENTS | false | (None) | Setting the value to `local` will mock the calls to the artifactory. Only for testing purposes |
| MICRONAUT_SERVER_PORT | false | 8080 | Port in which the app listens on |
| MICRONAUT_CONFIG_FILES | true | (None) | Path to YAML config files. The YAML files can be used to specify complex, object and array properties |
| LOGBACK_CONFIGURATION_FILE | false | (None) | Class, file or remote path to logback configuration file. Will be ignored when using a remote path with any `logger.*` or `LOGGER_` config set |
| Name | Required | Default | Description |
|--------------------------------------|--------------|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
| ARTIFACTORY_URL | true | (None) | The JFrog artifactory URL that hosts the docker registry |
| ARTIFACTORY_API_KEY | true | (None) | API key for interacting with artifactory's REST API |
| ARTIFACTORY_EXCLUDED_FOLDERS | false | /_uploads | Subfolders to be not treated as docker tags |
| ARTIFACTORY_DATE_FORMAT | false | yyyy-MM-dd'T'HH:mm:ss.SSSXXX | Date format to parse dates in artifactory API responses |
| ARTIFACTORY_BADGE_SHIELDS_IO_ENABLED | false | true | Indicates if <a href="https://shields.io">shields.io</a> should be used to generate the badge |
| ARTIFACTORY_BADGE_PARALLELISM | false | 5 | Amount of parallelism to use when fetching details about versions of an image |
| LOGGER_LEVELS_ROOT | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for all(framework and custom) code |
| LOGGER_LEVELS_IO_GITHUB_DEVATHEROCK | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for custom code |
| MICRONAUT_ENVIRONMENTS | false | (None) | Setting the value to `local` will mock the calls to the artifactory. Only for testing purposes |
| MICRONAUT_SERVER_PORT | false | 8080 | Port in which the app listens on |
| MICRONAUT_CONFIG_FILES | true | (None) | Path to YAML config files. The YAML files can be used to specify complex, object and array properties |
| LOGBACK_CONFIGURATION_FILE | false | (None) | Class, file or remote path to logback configuration file. Will be ignored when using a remote path with any `logger.*` or `LOGGER_` config set |

### API spec
When the app is running, detailed API documentation can be accessed at `{host}/swagger-ui` or `{host}/swagger/artifactory-badge-{version}.yml`. The available endpoints are listed below for reference:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.devatherock.artifactory.controllers;

import static io.gatling.javaapi.core.CoreDsl.exec;
import static io.gatling.javaapi.core.CoreDsl.global;
import static io.gatling.javaapi.core.CoreDsl.rampUsers;
import static io.gatling.javaapi.core.CoreDsl.scenario;

Expand All @@ -19,7 +20,10 @@ public class CompositeSimulation extends BaseSimulation {
rampUsers(Integer.parseInt(getConfig("perf.users")))
.during(1)))
.protocols(buildProtocol())
.maxDuration(Long.parseLong(getConfig("perf.duration")));
.maxDuration(Long.parseLong(getConfig("perf.duration")))
.assertions(
global().responseTime().percentile(95).lt(2500),
global().successfulRequests().percent().is(100d));
}

protected ScenarioBuilder buildScenario() {
Expand Down
20 changes: 20 additions & 0 deletions src/gatling/resources/wiremock/responses/versions-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,30 @@
"uri": "/1.1.2-rc.1",
"folder": true
},
{
"uri": "/1.2.0",
"folder": true
},
{
"uri": "/1.3.0",
"folder": true
},
{
"uri": "/1.4.0",
"folder": true
},
{
"uri": "/1.5.0",
"folder": true
},
{
"uri": "/latest",
"folder": true
},
{
"uri": "/2.0.0",
"folder": true
},
{
"uri": "/_uploads",
"folder": true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.github.devatherock.artifactory.config;

import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;

import io.github.devatherock.artifactory.util.ParallelProcessor;

import io.micronaut.context.annotation.Factory;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import jakarta.inject.Named;
import jakarta.inject.Singleton;

/**
Expand All @@ -22,4 +28,13 @@ public class AppConfig {
public BlockingHttpClient httpClient(@Client HttpClient httpClient) {
return httpClient.toBlocking();
}

/**
* @param appProperties miscellaneous application properties
* @return a parallel processor bean
*/
@Singleton
public ParallelProcessor parallelProcessor(@Named("blocking") Executor executor, AppProperties appProperties) {
return new ParallelProcessor(executor, new Semaphore(appProperties.getParallelism()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.devatherock.artifactory.config;

import io.micronaut.context.annotation.ConfigurationProperties;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@ConfigurationProperties("artifactory.badge")
public class AppProperties {
/**
* Amount of parallelism to use when fetching details about versions of an image
*/
private int parallelism = 5;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.devatherock.artifactory.service;

import java.time.Instant;
import java.util.List;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import io.github.devatherock.artifactory.config.ArtifactoryProperties;
Expand All @@ -10,6 +12,7 @@
import io.github.devatherock.artifactory.entities.DockerLayer;
import io.github.devatherock.artifactory.entities.DockerManifest;
import io.github.devatherock.artifactory.util.BadgeGenerator;
import io.github.devatherock.artifactory.util.ParallelProcessor;

import io.micronaut.cache.annotation.Cacheable;
import io.micronaut.core.annotation.Blocking;
Expand Down Expand Up @@ -53,6 +56,7 @@ public class DockerBadgeService {

private final BlockingHttpClient artifactoryClient;
private final BadgeGenerator badgeGenerator;
private final ParallelProcessor parallelProcessor;
private final ArtifactoryProperties artifactoryConfig;

@Cacheable(cacheNames = "size-cache")
Expand Down Expand Up @@ -93,15 +97,19 @@ public String getPullsCountBadge(String packageName, String badgeLabel) {
if (null != folderInfo && CollectionUtils.isNotEmpty(folderInfo.getChildren())) {
long downloadCount = 0;

for (ArtifactoryFolderElement child : folderInfo.getChildren()) {
if (isTag(child)) {
ArtifactoryFileStats fileStats = getManifestStats(packageName, child.getUri());
List<Supplier<ArtifactoryFileStats>> statsSuppliers = folderInfo.getChildren()
.stream()
.filter(this::isTag)
.map(child -> (Supplier<ArtifactoryFileStats>) () -> getManifestStats(packageName, child.getUri()))
.toList();
List<ArtifactoryFileStats> statsList = parallelProcessor.parallelProcess(statsSuppliers);

if (null != fileStats) {
downloadCount += fileStats.getDownloadCount();
}
for (ArtifactoryFileStats fileStats : statsList) {
if (null != fileStats) {
downloadCount += fileStats.getDownloadCount();
}
}

LOGGER.info("Download count of {}: {}", packageName, downloadCount);
return badgeGenerator.generateBadge(badgeLabel,
DockerBadgeServiceHelper.formatDownloadCount(downloadCount));
Expand All @@ -126,9 +134,9 @@ public String getLatestVersionBadge(String packageName, String badgeLabel, Strin
if (null != folderInfo && CollectionUtils.isNotEmpty(folderInfo.getChildren())) {
ArtifactoryFolderInfo latestVersion = null;

for (ArtifactoryFolderElement child : folderInfo.getChildren()) {
if (isTag(child)) {
if (SORT_TYPE_SEMVER.equals(sortType)) {
if (SORT_TYPE_SEMVER.equals(sortType)) {
for (ArtifactoryFolderElement child : folderInfo.getChildren()) {
if (isTag(child)) {
// Substring to remove the leading slash
String currentVersion = child.getUri().substring(1);

Expand All @@ -145,20 +153,28 @@ public String getLatestVersionBadge(String packageName, String badgeLabel, Strin
}
}
}
} else {
// Find the modified time of each subfolder - each subfolder corresponds to a
// tag
ArtifactoryFolderInfo currentVersion = getArtifactoryFolderInfo(packageName + child.getUri());

if (null == latestVersion || (null != currentVersion
&& Instant
.from(artifactoryConfig.getDateParser().parse(currentVersion.getLastModified()))
.compareTo(
Instant.from(
artifactoryConfig.getDateParser()
.parse(latestVersion.getLastModified()))) > 0)) {
latestVersion = currentVersion;
}
}
}
} else {
// Find the modified time of each subfolder - each subfolder corresponds to a
// tag
List<Supplier<ArtifactoryFolderInfo>> versionSuppliers = folderInfo.getChildren()
.stream()
.filter(this::isTag)
.map(child -> (Supplier<ArtifactoryFolderInfo>) () -> getArtifactoryFolderInfo(
packageName + child.getUri()))
.toList();
List<ArtifactoryFolderInfo> versions = parallelProcessor.parallelProcess(versionSuppliers);

for (ArtifactoryFolderInfo currentVersion : versions) {
if (null == latestVersion || (null != currentVersion
&& Instant
.from(artifactoryConfig.getDateParser().parse(currentVersion.getLastModified()))
.compareTo(
Instant.from(
artifactoryConfig.getDateParser()
.parse(latestVersion.getLastModified()))) > 0)) {
latestVersion = currentVersion;
}
}
}
Expand Down Expand Up @@ -255,7 +271,7 @@ private String generateNotFoundBadge(String badgeLabel) {

/**
* Checks if the supplied artifactory folder content corresponds to a tag
*
*
* @param child
* @return a flag
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.github.devatherock.artifactory.util;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.function.Supplier;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
public class ParallelProcessor {
private final Executor executor;
private final Semaphore semaphore; // To limit parallelism in case of outbound API calls

public <T> List<T> parallelProcess(List<Supplier<T>> suppliers) {
Map<Integer, Optional<T>> result = new ConcurrentHashMap<>();
CountDownLatch countDownLatch = new CountDownLatch(suppliers.size());

try {
for (int index = 0; index < suppliers.size(); index++) {
int currentIndex = index;

semaphore.acquire();
executor.execute(() -> {
try {
LOGGER.trace("In parallelProcess. Index: {}", currentIndex);
T currentResult = suppliers.get(currentIndex).get();
result.put(currentIndex, currentResult != null ? Optional.of(currentResult) : Optional.empty());
} catch (Exception exception) {
LOGGER.warn("Exception while processing index {}", currentIndex, exception);
result.put(currentIndex, Optional.empty());
} finally {
countDownLatch.countDown();
}
});
semaphore.release();
}

countDownLatch.await();
} catch (InterruptedException exception) {
LOGGER.warn("Exception during parallel processing", exception);
}

return result.entrySet()
.stream()
.sorted(Entry.comparingByKey(Comparator.naturalOrder()))
.map(entry -> entry.getValue().orElse(null))
.toList();
}
}
Loading

0 comments on commit c489fec

Please sign in to comment.