Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
79cbaa8
fix(discovery): tolerate temporary plugin discovery failures
andrewazores Apr 21, 2026
b6d7917
chore(discovery): refactor to extract PluginCallbackFactory
andrewazores Apr 21, 2026
24b95c1
fixup! fix(discovery): tolerate temporary plugin discovery failures
andrewazores Apr 21, 2026
10efe5a
test(discovery): add tests for temporary plugin failures
andrewazores Apr 21, 2026
11665e3
chore(db): configure default datasource size and timeouts, add monito…
andrewazores Apr 21, 2026
6d97c50
fix(discovery): exponential backoff of failed ping, improve failed pi…
andrewazores Apr 21, 2026
612bc68
fix(discovery): improve handling of stale plugins in deletion
andrewazores Apr 21, 2026
47e3a24
fix(discovery): atomic child node list updates
andrewazores Apr 21, 2026
35cc3c9
fix(discovery): idempotent plugin registration on (callback, realmName)
andrewazores Apr 22, 2026
0fafc7b
schema update
andrewazores Apr 22, 2026
dea5037
fix(discovery): plugin-associated credentials expire after 1 hour of …
andrewazores Apr 22, 2026
c77142a
schema update
andrewazores Apr 22, 2026
a12995f
fixup! fix(discovery): plugin-associated credentials expire after 1 h…
andrewazores Apr 22, 2026
6b1059c
JsonIgnore new properties
andrewazores Apr 22, 2026
5298cd1
update schema
andrewazores Apr 22, 2026
5ff7f9e
reset submodule
andrewazores Apr 22, 2026
beaa441
do not emit notifications on JsonIgnore'd or write-only attributes
andrewazores Apr 22, 2026
4592e44
unschedule ActiveRecordingUpdateJob on NoResultException
andrewazores Apr 23, 2026
283c619
reduce plugin ping period and max failures
andrewazores Apr 23, 2026
6c39e95
remove expiresAt, lastUsedAt, expiration job
andrewazores Apr 24, 2026
d3f0a2e
fixup! reset submodule
andrewazores Apr 24, 2026
a1d9171
allow 3 ping failures
andrewazores Apr 24, 2026
4c1f661
reduce max backoff
andrewazores Apr 24, 2026
15dea58
additional debug logging
andrewazores Apr 24, 2026
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
6 changes: 6 additions & 0 deletions src/main/java/io/cryostat/ConfigProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ public class ConfigProperties {
public static final String CONTAINERS_POLL_PERIOD = "cryostat.discovery.containers.poll-period";
public static final String CONTAINERS_REQUEST_TIMEOUT =
"cryostat.discovery.containers.request-timeout";
public static final String DISCOVERY_PLUGINS_MAX_FAILURES =
"cryostat.discovery.plugins.max-failures";
public static final String DISCOVERY_PLUGINS_PING_PERIOD =
"cryostat.discovery.plugins.ping-period";
public static final String DISCOVERY_PLUGINS_MAX_BACKOFF_MULTIPLIER =
"cryostat.discovery.plugins.max-backoff-multiplier";

public static final String CONNECTIONS_TTL = "cryostat.connections.ttl";
public static final String CONNECTIONS_FAILED_BACKOFF = "cryostat.connections.failed-backoff";
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/io/cryostat/discovery/CustomDiscovery.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
Expand Down Expand Up @@ -78,6 +80,7 @@ public class CustomDiscovery {

@Inject Logger logger;
@Inject EventBus bus;
@Inject EntityManager entityManager;
@Inject TargetConnectionManager connectionManager;
@Inject URIUtil uriUtil;

Expand Down Expand Up @@ -200,11 +203,16 @@ RestResponse<Target> doCreate(
DiscoveryNode.target(target, NodeType.BaseNodeType.JVM);
target.discoveryNode = node;
DiscoveryNode realm = DiscoveryNode.getRealm(REALM).orElseThrow();
realm =
entityManager.find(
DiscoveryNode.class,
realm.id,
LockModeType.PESSIMISTIC_WRITE);

realm.children.add(node);
node.parent = realm;
target.persist();
node.persist();
realm.children.add(node);
realm.persist();

return ResponseBuilder.<Target>created(
Expand Down Expand Up @@ -239,6 +247,7 @@ RestResponse<Target> doCreate(
public void delete(@RestPath long id) throws URISyntaxException {
Target target = Target.find("id", id).singleResult();
DiscoveryNode realm = DiscoveryNode.getRealm(REALM).orElseThrow();
realm = entityManager.find(DiscoveryNode.class, realm.id, LockModeType.PESSIMISTIC_WRITE);
boolean withinRealm = realm.children.remove(target.discoveryNode);
if (!withinRealm) {
throw new BadRequestException();
Expand Down
338 changes: 263 additions & 75 deletions src/main/java/io/cryostat/discovery/Discovery.java

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/main/java/io/cryostat/discovery/DiscoveryJwtFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import io.cryostat.ConfigProperties;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
Expand Down Expand Up @@ -71,7 +73,7 @@ public class DiscoveryJwtFactory {
static final String DISCOVERY_V4_API_PATH = "/api/v4/discovery/";
static final String DISCOVERY_V4_2_API_PATH = "/api/v4.2/discovery/";

@ConfigProperty(name = "cryostat.discovery.plugins.ping-period")
@ConfigProperty(name = ConfigProperties.DISCOVERY_PLUGINS_PING_PERIOD)
Duration discoveryPingPeriod;

@ConfigProperty(name = "cryostat.discovery.plugins.jwt.signature.algorithm")
Expand Down
51 changes: 49 additions & 2 deletions src/main/java/io/cryostat/discovery/DiscoveryPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;

Expand Down Expand Up @@ -74,7 +76,12 @@
@NamedQueries({
@NamedQuery(
name = "DiscoveryPlugin.getBuiltinRealmIds",
query = "SELECT p.realm.id FROM DiscoveryPlugin p WHERE p.builtin = true")
query = "SELECT p.realm.id FROM DiscoveryPlugin p WHERE p.builtin = true"),
@NamedQuery(
name = "DiscoveryPlugin.findByCallbackAndRealmName",
query =
"SELECT p FROM DiscoveryPlugin p JOIN FETCH p.realm r WHERE p.callback = ?1"
+ " AND r.name = ?2")
})
public class DiscoveryPlugin extends PanacheEntityBase {

Expand Down Expand Up @@ -109,10 +116,41 @@ public class DiscoveryPlugin extends PanacheEntityBase {
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public boolean builtin;

@Column(nullable = false)
@JsonIgnore
public int consecutiveFailures = 0;

@Column(nullable = true)
@Convert(converter = InstantConverter.class)
@JsonIgnore
public Instant lastSuccessfulPing;

@Column(nullable = true)
@Convert(converter = InstantConverter.class)
@JsonIgnore
public Instant lastFailedPing;

@Column(nullable = false)
@JsonIgnore
public int backoffMultiplier = 1;

@Column(nullable = true)
@Convert(converter = InstantConverter.class)
@JsonIgnore
public Instant nextPingAt;

public static Optional<DiscoveryPlugin> findByCallbackAndRealmName(
URI callback, String realmName) {
return DiscoveryPlugin.<DiscoveryPlugin>find(
"#DiscoveryPlugin.findByCallbackAndRealmName", callback, realmName)
.singleResultOptional();
}

@ApplicationScoped
static class Listener {

@Inject Logger logger;
@Inject PluginCallbackFactory callbackFactory;

@PrePersist
@Transactional
Expand All @@ -126,13 +164,22 @@ public void prePersist(DiscoveryPlugin plugin) {
if (plugin.credential == null) {
var credential = getCredential(plugin);
plugin.credential = credential;
credential.discoveryPlugin = plugin;
plugin.callback = UriBuilder.fromUri(plugin.callback).userInfo(null).build();
}
if (plugin.nextPingAt != null
|| plugin.lastFailedPing != null
|| plugin.lastSuccessfulPing != null) {
logger.debugv(
"Skipping prePersist ping for plugin with existing state: {0} @ {1}",
plugin.realm.name, plugin.callback);
return;
}
try {
logger.debugv(
"Testing discovery plugin callback: {0} @ {1}",
plugin.realm.name, plugin.callback);
PluginCallback.create(plugin).ping();
callbackFactory.create(plugin).ping();
logger.debugv(
"Registered discovery plugin: {0} @ {1}",
plugin.realm.name, plugin.callback);
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/io/cryostat/discovery/InstantConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright The Cryostat Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cryostat.discovery;

import java.time.Instant;

import jakarta.persistence.AttributeConverter;

public class InstantConverter implements AttributeConverter<Instant, Long> {

@Override
public Long convertToDatabaseColumn(Instant attribute) {
if (attribute == null) {
return null;
}
return attribute.toEpochMilli();
}

@Override
public Instant convertToEntityAttribute(Long dbData) {
if (dbData == null) {
return null;
}
return Instant.ofEpochMilli(dbData);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.transaction.Transactional;
import jakarta.transaction.Transactional.TxType;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -115,6 +117,8 @@ public class KubeEndpointSlicesDiscovery implements ResourceEventHandler<Endpoin

@Inject EventBus bus;

@Inject EntityManager entityManager;

@ConfigProperty(name = "cryostat.discovery.kubernetes.enabled")
boolean enabled;

Expand Down Expand Up @@ -383,11 +387,18 @@ public void handleQueryEvent(NamespaceQueryEvent evt) {
public void handleEndpointEvent(EndpointDiscoveryEvent evt) {
String namespace = evt.namespace;
DiscoveryNode realm = DiscoveryNode.getRealm(REALM).orElseThrow();
realm = entityManager.find(DiscoveryNode.class, realm.id, LockModeType.PESSIMISTIC_WRITE);
DiscoveryNode lockedRealm = realm;
DiscoveryNode nsNode =
DiscoveryNode.getChild(realm, n -> n.name.equals(namespace))
.orElse(
DiscoveryNode.environment(
namespace, KubeDiscoveryNodeType.NAMESPACE));
DiscoveryNode.getChild(lockedRealm, n -> n.name.equals(namespace))
.orElseGet(
() -> {
DiscoveryNode created =
DiscoveryNode.environment(
namespace, KubeDiscoveryNodeType.NAMESPACE);
created.parent = lockedRealm;
return created;
});

if (evt.eventKind == EventKind.FOUND) {
persistOwnerChain(nsNode, evt.target, evt.objRef);
Expand All @@ -402,6 +413,7 @@ public void handleEndpointEvent(EndpointDiscoveryEvent evt) {
realm.children.add(nsNode);
nsNode.parent = realm;
}
entityManager.flush();
realm.persist();
}

Expand Down Expand Up @@ -718,6 +730,7 @@ private void pruneOwnerChain(DiscoveryNode nsNode, Target target) {
child = parent;
}

entityManager.flush();
nsNode.persist();
target.delete();
}
Expand Down Expand Up @@ -807,6 +820,7 @@ private void persistNodeChain(DiscoveryNode nsNode, Target target, DiscoveryNode
}

// Finally persist the namespace node
entityManager.flush();
nsNode.persist();
}

Expand Down
34 changes: 34 additions & 0 deletions src/main/java/io/cryostat/discovery/PluginCallbackFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The Cryostat Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cryostat.discovery;

import java.net.URISyntaxException;

import io.cryostat.discovery.DiscoveryPlugin.PluginCallback;

import jakarta.enterprise.context.ApplicationScoped;

/**
* Factory for creating PluginCallback instances. This abstraction allows tests to inject mock
* callbacks without making real network connections.
*/
@ApplicationScoped
public class PluginCallbackFactory {

public PluginCallback create(DiscoveryPlugin plugin) throws URISyntaxException {
return PluginCallback.create(plugin);
}
}
Loading
Loading