Skip to content
Closed
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
9 changes: 6 additions & 3 deletions .github/BRANCHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ Details: ongdb-dev [CI-BRANCH-STRATEGY.md](https://github.com/graphfoundation/on

That script sets:

- Release branches `1.0`, `1.1`, `2.0` → require `full-reactor`
- Dev branches `1.0-dev`, `1.1-dev`, `2.0-dev` → require `dev-quality-gate`
- Environment `release` with required reviewer
- Release / dev branches → required status checks + **1 approving review** for contributors
- **You** (`bypass_pull_request_allowances`) may merge your own PRs without a second reviewer; others cannot
- Status checks still apply to you (`enforce_admins: true`)
- Environment `release` → you can self-approve **deployments** (not the same as PR self-approve)

GitHub has **no** “approve your own PR” option; review bypass is the supported mechanism.

**Nightly cron:** loaded from the repo **default** branch; builds matrix `1.0`, `1.1`, `2.0`.

Expand Down
42 changes: 31 additions & 11 deletions .github/scripts/apply-github-settings.sh
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
#!/usr/bin/env bash
# Apply branch protection for release vs dev lines on graphfoundation/ongdb.
#
# Solo-maintainer model (GitHub has no "self-approve own PR"):
# - Everyone else: must get an approving review (+ CODEOWNERS) before merge
# - Maintainer: listed in bypass_pull_request_allowances so they can merge
# their own PRs without a second person, while still needing status checks
# - enforce_admins=true so status checks apply even to admins
#
# Requires: gh auth with admin on the repo.
set -euo pipefail

REPO="${REPO:-graphfoundation/ongdb}"
REVIEWER_LOGIN="${RELEASE_REVIEWER:-$(gh api user --jq .login)}"
REVIEWER_ID="$(gh api "users/${REVIEWER_LOGIN}" --jq .id)"
MAINTAINER_LOGIN="${RELEASE_REVIEWER:-$(gh api user --jq .login)}"
MAINTAINER_ID="$(gh api "users/${MAINTAINER_LOGIN}" --jq .id)"

echo "Applying settings to ${REPO}"
echo "Applying settings to ${REPO} (maintainer bypass: @${MAINTAINER_LOGIN})"
gh api user --jq .login >/dev/null

protect_release() {
local branch="$1"
echo "Release protection: ${branch} (require full-reactor)"
echo "Release protection: ${branch} (full-reactor + reviews; @${MAINTAINER_LOGIN} may bypass reviews)"
gh api -X PUT "repos/${REPO}/branches/${branch}/protection" --input - <<EOF
{
"required_status_checks": {
Expand All @@ -23,7 +30,10 @@ protect_release() {
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true
"require_code_owner_reviews": true,
"bypass_pull_request_allowances": {
"users": ["${MAINTAINER_LOGIN}"]
}
},
"restrictions": null,
"allow_force_pushes": false,
Expand All @@ -35,18 +45,26 @@ EOF

protect_dev() {
local branch="$1"
echo "Dev protection: ${branch} (require dev-quality-gate)"
echo "Dev protection: ${branch} (dev-quality-gate + reviews; @${MAINTAINER_LOGIN} may bypass reviews)"
gh api -X PUT "repos/${REPO}/branches/${branch}/protection" --input - <<EOF
{
"required_status_checks": {
"strict": true,
"contexts": ["dev-quality-gate"]
},
"enforce_admins": false,
"required_pull_request_reviews": null,
"enforce_admins": true,
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true,
"bypass_pull_request_allowances": {
"users": ["${MAINTAINER_LOGIN}"]
}
},
"restrictions": null,
"allow_force_pushes": false,
"allow_deletions": false
"allow_deletions": false,
"required_conversation_resolution": false
}
EOF
}
Expand All @@ -59,17 +77,19 @@ for b in 1.0-dev 1.1-dev 2.0-dev; do
protect_dev "$b" || echo "WARN: could not protect ${b} (branch missing?)"
done

# Environment approval is separate from PR reviews: prevent_self_review=false lets
# the maintainer approve their own release deployment after tagging.
gh api -X PUT "repos/${REPO}/environments/release" --input - <<EOF
{
"wait_timer": 0,
"prevent_self_review": false,
"reviewers": [
{"type": "User", "id": ${REVIEWER_ID}}
{"type": "User", "id": ${MAINTAINER_ID}}
],
"deployment_branch_policy": null
}
EOF
echo "Environment 'release' ensured with reviewer @${REVIEWER_LOGIN}."
echo "Environment 'release' ensured; @${MAINTAINER_LOGIN} can self-approve deployments."
echo
echo "Manual: Org Billing → Actions spend budget (larger runners \$0)."
echo "Ensure nightly.yml + full-reactor-public.yml exist on each release line (or default branch)."
Original file line number Diff line number Diff line change
Expand Up @@ -680,12 +680,17 @@ public Lifecycle schemaAndTokensLifecycle()
@Override
public void init()
{
// Tokens and schema cache must be loaded before indexes open (fulltext accessors resolve
// property-key names). Recovery calls this after reverse recovery returns the store to a
// readable state; see Recovery.init().
reloadTokensAndSchemaFromStore();
indexingService.init();
}

@Override
public void start()
{
// Forward recovery / store replacement may have written additional tokens; refresh holders.
reloadTokensAndSchemaFromStore();
}
};
Expand Down
7 changes: 7 additions & 0 deletions enterprise/causal-clustering/LICENSES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ Apache Software License, Version 2.0
error-prone annotations
hazelcast-all
IPAddress
Jackson-annotations
Jackson-core
jackson-databind
Jetty :: Asynchronous HTTP Client
Jetty :: Http Utility
Jetty :: IO Utility
Jetty :: Utilities
jPowerShell
jProcesses
Lucene codecs
Expand Down
7 changes: 7 additions & 0 deletions enterprise/causal-clustering/NOTICE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ Apache Software License, Version 2.0
error-prone annotations
hazelcast-all
IPAddress
Jackson-annotations
Jackson-core
jackson-databind
Jetty :: Asynchronous HTTP Client
Jetty :: Http Utility
Jetty :: IO Utility
Jetty :: Utilities
jPowerShell
jProcesses
Lucene codecs
Expand Down
22 changes: 22 additions & 0 deletions enterprise/causal-clustering/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@
<version>3.12.12</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${jetty.version}</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>annotations</artifactId>
Expand Down Expand Up @@ -261,6 +272,17 @@
<artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ private GraphDatabaseService newTempDatabase( File tempStore )
.setUserLogProvider( NullLogProvider.getInstance() )
.newEmbeddedDatabaseBuilder( tempStore )
.setConfig( OnlineBackupSettings.online_backup_enabled, Settings.FALSE )
// Temp recovery DB must not load metrics: entity-count metrics need StoreEntityCounters
// that are not available until NeoStoreDataSource starts (after GlobalKernelExtensions).
.setConfig( "metrics.enabled", Settings.FALSE )
.setConfig( GraphDatabaseSettings.pagecache_warmup_enabled, Settings.FALSE )
.setConfig( GraphDatabaseSettings.keep_logical_logs, Settings.FALSE )
.setConfig( GraphDatabaseSettings.allow_upgrade,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
import org.neo4j.kernel.impl.enterprise.configuration.EnterpriseEditionSettings.Mode;
import org.neo4j.logging.Log;

import static org.neo4j.causalclustering.core.CausalClusteringSettings.initial_discovery_members;
import static org.neo4j.causalclustering.core.CausalClusteringSettings.minimum_core_cluster_size_at_runtime;
import static org.neo4j.causalclustering.core.CausalClusteringSettings.minimum_core_cluster_size_at_formation;

Expand All @@ -59,7 +58,7 @@ public Map<String,String> validate( @Nonnull Config config, @Nonnull Log log ) t
Mode mode = config.get( EnterpriseEditionSettings.mode );
if ( mode.equals( Mode.CORE ) || mode.equals( Mode.READ_REPLICA ) )
{
validateInitialDiscoveryMembers( config );
validateDiscoverySettings( config );
validateBoltConnector( config );
validateLoadBalancing( config, log );
validateDeclaredClusterSizes( config );
Expand Down Expand Up @@ -93,12 +92,17 @@ private void validateBoltConnector( Config config )
}
}

private void validateInitialDiscoveryMembers( Config config )
private void validateDiscoverySettings( Config config )
{
if ( !config.isConfigured( initial_discovery_members ) )
CausalClusteringSettings.DiscoveryType discoveryType = config.get( CausalClusteringSettings.discovery_type );
discoveryType.requiredSettings().forEach( setting ->
{
throw new InvalidSettingException(
String.format( "Missing mandatory non-empty value for '%s'", initial_discovery_members.name() ) );
}
if ( !config.isConfigured( setting ) )
{
throw new InvalidSettingException( String.format(
"Missing value for '%s', which is mandatory with '%s=%s'",
setting.name(), CausalClusteringSettings.discovery_type.name(), discoveryType ) );
}
} );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,19 @@
package org.neo4j.causalclustering.core;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.logging.Level;

import org.neo4j.causalclustering.core.consensus.log.cache.InFlightCacheFactory;
import org.neo4j.causalclustering.discovery.DnsHostnameResolver;
import org.neo4j.causalclustering.discovery.DomainNameResolverImpl;
import org.neo4j.causalclustering.discovery.HostnameResolver;
import org.neo4j.causalclustering.discovery.KubernetesResolver;
import org.neo4j.causalclustering.discovery.NoOpHostnameResolver;
import org.neo4j.causalclustering.discovery.SrvHostnameResolver;
import org.neo4j.causalclustering.discovery.SrvRecordResolverImpl;
Expand All @@ -54,6 +58,7 @@
import org.neo4j.graphdb.config.Setting;
import org.neo4j.helpers.AdvertisedSocketAddress;
import org.neo4j.helpers.ListenSocketAddress;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.logging.LogProvider;

import static org.neo4j.causalclustering.protocol.Protocol.ModifierProtocols.Implementations.GZIP;
Expand Down Expand Up @@ -189,6 +194,52 @@ public class CausalClusteringSettings implements LoadableConfig
setting( "causal_clustering.initial_discovery_members", list( ",", ADVERTISED_SOCKET_ADDRESS ),
NO_DEFAULT );

@Description( "Address for Kubernetes API" )
public static final Setting<AdvertisedSocketAddress> kubernetes_address =
setting( "causal_clustering.kubernetes.address", ADVERTISED_SOCKET_ADDRESS, "kubernetes.default.svc:443" );

@Description( "File location of token for Kubernetes API" )
public static final Setting<File> kubernetes_token =
pathUnixAbsolute( "causal_clustering.kubernetes.token", "/var/run/secrets/kubernetes.io/serviceaccount/token" );

@Description( "File location of namespace for Kubernetes API" )
public static final Setting<File> kubernetes_namespace =
pathUnixAbsolute( "causal_clustering.kubernetes.namespace", "/var/run/secrets/kubernetes.io/serviceaccount/namespace" );

@Description( "File location of CA certificate for Kubernetes API" )
public static final Setting<File> kubernetes_ca_crt =
pathUnixAbsolute( "causal_clustering.kubernetes.ca_crt", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" );

/**
* Creates absolute path on the first filesystem root. This will be `/` on Unix but arbitrary on Windows.
* If filesystem roots cannot be listed then `//` will be used - this will be resolved to `/` on Unix and `\\` (a UNC network path) on Windows.
* An absolute path is always needed for validation, even though we only care about a path on Linux.
*/
private static Setting<File> pathUnixAbsolute( String name, String path )
{
File[] roots = File.listRoots();
Path root = roots.length > 0 ? roots[0].toPath() : Paths.get( "//" );
return setting( name, PATH, root.resolve( path ).toString() );
}

@Description( "LabelSelector for Kubernetes API" )
public static final Setting<String> kubernetes_label_selector =
setting( "causal_clustering.kubernetes.label_selector", STRING, NO_DEFAULT );

@Description( "Service port name for discovery for Kubernetes API" )
public static final Setting<String> kubernetes_service_port_name =
setting( "causal_clustering.kubernetes.service_port_name", STRING, NO_DEFAULT );

@Internal
@Description( "The polling interval when attempting to resolve initial discovery members from DNS, SRV, or Kubernetes." )
public static final Setting<Duration> discovery_resolution_retry_interval =
setting( "causal_clustering.discovery_resolution_retry_interval", DURATION, "5s" );

@Internal
@Description( "Configures the time after which we give up trying to resolve discovery members." )
public static final Setting<Duration> discovery_resolution_timeout =
setting( "causal_clustering.discovery_resolution_timeout", DURATION, "5m" );

@Description( "Type of in-flight cache." )
public static final Setting<InFlightCacheFactory.Type> in_flight_cache_type =
setting( "causal_clustering.in_flight_cache.type", optionsIgnoreCase( InFlightCacheFactory.Type.class ),
Expand All @@ -202,24 +253,44 @@ public class CausalClusteringSettings implements LoadableConfig
public static final Setting<Long> in_flight_cache_max_bytes =
setting( "causal_clustering.in_flight_cache.max_bytes", BYTES, "2G" );

@FunctionalInterface
interface DiscoveryResolverFactory
{
HostnameResolver create( LogProvider logProvider, LogProvider userLogProvider, Config config );
}

public enum DiscoveryType
{
DNS( ( logProvider, userLogProvider ) -> new DnsHostnameResolver( logProvider, userLogProvider, new DomainNameResolverImpl() ) ),
DNS( ( logProvider, userLogProvider, config ) ->
new DnsHostnameResolver( logProvider, userLogProvider, new DomainNameResolverImpl() ),
initial_discovery_members ),

LIST( ( logProvider, userLogProvider, config ) -> new NoOpHostnameResolver(),
initial_discovery_members ),

LIST( ( logProvider, userLogProvider ) -> new NoOpHostnameResolver() ),
SRV( ( logProvider, userLogProvider, config ) ->
new SrvHostnameResolver( logProvider, userLogProvider, new SrvRecordResolverImpl() ),
initial_discovery_members ),

SRV( ( logProvider, userLogProvider ) -> new SrvHostnameResolver( logProvider, userLogProvider, new SrvRecordResolverImpl() ) );
K8S( KubernetesResolver::create, kubernetes_label_selector, kubernetes_service_port_name );

private final BiFunction<LogProvider,LogProvider,HostnameResolver> resolverSupplier;
private final DiscoveryResolverFactory resolverSupplier;
private final Collection<Setting<?>> requiredSettings;

DiscoveryType( BiFunction<LogProvider,LogProvider,HostnameResolver> resolverSupplier )
DiscoveryType( DiscoveryResolverFactory resolverSupplier, Setting<?>... requiredSettings )
{
this.resolverSupplier = resolverSupplier;
this.requiredSettings = Arrays.asList( requiredSettings );
}

public HostnameResolver getHostnameResolver( LogProvider logProvider, LogProvider userLogProvider, Config config )
{
return this.resolverSupplier.create( logProvider, userLogProvider, config );
}

public HostnameResolver getHostnameResolver( LogProvider logProvider, LogProvider userLogProvider )
public Collection<Setting<?>> requiredSettings()
{
return this.resolverSupplier.apply( logProvider, userLogProvider );
return requiredSettings;
}
}

Expand Down
Loading
Loading