docs update

This commit is contained in:
Jonathan Shook 2024-11-21 15:33:28 -06:00
parent da609d76da
commit ac78ea5667
4 changed files with 173 additions and 165 deletions

View File

@ -2,13 +2,13 @@ package io.nosqlbench.adapter.prototype;
/* /*
* Copyright (c) nosqlbench * Copyright (c) nosqlbench
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@ -21,6 +21,9 @@ package io.nosqlbench.adapter.prototype;
import io.nosqlbench.adapters.api.activityimpl.uniform.BaseSpace; import io.nosqlbench.adapters.api.activityimpl.uniform.BaseSpace;
import io.nosqlbench.adapters.api.activityimpl.uniform.DriverAdapter; import io.nosqlbench.adapters.api.activityimpl.uniform.DriverAdapter;
/**
* {@inheritDoc}
*/
public class ExampleSpace extends BaseSpace<ExampleSpace> { public class ExampleSpace extends BaseSpace<ExampleSpace> {
public ExampleSpace(DriverAdapter<?, ExampleSpace> adapter, long idx) { public ExampleSpace(DriverAdapter<?, ExampleSpace> adapter, long idx) {

View File

@ -23,6 +23,9 @@ import io.nosqlbench.adapters.api.activityimpl.uniform.flowtypes.CycleOp;
/** /**
* This example of a space uses the <EM>SelfT</EM> technique to enable * This example of a space uses the <EM>SelfT</EM> technique to enable
* the self type to be used in method signatures and return types. * the self type to be used in method signatures and return types.
*
* {@inheritDoc}
*
* @param <SelfT> * @param <SelfT>
*/ */
public class BaseSpace<SelfT extends BaseSpace<SelfT> > implements Space { public class BaseSpace<SelfT extends BaseSpace<SelfT> > implements Space {

View File

@ -35,120 +35,114 @@ import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.LongFunction; import java.util.function.LongFunction;
/**
* <P>The DriverAdapter interface is the top level API for implementing /// The DriverAdapter interface is the top level API for implementing
* operations in NoSQLBench. It defines the related APIs needed to fully realized an adapter /// operations in NoSQLBench. It defines the related APIs needed to fully realized an adapter
* at runtime. A driver adapter can map op templates from YAML form to a fully executable /// at runtime. A driver adapter can map op templates from YAML form to a fully executable
* form in native Java code, just as an application might do with a native driver. It can also /// form in native Java code, and then execute those native operations just as an application might do directly with a
* do trivial operations, like simply calling {@code System.out.println(...)}. When you specify an adapter by name, /// native driver. It can also do trivial operations, like simply calling `System.out.println(...)`. What a
* you are choosing both the available operations, and the rules for converting a YAML op template into those /// particular DriverAdapter does is open-ended, but this usually means wrapping a native driver when supporting
* operations. This is a two-step process: The adapter provides mapping logic for converting high-level templates from /// specific protocols.
* users into op types, and separate dispensing logic which can efficiently create these ops at runtime. When used ///
* together, they power <EM>op synthesis</EM> -- efficient and deterministic construction of runtime operations using /// Every DriverAdapter has a simple name, as indicated on it's [Service] annotation. When you
* procedural generation methods. /// specify an adapter by name, you are choosing both the available operations, and the rules for converting
* </p> /// a YAML op template into those operations. This is a two-step process: Mapping user intentions, and generating
* /// executable operations, called op mapping and op dispensing, respectively. Each adapter provides implementations
* <p> /// for both of these phases for all the op types is supports. When used together, they power _op synthesis_ --
* Generally speaking, a driver adapter is responsible for /// efficient and deterministic construction of runtime operations using procedural generation methods.
* <UL> ///
* <LI>Defining a class type for holding related state known as a {@link Space}. /// Generally speaking, a driver adapter is responsible for
* <UL><LI>The type of this is specified as /// - Defining a class type for holding related state known as a [Space]. The type of this is specified as generic
* generic parameter {@link SPACETYPE}.</LI></UL></LI> /// parameter [SPACETYPE]
* <LI>Defining a factory method for constructing an instance of the space.</LI> /// - Defining a factory method for constructing an instance of the space.
* <LI>Recognizing the op templates that are documented for it via an {@link OpMapper} /// - Recognizing the op templates that are documented for it via an [OpMapper] and assigning them to an op
* and assigning them to an op implementation. /// implementation. The base type of these ops is specified as generic [OPTYPE]
* <UL><LI>The base type of these ops is specified as generic /// - Constructing dispensers for each matching op implementation with a matching [OpDispenser] implementation.
* parameter {@link OPTYPE}.</LI></UL> ///
* </LI> /// At a high level, the following sequence is executed for every cycle in an activity:
* <LI>Constructing dispensers for each matching op implementation with a matching {@link OpDispenser}</LI> /// ```
* implementation. ///[cycle value] -> op synthesis -> [executable op] -> op execution```
* </UL> ///
* <P>At runtime, the chain of these together ({@code cycle -> op mapping -> op dispensing -> op}) is cached /// or in another way, simply `opExecution(opFunction(cycle))`.
* as a look-up table of op dispensers. This results in the simpler logical form {@code cycle -> op synthesis -> ///
* operation}. /// This is a simplified view of the detailed steps, most of which are handled automatically by the nosqlbench engine:
* </P> ///
* /// ```
* <H3>Variable Naming Conventions</H3> /// cycle value
* <p> /// -> op template sequencing # memoized
* Within the related {@link DriverAdapter} APIs, the following conventions are (more often) used, and will be found /// -> op template selection # memoized
* everywhere: /// -> op template parsing # memoized
* <UL> /// -> op template normalization # memoized
* <LI>{@code namedF} describes a namedFunction variable. Functional patterns are used everywhere in these APIs /// -> op type mapping # memoized
* .</LI> /// -> op dispensing
* <LI>{@code namedC} describes a namedComponent variable. All key elements of the nosqlbench runtime are /// -> op execution
* part of a component tree.</LI> ///```
* <LI>{@code pop} describes a {@link ParsedOp} instance.</LI> ///
* </UL> /// Notice that all of the stages are optimized via a form of memoization. This is a side-effect of forcing the
* </P> /// initialization of the op construction pipelines into lamda form, where unchanging initialization data is captured
* <H3>Generic Parameters</H3> /// explicitly or by reference in closures. This works in our favor for performance, since these lambdas are
* <p> /// optimized and executed very efficiently within modern Java VMs.
* When a new driver adapter is defined with the generic parameters below, it becomes easy to build out a matching ///
* DriverAdapter with any modern IDE.</P> /// ### Variable Naming Conventions
* ///
* @param <OPTYPE> /// Within the related [DriverAdapter] APIs, the following conventions are (more often) used, and will be
* The type of {@link CycleOp} which will be used to wrap all operations for this driver adapter. This allows you /// found everywhere:
* to add context or features common to all operations of this type. This can be a simple <a /// - `namedF` describes a namedFunction variable. Functional patterns are used everywhere in these APIs.
* href="https://en.wikipedia.org/wiki/Marker_interface_pattern">Marker</a> interface, or it can be something more /// - `namedC` describes a namedComponent variable. All key elements of the nosqlbench runtime are part of a
* concrete that captures common logic or state across all the operations used for a given adapter. It is highly /// component tree.
* advised to <EM>NOT</EM> leave it as simply {@code CycleOp<?>}, since specific op implementations offer much /// - `pop` describes a [ParsedOp] instance.
* better performance. ///
* @param <SPACETYPE> /// ### Generic Parameters
* The type of context space used by this driver to hold cached instances of clients, session, or other native /// When a new driver adapter is defined with the generic parameters below, it becomes easy to build out a matching
* driver state. This is the shared state which might be needed during construction operations for an adapter. /// [DriverAdapter] with any modern IDE.
* <EM>No other mechanism is provided nor intended for holding adapter-specific state. You must store it in ///
* this type. This includes client instances, codec mappings, or anything else that a single instance of an /// @param OPTYPE The type of [CycleOp] which will be used to wrap all operations for this driver adapter. This
* application would need to effectively use a given native driver.</EM> /// allows you to add context or features common to all operations of this type. This can be a simple
*/ /// [marker](https://en.wikipedia.org/wiki/Marker_interface_pattern) interface, or it can be something more concrete
/// that captures common logic or state across all the operations used for a given adapter. It is
/// highly advised to _NOT_ leave it as simply `CycleOp<?>`, since specific op implementations offer much
/// better performance.
///
/// @param SPACETYPE The type of context space used by this driver to hold cached instances of clients, session, or
/// other native driver state. This is the shared state which might be needed during construction operations for an
/// adapter. No other mechanism is provided nor intended for holding adapter-specific state. You must store it in
/// this type. This includes client instances, codec mappings, or anything else that a single instance of an
/// application would need to effectively use a given native driver.
public interface DriverAdapter<OPTYPE extends CycleOp<?>, SPACETYPE extends Space> extends NBComponent { public interface DriverAdapter<OPTYPE extends CycleOp<?>, SPACETYPE extends Space> extends NBComponent {
/** ///
* <p> /// ## Op Mapping
* <H2>Op Mapping</H2> ///
* An Op Mapper is a function which can look at a {@link ParsedOp} and create a matching {@link OpDispenser}. /// An Op Mapper is a function which can look at a [ParsedOp] and create a matching [OpDispenser].
* An OpDispenser is a function that will produce a special type {@link OPTYPE} that this DriverAdapter implements /// An OpDispenser is a function that will produce a special type [OPTYPE] that this DriverAdapter implements.
* as its op implementation. There may be many different ops supported by an adapter, thus there may be similarly /// There may be many different ops supported by an adapter, thus there may be similarly many dispensers.
* many dispensers.</p> ///
* /// Both [OpMapper] and [OpDispenser] are functions. The role of [OpMapper] is to map the op template provided
* <p> /// by the user to an op implementation provided by the driver adapter, and then to create a suitable function for
* Both {@link OpMapper} and {@link OpDispenser} are functions. The role of {@link OpMapper} is to /// creating that type of operations, known as the [OpDispenser].
* map the op template provided by the user to an op implementation provided by the driver adapter, ///
* and then to create a factor function for it (the {@link OpDispenser}).</p> /// These roles are split for a very good reason: Mapping what the user wants to do with an op template
* /// is resource intensive, and should be as pre-baked as possible. This phase is the _op mapping_ phase.
* <p>These roles are split for a very good reason: Mapping what the user wants to do with an op template /// It is essential that the mapping logic be very clear and maintainable. Performance is not as important
* is resource intenstive, and should be as pre-baked as possible. This phase is the <EM>op mapping</EM> phase. /// at this phase, because all of the mapping logic is run during initialization of an activity.
* It is essential that the mapping logic be very clear and maintainable. Performance is not as important ///
* at this phase, because all of the mapping logic is run during initialization of an activity. /// Conversely, _op dispensing_ (the next phase) while an activity is running should be as efficient as possible.
* </p> /// @return a dispensing function for an [OPTYPE] op generation
* <p>
* Conversely, <EM>op dispensing</EM> (the next phase) while an activity is running should be as efficient as
* possible.
* </p>
*
* @return a dispensing function for {@link OPTYPE} op generation
*/
OpMapper<OPTYPE, SPACETYPE> getOpMapper(); OpMapper<OPTYPE, SPACETYPE> getOpMapper();
/** /// The preprocessor function allows the driver adapter to remap the fields in the op template before they are
* The preprocessor function allows the driver adapter to remap /// interpreted canonically. At this level, the transform is applied once to the input map (once per op
* the fields in the op template before they are interpreted canonically. /// template) to yield the map that is provided to [OpMapper] implementations. _This is here to make
* At this level, the transform is applied once to the input map /// backwards compatibility possible for op templates which have changed. Avoid using it unless necessary._
* (once per op template) to yield the map that is provided to /// @return A function to pre-process the op template fields.
* {@link OpMapper} implementations. <EM>This is here to make backwards compatibility
* possible for op templates which have changed. Avoid using it unless necessary.</EM>
*
* @return A function to pre-process the op template fields.
*/
default Function<Map<String, Object>, Map<String, Object>> getPreprocessor() { default Function<Map<String, Object>, Map<String, Object>> getPreprocessor() {
return f -> f; return f -> f;
} }
/**
* When a driver needs to identify an error uniquely for the purposes of /// When a driver needs to identify an error uniquely for the purposes of routing it to the correct error
* routing it to the correct error handler, or naming it in logs, or naming /// handler, or naming it in logs, or naming metrics, override this method in your activity.
* metrics, override this method in your activity. /// @return A function that can reliably and safely map an instance of Throwable to a stable adapter-specific name.
*
* @return A function that can reliably and safely map an instance of Throwable to a stable adapter-specific name.
*/
default Function<Throwable, String> getErrorNameMapper() { default Function<Throwable, String> getErrorNameMapper() {
return t -> t.getClass().getSimpleName(); return t -> t.getClass().getSimpleName();
} }
@ -157,20 +151,17 @@ public interface DriverAdapter<OPTYPE extends CycleOp<?>, SPACETYPE extends Spac
return List.of(f -> f); return List.of(f -> f);
} }
/** /// This method allows each driver adapter to create named state which is automatically cached and re-used by
* <P>This method allows each driver adapter to create named state which is automatically /// name. For each (driver,space) combination in an activity, a distinct space instance will be created. In
* cached and re-used by name. For each (driver,space) combination in an activity, /// general, adapter developers will use the space type associated with an adapter to wrap native driver
* a distinct space instance will be created. In general, adapter developers will /// instances one-to-one. As such, if the space implementation is an [AutoCloseable], it will be
* use the space type associated with an adapter to wrap native driver instances /// explicitly shutdown as part of the activity shutdown.
* one-to-one. As such, if the space implementation is a {@link AutoCloseable}, ///
* it will be explicitly shutdown as part of the activity shutdown.</P> /// It is not necessary to implement a space for a stateless driver adapter, or one which injects all necessary
* /// state into each op instance.
* <p>It is not necessary to implement a space for a stateless driver adapter, or one ///
* which puts all state into each op instance.</p> /// @return A function which can initialize a new Space, which is a place to hold object state related to
* /// retained objects for the lifetime of a native driver.
* @return A function which can initialize a new Space, which is a place to hold
* object state related to retained objects for the lifetime of a native driver.
*/
default LongFunction<SPACETYPE> getSpaceInitializer(NBConfiguration cfg) { default LongFunction<SPACETYPE> getSpaceInitializer(NBConfiguration cfg) {
return n -> (SPACETYPE) new Space() { return n -> (SPACETYPE) new Space() {
@Override @Override
@ -180,38 +171,38 @@ public interface DriverAdapter<OPTYPE extends CycleOp<?>, SPACETYPE extends Spac
}; };
} }
/// Provides the configuration for this driver adapter, which comes from the superset of
/// activity parameters given for the owning activity. Presently, the driver adapter acts
/// as a proxy to set these parameters on the space, but this will likely be updated.
/// Instead, the configuratin will be properly attached to the space directly, and the APIs
/// supporting it will enforce this.
NBConfiguration getConfiguration(); NBConfiguration getConfiguration();
/** /// The standard way to provide docs for a driver adapter is to put them in one of two common places:
* The standard way to provide docs for a driver adapter is to put them in one of two common places: /// - `resources/<adaptername>.md`
* <ul> /// - A single markdown file which is the named top-level markdown file for this driver adapter.
* <li>&lt;resources&gt;/&lt;adaptername&gt;.md - A single markdown file which is the named top-level /// - `resources/docs/<adaptername>/`
* markdown file for this driver adapter.</li> /// - A directory containing any type of file which is to be included in docs under the adapter name, otherwise
* <li>&lt;resources&gt;/docs/&lt;adaptername&gt;/ - A directory containing any type of file which /// known as the [Service#selector()]
* is to be included in docs under the adapter name, otherwise known as the {@link Service#selector()}</li> /// - `resources/docs/<adaptername>.md`
* <li>&lt;resources&gt;/docs/&lt;adaptername&gt;.md</li> /// - An alternate location for the main doc file for an adapter, assuming you are using the docs/ path.
* </ul> ///
* /// _A build will fail if any driver adapter implementation is missing at least one self-named markdown doc file._
* <P><EM>A build will fail if any driver adapter implementation is missing at least one self-named ///
* markdown doc file.</EM></P> /// @return A [DocsBinder] which describes docs to include for a given adapter.
*
* @return A {@link DocsBinder} which describes docs to include for a given adapter.
*/
default DocsBinder getBundledDocs() { default DocsBinder getBundledDocs() {
Docs docs = new Docs().namespace("drivers"); Docs docs = new Docs().namespace("drivers");
String dev_docspath = "adapter-" + this.getAdapterName() + "/src/main/resources/docs/" + this.getAdapterName(); String dev_docspath = "adapter-" + this.getAdapterName() + "/src/main/resources/docs/" + this.getAdapterName();
String cp_docspath = "docs/" + this.getAdapterName(); String cp_docspath = "docs/" + this.getAdapterName();
Optional<Content<?>> bundled_docs = NBIO.local().pathname(dev_docspath, cp_docspath).first(); Optional<Content<?>> bundled_docs = NBIO.local().pathname(dev_docspath, cp_docspath).first();
bundled_docs.map(Content::asPath).ifPresent(docs::addContentsOf); bundled_docs.map(Content::asPath).ifPresent(docs::addContentsOf);
Optional<Content<?>> maindoc = NBIO.local().pathname("/src/main/resources/" + this.getAdapterName() + ".md", this.getAdapterName() + ".md").first(); Optional<Content<?>> maindoc = NBIO.local().pathname("/src/main/resources/" + this.getAdapterName() + ".md", this.getAdapterName() + ".md").first();
maindoc.map(Content::asPath).ifPresent(docs::addPath); maindoc.map(Content::asPath).ifPresent(docs::addPath);
return docs.asDocsBinder(); return docs.asDocsBinder();
} }
/// Provide the simple name for this [DriverAdapter] implementation, derived from the
/// required [Service] annotation.
default String getAdapterName() { default String getAdapterName() {
Service svc = this.getClass().getAnnotation(Service.class); Service svc = this.getClass().getAnnotation(Service.class);
if (svc == null) { if (svc == null) {
@ -220,31 +211,41 @@ public interface DriverAdapter<OPTYPE extends CycleOp<?>, SPACETYPE extends Spac
return svc.selector(); return svc.selector();
} }
/// Indicate the level of testing and muturity for the current adapter. This is not actively used
/// and may be removed.
/// @deprecated
default Maturity getAdapterMaturity() { default Maturity getAdapterMaturity() {
return this.getClass().getAnnotation(Service.class).maturity(); return this.getClass().getAnnotation(Service.class).maturity();
} }
/** /// The function returned by [#getSpaceFunc(ParsedOp)] provides access to a cache of all stateful objects needed
* <p>The cache of all objects needed within a single instance /// within a single instance of a DriverAdapter. These are generally things needed by operations, or things
* of a DriverAdapter which are not operations. These are generally /// needed during the construction of operations. Typically, a space is where you store a native driver instance
* things needed by operations, or things needed during the /// which is expected to be created/initialized once and reused within an application. Generally, users can
* construction of operations.</p> /// think of __space__ as __driver instance__, or __client instance__, although there are driver adapters that
* /// do things other than wrap native drivers and clients.
* <p>During Adapter Initialization, Op Mapping, Op Synthesis, or Op Execution, ///
* you may need access to the objects in (the or a) space cache. You can build the /// The value of the op field `space` is used to customize the instancing behavior of spaces. If none is provided
* type of context needed and then provide this function to provide new instances /// by the user, then only a singular space will be created for a given adapter in an activity. This is normal,
* when needed.</p> /// and what most users will expect to do. However, customizing the space selector can be a powerful way to test
* /// any system with high logical concurrency. For example, each instance of a native driver will typically
* <p>The function returned by this method is specialized to the space mapping /// maintain its own thread or connection pools, cached resources and so on. ( Unless the developers of
* logic in the op template. Specifically, it uses whatever binding is set on a given /// said native driver are breaking encapsulation by using global singletons in the runtime, which is highly
* op template for the <em>space</em> op field. If none are provided, then this /// frowned upon.) The spaces feature allows any nosqlbench workload to be easily converted into an unreasonably parallel
* becomes a short-circuit for the default '0'. If a non-numeric binding is provided, /// client topology test with a single change. It works the same way for any adapter or protocol supported by
* then an interstitial mapping is added which converts the {@link Object#toString()} /// nosqlbench.
* value to ordinals using a hash map. This is less optimal by far than using ///
* any binding that produces a {@link Number}.</p> /// The value of the op field `space` should be a [Number] type. [Number#intValue()] is used to determine which
* /// space instance is, if needed, initialized first, and then returned. If users provide a non-[Number] type,
* @return A cache of named objects /// then an enumerating layer is added inline to convert any such value to an integer, which is less optimal.
*/ ///
/// During op mapping or dispensing, you may need access to state held by a driver-specific implementation of
/// [SPACETYPE]. In the initialization phase, you only have access to the space function itself.
/// This is important to maintain a boundary betwen the explicitly stateless and stateful parts of the
/// runtime. To use the space, incorporate the space function into the lambdas which produce the operation to be
/// executed. This is typically done in the construtor of the related [OpDispenser].
///
/// @return A cache of named objects
public LongFunction<SPACETYPE> getSpaceFunc(ParsedOp pop); public LongFunction<SPACETYPE> getSpaceFunc(ParsedOp pop);
} }

View File

@ -26,8 +26,9 @@ import io.nosqlbench.nb.api.components.core.NBNamedElement;
* in testing scenarios. Within the operations for an adapter, the space * in testing scenarios. Within the operations for an adapter, the space
* may be needed, for example, to construct prepared statements, or other * may be needed, for example, to construct prepared statements, or other
* 'session-attached' objects. Put any state that you would normally * 'session-attached' objects. Put any state that you would normally
* associate with an instance of a native driver into a space, and use * associate with an instance of a native driver into a space.
* the {@link DriverAdapter#getSpaceCache()} to access it when needed.</P> * A function to access the cycle-specific space instance is provided where
* you might needed, such as in the mapping or dispensing APIs.
*/ */
public interface Space extends NBNamedElement, AutoCloseable { public interface Space extends NBNamedElement, AutoCloseable {