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
*
*
* 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
@ -21,6 +21,9 @@ package io.nosqlbench.adapter.prototype;
import io.nosqlbench.adapters.api.activityimpl.uniform.BaseSpace;
import io.nosqlbench.adapters.api.activityimpl.uniform.DriverAdapter;
/**
* {@inheritDoc}
*/
public class ExampleSpace extends BaseSpace<ExampleSpace> {
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
* the self type to be used in method signatures and return types.
*
* {@inheritDoc}
*
* @param <SelfT>
*/
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.LongFunction;
/**
* <P>The DriverAdapter interface is the top level API for implementing
* 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
* form in native Java code, just as an application might do with a native driver. It can also
* do trivial operations, like simply calling {@code System.out.println(...)}. When you specify an adapter by name,
* you are choosing both the available operations, and the rules for converting a YAML op template into those
* operations. This is a two-step process: The adapter provides mapping logic for converting high-level templates from
* 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
* procedural generation methods.
* </p>
*
* <p>
* Generally speaking, a driver adapter is responsible for
* <UL>
* <LI>Defining a class type for holding related state known as a {@link Space}.
* <UL><LI>The type of this is specified as
* generic parameter {@link SPACETYPE}.</LI></UL></LI>
* <LI>Defining a factory method for constructing an instance of the space.</LI>
* <LI>Recognizing the op templates that are documented for it via an {@link OpMapper}
* and assigning them to an op implementation.
* <UL><LI>The base type of these ops is specified as generic
* parameter {@link OPTYPE}.</LI></UL>
* </LI>
* <LI>Constructing dispensers for each matching op implementation with a matching {@link OpDispenser}</LI>
* implementation.
* </UL>
* <P>At runtime, the chain of these together ({@code cycle -> op mapping -> op dispensing -> op}) is cached
* as a look-up table of op dispensers. This results in the simpler logical form {@code cycle -> op synthesis ->
* operation}.
* </P>
*
* <H3>Variable Naming Conventions</H3>
* <p>
* Within the related {@link DriverAdapter} APIs, the following conventions are (more often) used, and will be found
* everywhere:
* <UL>
* <LI>{@code namedF} describes a namedFunction variable. Functional patterns are used everywhere in these APIs
* .</LI>
* <LI>{@code namedC} describes a namedComponent variable. All key elements of the nosqlbench runtime are
* part of a component tree.</LI>
* <LI>{@code pop} describes a {@link ParsedOp} instance.</LI>
* </UL>
* </P>
* <H3>Generic Parameters</H3>
* <p>
* 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>
*
* @param <OPTYPE>
* The type of {@link CycleOp} which will be used to wrap all operations for this driver adapter. This allows you
* to add context or features common to all operations of this type. This can be a simple <a
* href="https://en.wikipedia.org/wiki/Marker_interface_pattern">Marker</a> 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 <EM>NOT</EM> leave it as simply {@code 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.
* <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
* application would need to effectively use a given native driver.</EM>
*/
/// The DriverAdapter interface is the top level API for implementing
/// 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
/// form in native Java code, and then execute those native operations just as an application might do directly with a
/// native driver. It can also do trivial operations, like simply calling `System.out.println(...)`. What a
/// particular DriverAdapter does is open-ended, but this usually means wrapping a native driver when supporting
/// specific protocols.
///
/// Every DriverAdapter has a simple name, as indicated on it's [Service] annotation. When you
/// specify an adapter by name, you are choosing both the available operations, and the rules for converting
/// 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
/// for both of these phases for all the op types is supports. When used together, they power _op synthesis_ --
/// efficient and deterministic construction of runtime operations using procedural generation methods.
///
/// Generally speaking, a driver adapter is responsible for
/// - Defining a class type for holding related state known as a [Space]. The type of this is specified as generic
/// parameter [SPACETYPE]
/// - Defining a factory method for constructing an instance of the space.
/// - Recognizing the op templates that are documented for it via an [OpMapper] and assigning them to an op
/// implementation. The base type of these ops is specified as generic [OPTYPE]
/// - Constructing dispensers for each matching op implementation with a matching [OpDispenser] implementation.
///
/// At a high level, the following sequence is executed for every cycle in an activity:
/// ```
///[cycle value] -> op synthesis -> [executable op] -> op execution```
///
/// or in another way, simply `opExecution(opFunction(cycle))`.
///
/// This is a simplified view of the detailed steps, most of which are handled automatically by the nosqlbench engine:
///
/// ```
/// cycle value
/// -> op template sequencing # memoized
/// -> op template selection # memoized
/// -> op template parsing # memoized
/// -> op template normalization # memoized
/// -> op type mapping # memoized
/// -> op dispensing
/// -> op execution
///```
///
/// Notice that all of the stages are optimized via a form of memoization. This is a side-effect of forcing the
/// initialization of the op construction pipelines into lamda form, where unchanging initialization data is captured
/// explicitly or by reference in closures. This works in our favor for performance, since these lambdas are
/// optimized and executed very efficiently within modern Java VMs.
///
/// ### Variable Naming Conventions
///
/// Within the related [DriverAdapter] APIs, the following conventions are (more often) used, and will be
/// found everywhere:
/// - `namedF` describes a namedFunction variable. Functional patterns are used everywhere in these APIs.
/// - `namedC` describes a namedComponent variable. All key elements of the nosqlbench runtime are part of a
/// component tree.
/// - `pop` describes a [ParsedOp] instance.
///
/// ### Generic Parameters
/// 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.
///
/// @param OPTYPE The type of [CycleOp] which will be used to wrap all operations for this driver adapter. This
/// 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 {
/**
* <p>
* <H2>Op Mapping</H2>
* An Op Mapper is a function which can look at a {@link ParsedOp} and create a matching {@link OpDispenser}.
* An OpDispenser is a function that will produce a special type {@link OPTYPE} that this DriverAdapter implements
* as its op implementation. There may be many different ops supported by an adapter, thus there may be similarly
* many dispensers.</p>
*
* <p>
* Both {@link OpMapper} and {@link OpDispenser} are functions. The role of {@link OpMapper} is to
* 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>
*
* <p>These roles are split for a very good reason: Mapping what the user wants to do with an op template
* is resource intenstive, and should be as pre-baked as possible. This phase is the <EM>op mapping</EM> phase.
* 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.
* </p>
* <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
*/
///
/// ## Op Mapping
///
/// 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 [OPTYPE] that this DriverAdapter implements.
/// There may be many different ops supported by an adapter, thus there may be similarly many dispensers.
///
/// Both [OpMapper] and [OpDispenser] are functions. The role of [OpMapper] is to map the op template provided
/// by the user to an op implementation provided by the driver adapter, and then to create a suitable function for
/// creating that type of operations, known as the [OpDispenser].
///
/// 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.
/// 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.
/// @return a dispensing function for an [OPTYPE] op generation
OpMapper<OPTYPE, SPACETYPE> getOpMapper();
/**
* The preprocessor function allows the driver adapter to remap
* the fields in the op template before they are interpreted canonically.
* At this level, the transform is applied once to the input map
* (once per op template) to yield the map that is provided to
* {@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.
*/
/// The preprocessor function allows the driver adapter to remap the fields in the op template before they are
/// interpreted canonically. At this level, the transform is applied once to the input map (once per op
/// template) to yield the map that is provided to [OpMapper] implementations. _This is here to make
/// backwards compatibility possible for op templates which have changed. Avoid using it unless necessary._
/// @return A function to pre-process the op template fields.
default Function<Map<String, Object>, Map<String, Object>> getPreprocessor() {
return f -> f;
}
/**
* When a driver needs to identify an error uniquely for the purposes of
* routing it to the correct error handler, or naming it in logs, or naming
* 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.
*/
/// When a driver needs to identify an error uniquely for the purposes of routing it to the correct error
/// handler, or naming it in logs, or naming 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.
default Function<Throwable, String> getErrorNameMapper() {
return t -> t.getClass().getSimpleName();
}
@ -157,20 +151,17 @@ public interface DriverAdapter<OPTYPE extends CycleOp<?>, SPACETYPE extends Spac
return List.of(f -> f);
}
/**
* <P>This method allows each driver adapter to create named state which is automatically
* cached and re-used by name. For each (driver,space) combination in an activity,
* a distinct space instance will be created. In general, adapter developers will
* use the space type associated with an adapter to wrap native driver instances
* 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>
*
* <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.
*/
/// This method allows each driver adapter to create named state which is automatically cached and re-used by
/// name. For each (driver,space) combination in an activity, a distinct space instance will be created. In
/// general, adapter developers will use the space type associated with an adapter to wrap native driver
/// instances one-to-one. As such, if the space implementation is an [AutoCloseable], it will be
/// explicitly shutdown as part of the activity shutdown.
///
/// It is not necessary to implement a space for a stateless driver adapter, or one which injects all necessary
/// state into each op instance.
///
/// @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) {
return n -> (SPACETYPE) new Space() {
@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();
/**
* The standard way to provide docs for a driver adapter is to put them in one of two common places:
* <ul>
* <li>&lt;resources&gt;/&lt;adaptername&gt;.md - A single markdown file which is the named top-level
* markdown file for this driver adapter.</li>
* <li>&lt;resources&gt;/docs/&lt;adaptername&gt;/ - A directory containing any type of file which
* is to be included in docs under the adapter name, otherwise known as the {@link Service#selector()}</li>
* <li>&lt;resources&gt;/docs/&lt;adaptername&gt;.md</li>
* </ul>
*
* <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 {@link DocsBinder} which describes docs to include for a given adapter.
*/
/// The standard way to provide docs for a driver adapter is to put them in one of two common places:
/// - `resources/<adaptername>.md`
/// - A single markdown file which is the named top-level markdown file for this driver adapter.
/// - `resources/docs/<adaptername>/`
/// - A directory containing any type of file which is to be included in docs under the adapter name, otherwise
/// known as the [Service#selector()]
/// - `resources/docs/<adaptername>.md`
/// - An alternate location for the main doc file for an adapter, assuming you are using the docs/ path.
///
/// _A build will fail if any driver adapter implementation is missing at least one self-named markdown doc file._
///
/// @return A [DocsBinder] which describes docs to include for a given adapter.
default DocsBinder getBundledDocs() {
Docs docs = new Docs().namespace("drivers");
String dev_docspath = "adapter-" + this.getAdapterName() + "/src/main/resources/docs/" + this.getAdapterName();
String cp_docspath = "docs/" + this.getAdapterName();
Optional<Content<?>> bundled_docs = NBIO.local().pathname(dev_docspath, cp_docspath).first();
bundled_docs.map(Content::asPath).ifPresent(docs::addContentsOf);
Optional<Content<?>> maindoc = NBIO.local().pathname("/src/main/resources/" + this.getAdapterName() + ".md", this.getAdapterName() + ".md").first();
maindoc.map(Content::asPath).ifPresent(docs::addPath);
return docs.asDocsBinder();
}
/// Provide the simple name for this [DriverAdapter] implementation, derived from the
/// required [Service] annotation.
default String getAdapterName() {
Service svc = this.getClass().getAnnotation(Service.class);
if (svc == null) {
@ -220,31 +211,41 @@ public interface DriverAdapter<OPTYPE extends CycleOp<?>, SPACETYPE extends Spac
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() {
return this.getClass().getAnnotation(Service.class).maturity();
}
/**
* <p>The cache of all objects needed within a single instance
* of a DriverAdapter which are not operations. These are generally
* things needed by operations, or things needed during the
* construction of operations.</p>
*
* <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
* type of context needed and then provide this function to provide new instances
* when needed.</p>
*
* <p>The function returned by this method is specialized to the space mapping
* logic in the op template. Specifically, it uses whatever binding is set on a given
* op template for the <em>space</em> op field. If none are provided, then this
* becomes a short-circuit for the default '0'. If a non-numeric binding is provided,
* then an interstitial mapping is added which converts the {@link Object#toString()}
* value to ordinals using a hash map. This is less optimal by far than using
* any binding that produces a {@link Number}.</p>
*
* @return A cache of named objects
*/
/// The function returned by [#getSpaceFunc(ParsedOp)] provides access to a cache of all stateful objects needed
/// within a single instance of a DriverAdapter. These are generally things needed by operations, or things
/// needed during the construction of operations. Typically, a space is where you store a native driver instance
/// which is expected to be created/initialized once and reused within an application. Generally, users can
/// think of __space__ as __driver instance__, or __client instance__, although there are driver adapters that
/// do things other than wrap native drivers and clients.
///
/// The value of the op field `space` is used to customize the instancing behavior of spaces. If none is provided
/// by the user, then only a singular space will be created for a given adapter in an activity. This is normal,
/// 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
/// maintain its own thread or connection pools, cached resources and so on. ( Unless the developers of
/// said native driver are breaking encapsulation by using global singletons in the runtime, which is highly
/// frowned upon.) The spaces feature allows any nosqlbench workload to be easily converted into an unreasonably parallel
/// client topology test with a single change. It works the same way for any adapter or protocol supported by
/// nosqlbench.
///
/// 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,
/// 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);
}

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
* may be needed, for example, to construct prepared statements, or other
* 'session-attached' objects. Put any state that you would normally
* associate with an instance of a native driver into a space, and use
* the {@link DriverAdapter#getSpaceCache()} to access it when needed.</P>
* associate with an instance of a native driver into a space.
* 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 {