Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduces SchemaRegistry and related classes #8614

Merged
merged 10 commits into from
Sep 24, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ public boolean isGreaterThanOrEqualTo(final SpecMilestone other) {
return compareTo(other) >= 0;
}

public boolean isGreaterThan(final SpecMilestone other) {
return compareTo(other) > 0;
}

public boolean isLessThanOrEqualTo(final SpecMilestone other) {
return compareTo(other) <= 0;
}

/** Returns the milestone prior to this milestone */
public SpecMilestone getPreviousMilestone() {
if (equals(PHASE0)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright Consensys Software Inc., 2024
*
* 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 tech.pegasys.teku.spec.schemas;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.EnumMap;
import java.util.Map;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.config.SpecConfig;
import tech.pegasys.teku.spec.schemas.SchemaTypes.SchemaId;

public abstract class AbstractSchemaProvider<T> implements SchemaProvider<T> {
tbenr marked this conversation as resolved.
Show resolved Hide resolved
private final Map<SpecMilestone, SpecMilestone> milestoneToEffectiveMilestone =
new EnumMap<>(SpecMilestone.class);
private final SchemaId<T> schemaId;

protected AbstractSchemaProvider(final SchemaId<T> schemaId) {
this.schemaId = schemaId;
}

protected void addMilestoneMapping(
final SpecMilestone milestone, final SpecMilestone untilMilestone) {
checkArgument(
untilMilestone.isGreaterThan(milestone),
"%s must be earlier than %s",
milestone,
untilMilestone);

SpecMilestone currentMilestone = untilMilestone;
while (!currentMilestone.equals(milestone)) {

checkIfAlreadyMapped(currentMilestone);

milestoneToEffectiveMilestone.put(currentMilestone, milestone);

currentMilestone = currentMilestone.getPreviousMilestone();
}

checkIfAlreadyMapped(currentMilestone);
}

private void checkIfAlreadyMapped(final SpecMilestone milestone) {
if (milestoneToEffectiveMilestone.containsKey(milestone)) {
throw new IllegalArgumentException(
String.format(
"Milestone %s is already mapped to %s",
milestone, milestoneToEffectiveMilestone.get(milestone)));
}
}

@Override
public SpecMilestone getEffectiveMilestone(final SpecMilestone milestone) {
return milestoneToEffectiveMilestone.getOrDefault(milestone, milestone);
}

@Override
public T getSchema(final SchemaRegistry registry) {
final SpecMilestone milestone = registry.getMilestone();
final SpecMilestone effectiveMilestone = getEffectiveMilestone(milestone);
return createSchema(registry, effectiveMilestone, registry.getSpecConfig());
}

@Override
public SchemaId<T> getSchemaId() {
return schemaId;
}

protected abstract T createSchema(
SchemaRegistry registry, SpecMilestone baseVersion, SpecConfig specConfig);
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Consensys Software Inc., 2024
*
* 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 tech.pegasys.teku.spec.schemas;

import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.schemas.SchemaTypes.SchemaId;

public interface SchemaCache {
tbenr marked this conversation as resolved.
Show resolved Hide resolved
static SchemaCache createDefault() {
return new SchemaCache() {
private final Map<SpecMilestone, Map<SchemaId<?>, Object>> cache =
new EnumMap<>(SpecMilestone.class);

@SuppressWarnings("unchecked")
@Override
public <T> T get(final SpecMilestone milestone, final SchemaId<T> schemaId) {
return (T) cache.computeIfAbsent(milestone, __ -> new HashMap<>()).get(schemaId);
tbenr marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public <T> void put(
final SpecMilestone milestone, final SchemaId<T> schemaId, final T schema) {
cache.computeIfAbsent(milestone, __ -> new HashMap<>()).put(schemaId, schema);
}
};
}

<T> T get(SpecMilestone milestone, SchemaId<T> schemaId);

<T> void put(SpecMilestone milestone, SchemaId<T> schemaId, T schema);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Consensys Software Inc., 2024
*
* 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 tech.pegasys.teku.spec.schemas;

import static tech.pegasys.teku.spec.SpecMilestone.BELLATRIX;
import static tech.pegasys.teku.spec.SpecMilestone.CAPELLA;
import static tech.pegasys.teku.spec.SpecMilestone.DENEB;
import static tech.pegasys.teku.spec.SpecMilestone.ELECTRA;

import java.util.EnumSet;
import java.util.Set;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.schemas.SchemaTypes.SchemaId;

public interface SchemaProvider<T> {
Set<SpecMilestone> ALL_MILESTONES = EnumSet.allOf(SpecMilestone.class);
Set<SpecMilestone> FROM_BELLATRIX = from(BELLATRIX);
Set<SpecMilestone> FROM_CAPELLA = from(CAPELLA);
Set<SpecMilestone> FROM_DENEB = from(DENEB);
Set<SpecMilestone> FROM_ELECTRA = from(ELECTRA);

static Set<SpecMilestone> from(final SpecMilestone milestone) {
return EnumSet.copyOf(SpecMilestone.getAllMilestonesFrom(milestone));
}

static Set<SpecMilestone> fromTo(
final SpecMilestone fromMilestone, final SpecMilestone toMilestone) {
return EnumSet.copyOf(
SpecMilestone.getAllMilestonesFrom(fromMilestone).stream()
.filter(toMilestone::isLessThanOrEqualTo)
.toList());
}

T getSchema(SchemaRegistry registry);

Set<SpecMilestone> getSupportedMilestones();

SpecMilestone getEffectiveMilestone(SpecMilestone version);

SchemaId<T> getSchemaId();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright Consensys Software Inc., 2024
*
* 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 tech.pegasys.teku.spec.schemas;

import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.config.SpecConfig;
import tech.pegasys.teku.spec.schemas.SchemaTypes.SchemaId;

public class SchemaRegistry {
private final Map<SchemaId<?>, SchemaProvider<?>> providers = new HashMap<>();
private final SpecMilestone milestone;
private final SchemaCache cache;
private final SpecConfig specConfig;

public SchemaRegistry(
tbenr marked this conversation as resolved.
Show resolved Hide resolved
final SpecMilestone milestone, final SpecConfig specConfig, final SchemaCache cache) {
this.milestone = milestone;
this.specConfig = specConfig;
this.cache = cache;
}

public void registerProvider(final SchemaProvider<?> provider) {
providers.put(provider.getSchemaId(), provider);
}

@VisibleForTesting
boolean isProviderRegistered(final SchemaProvider<?> provider) {
return provider.equals(providers.get(provider.getSchemaId()));
}

@SuppressWarnings("unchecked")
public <T> T get(final SchemaId<T> schemaClass) {
SchemaProvider<T> provider = (SchemaProvider<T>) providers.get(schemaClass);
if (provider == null) {
throw new IllegalArgumentException(
"No provider registered for schema "
+ schemaClass
+ " or it does not support milestone "
+ milestone);
}
T schema = cache.get(milestone, schemaClass);
if (schema != null) {
return schema;
}
final SpecMilestone effectiveMilestone = provider.getEffectiveMilestone(milestone);
if (effectiveMilestone != milestone) {
schema = cache.get(effectiveMilestone, schemaClass);
if (schema != null) {
cache.put(milestone, schemaClass, schema);
return schema;
}
}
schema = provider.getSchema(this);
cache.put(effectiveMilestone, schemaClass, schema);
if (effectiveMilestone != milestone) {
cache.put(milestone, schemaClass, schema);
}
return schema;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are relying on the fact that we are calling SchemaRegistryBuilder from a "concurrency free" part of the code. I understand this is by design but maybe we can remove that assumption (so it does not bite us in the future).

Once the registry is primed, we should always hit the cache. However, it isn't thread-safe before priming.

What about separating the logic on get() to only read the cache. If we attempt to call get() w/o priming the registry first, we fail. We can move the logic that populates the cache to primeRegistry(), and make it synchronized so we know it is thread-safe no matter where it is being called from.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree! Generally, a better separation between priming and lookup reflects the usage we expect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relooking at this, the separation can't be done.
The way this works is that, when the schema is being created, it can lookup in the registry itself so it may trigger a creation of a schema (dependency). This help us not dealing with ordering (it works as soon as there are no dependency loops).

The real point here is if we want or not a dependency loop detection. To me it can only happen if as a developer you pick the wrong schemaId (and it is compatible with the type you need in that moment). Let me think


public SpecMilestone getMilestone() {
return milestone;
}

public SpecConfig getSpecConfig() {
return specConfig;
}

public void primeRegistry() {
for (final SchemaId<?> schemaClass : providers.keySet()) {
get(schemaClass);
}
}
tbenr marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Consensys Software Inc., 2024
*
* 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 tech.pegasys.teku.spec.schemas;

import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import tech.pegasys.teku.spec.SpecMilestone;
import tech.pegasys.teku.spec.config.SpecConfig;

public class SchemaRegistryBuilder {
private final List<SchemaProvider<?>> providers = new ArrayList<>();
private final SchemaCache cache;

public static SchemaRegistryBuilder create() {
return new SchemaRegistryBuilder();
}

public SchemaRegistryBuilder() {
this.cache = SchemaCache.createDefault();
}

@VisibleForTesting
SchemaRegistryBuilder(final SchemaCache cache) {
this.cache = cache;
}

public <T> SchemaRegistryBuilder addProvider(final SchemaProvider<T> provider) {
providers.add(provider);
return this;
}

public SchemaRegistry build(final SpecMilestone milestone, final SpecConfig specConfig) {
final SchemaRegistry registry = new SchemaRegistry(milestone, specConfig, cache);

for (final SchemaProvider<?> provider : providers) {
if (provider.getSupportedMilestones().contains(milestone)) {
registry.registerProvider(provider);
}
}

registry.primeRegistry();

return registry;
}
}
Loading