diff options
author | Henning Baldersheim <balder@yahoo-inc.com> | 2022-10-17 16:27:02 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-17 16:27:02 +0200 |
commit | bbcccf78cfaa5438c18f188c5dd15a9a979617ee (patch) | |
tree | cb3b35e15c47c108bae252c1cc169945c88c365c | |
parent | 849401dd245eb9193d1ca31bc288c6b665795747 (diff) | |
parent | b7123d3a07bc823961e452ad527d00e236012ebe (diff) |
Merge branch 'master' into balder/gc-unused-phrase-flags
608 files changed, 11336 insertions, 7806 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b8d24d690d..3b9bc1c9f07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ # @author Vegard Sjonfjell # @author Eirik Nygaard # @author Arnstein Ressem -cmake_minimum_required(VERSION 3.5 FATAL_ERROR) +cmake_minimum_required(VERSION 3.18 FATAL_ERROR) include(functions.cmake) list(APPEND CMAKE_MODULE_PATH @@ -19,8 +19,24 @@ vespa_use_default_java_home() project(vespa CXX C) vespa_use_default_vespa_unprivileged() vespa_use_default_cmake_install_prefix() +include(GNUInstallDirs) vespa_use_default_vespa_user() vespa_use_default_vespa_group() +vespa_use_default_vespa_deps_prefix() +vespa_use_default_cmake_prefix_path() + +SET(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL) +SET(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC) + +find_package(LLVM REQUIRED CONFIG) +message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") +message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") +message(STATUS "LLVM_INCLUDE_DIRS is ${LLVM_INCLUDE_DIRS}") +message(STATUS "LLVM_LIBRARY_DIRS is ${LLVM_LIBRARY_DIRS}") +message(STATUS "LLVM_INCLUDE_DIR is ${LLVM_INCLUDE_DIR}") +message(STATUS "LLVM_MAIN_INCLUDE_DIR is ${LLVM_MAIN_INCLUDE_DIR}") +message(STATUS "LLVM_LIBRARY_DIR is ${LLVM_LIBRARY_DIR}") + vespa_use_default_build_settings() # allows import of project in CLion on OSX diff --git a/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java b/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java index 081e9ad6036..9696e9c9848 100644 --- a/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java +++ b/application-preprocessor/src/main/java/com/yahoo/application/preprocessor/ApplicationPreprocessor.java @@ -3,14 +3,21 @@ package com.yahoo.application.preprocessor; import com.yahoo.config.application.api.ApplicationPackage; import com.yahoo.config.model.application.provider.BaseDeployLogger; +import com.yahoo.config.model.application.provider.DeployData; import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; +import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.yolean.Exceptions; import java.io.File; import java.io.IOException; import java.util.Optional; +import java.util.Set; /** * Main entry for preprocessing an application package. @@ -21,36 +28,75 @@ public class ApplicationPreprocessor { private final File applicationDir; private final Optional<File> outputDir; + private final Optional<InstanceName> instance; private final Optional<Environment> environment; private final Optional<RegionName> region; + private final Tags tags; - public ApplicationPreprocessor(File applicationDir, Optional<File> outputDir, Optional<Environment> environment, Optional<RegionName> region) { + public ApplicationPreprocessor(File applicationDir, + Optional<File> outputDir, + Optional<InstanceName> instance, + Optional<Environment> environment, + Optional<RegionName> region, + Tags tags) { this.applicationDir = applicationDir; this.outputDir = outputDir; + this.instance = instance; this.environment = environment; this.region = region; + this.tags = tags; } public void run() throws IOException { FilesApplicationPackage.Builder applicationPackageBuilder = new FilesApplicationPackage.Builder(applicationDir); outputDir.ifPresent(applicationPackageBuilder::preprocessedDir); - ApplicationPackage preprocessed = applicationPackageBuilder.build().preprocess( - new Zone(environment.orElse(Environment.defaultEnvironment()), region.orElse(RegionName.defaultName())), - new BaseDeployLogger()); + applicationPackageBuilder.deployData(new DeployData(applicationDir.getAbsolutePath(), + ApplicationId.from(TenantName.defaultName(), + ApplicationName.defaultName(), + instance.orElse(InstanceName.defaultName())), + tags, + System.currentTimeMillis(), + false, + 0L, + -1)); + + ApplicationPackage preprocessed = applicationPackageBuilder.build().preprocess(new Zone(environment.orElse(Environment.defaultEnvironment()), + region.orElse(RegionName.defaultName())), + new BaseDeployLogger()); preprocessed.validateXML(); } public static void main(String[] args) { - int argCount = args.length; - if (argCount < 1) { - System.out.println("Usage: vespa-application-preprocessor <application package path> [environment] [region] [output path]"); + if (args.length < 1) { + System.out.println("Usage: vespa-application-preprocessor <application package path> [instance] [environment] [region] [tag] [output path]"); System.exit(1); } File applicationDir = new File(args[0]); - Optional<Environment> environment = (argCount > 1) ? Optional.of(Environment.valueOf(args[1])) : Optional.empty(); - Optional<RegionName> region = (argCount > 2) ? Optional.of(RegionName.from(args[2])) : Optional.empty(); - Optional<File> outputDir = (argCount > 3) ? Optional.of(new File(args[3])) : Optional.empty(); - ApplicationPreprocessor preprocessor = new ApplicationPreprocessor(applicationDir, outputDir, environment, region); + Optional<InstanceName> instance; + Optional<Environment> environment; + Optional<RegionName> region; + Tags tags; + Optional<File> outputDir; + if (args.length <= 4) { // Legacy: No instance and tags + instance = Optional.empty(); + environment = args.length > 1 ? Optional.of(Environment.valueOf(args[1])) : Optional.empty(); + region = args.length > 2 ? Optional.of(RegionName.from(args[2])) : Optional.empty(); + tags = Tags.empty(); + outputDir = args.length > 3 ? Optional.of(new File(args[3])) : Optional.empty(); + } + else { + instance = Optional.of(InstanceName.from(args[1])); + environment = Optional.of(Environment.valueOf(args[2])); + region = Optional.of(RegionName.from(args[3])); + tags = Tags.fromString(args[4]); + outputDir = args.length > 5 ? Optional.of(new File(args[5])) : Optional.empty(); + } + ApplicationPreprocessor preprocessor = new ApplicationPreprocessor(applicationDir, + outputDir, + instance, + environment, + region, + tags); try { preprocessor.run(); System.out.println("Application preprocessed successfully and written to " + diff --git a/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.java b/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.java index 0e5d8fb2784..4de4312c099 100644 --- a/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.java +++ b/application-preprocessor/src/test/java/com/yahoo/application/preprocessor/ApplicationPreprocessorTest.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.application.preprocessor; +import com.yahoo.config.provision.Tags; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.xml.sax.SAXException; @@ -18,11 +19,13 @@ public class ApplicationPreprocessorTest { // Basic test just to check that instantiation and run() works. Unit testing is in config-application-package @Test - void basic() throws ParserConfigurationException, TransformerException, SAXException, IOException { + void basic() throws IOException { ApplicationPreprocessor preprocessor = new ApplicationPreprocessor(new File("src/test/resources/simple"), - Optional.of(newFolder(outputDir, "basic")), - Optional.empty(), - Optional.empty()); + Optional.of(newFolder(outputDir, "basic")), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Tags.empty()); preprocessor.run(); } diff --git a/application/src/main/java/com/yahoo/application/container/handler/Headers.java b/application/src/main/java/com/yahoo/application/container/handler/Headers.java index 03b31e38659..53ffe76c923 100644 --- a/application/src/main/java/com/yahoo/application/container/handler/Headers.java +++ b/application/src/main/java/com/yahoo/application/container/handler/Headers.java @@ -175,44 +175,44 @@ public class Headers implements Map<String, List<String>> { } /** - * <p>Removes the given value from the entry of the specified key.</p> + * Removes the given value from the entry of the specified key. * - * @param key The key of the entry to remove from. - * @param value The value to remove from the entry. - * @return True if the value was removed. + * @param key the key of the entry to remove from + * @param value the value to remove from the entry + * @return true if the value was removed */ public boolean remove(String key, String value) { return h.remove(key, value); } /** - * <p>Convenience method for retrieving the first value of a named header field. If the header is not set, or if the - * value list is empty, this method returns null.</p> + * Convenience method for retrieving the first value of a named header field. If the header is not set, or if the + * value list is empty, this method returns null. * - * @param key The key whose first value to return. - * @return The first value of the named header, or null. + * @param key the key whose first value to return. + * @return the first value of the named header, or null. */ public String getFirst(String key) { return h.getFirst(key); } /** - * <p>Convenience method for checking whether or not a named header field is <em>true</em>. To satisfy this, the + * Convenience method for checking whether a named header field is <em>true</em>. To satisfy this, the * header field needs to have at least 1 entry, and Boolean.valueOf() of all its values must parse as - * <em>true</em>.</p> + * <em>true</em>. * - * @param key The key whose values to parse as a boolean. - * @return The boolean value of the named header. + * @param key the key whose values to parse as a boolean + * @return the boolean value of the named header */ public boolean isTrue(String key) { return h.isTrue(key); } /** - * <p>Returns an unmodifiable list of all key-value pairs of this. This provides a flattened view on the content of - * this map.</p> + * Returns an unmodifiable list of all key-value pairs of this. This provides a flattened view on the content of + * this map. * - * @return The collection of entries. + * @return the collection of entries */ public List<Entry<String, String>> entries() { return h.entries(); diff --git a/build_settings.cmake b/build_settings.cmake index 0976e73442e..0d5b51cc26f 100644 --- a/build_settings.cmake +++ b/build_settings.cmake @@ -83,6 +83,9 @@ if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fvisibility-inlines-hidden ") endif() +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11.0) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines") +endif() if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGOOGLE_PROTOBUF_NO_RDTSC") if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15.0) diff --git a/client/go/cmd/logfmt/cmd.go b/client/go/cmd/logfmt/cmd.go index 54e82844a30..84322c7ff08 100644 --- a/client/go/cmd/logfmt/cmd.go +++ b/client/go/cmd/logfmt/cmd.go @@ -38,6 +38,7 @@ and converts it to something human-readable`, cmd.Flags().StringVarP(&curOptions.OnlyHostname, "host", "H", "", "select only one host") cmd.Flags().StringVarP(&curOptions.OnlyPid, "pid", "p", "", "select only one process ID") cmd.Flags().StringVarP(&curOptions.OnlyService, "service", "S", "", "select only one service") + cmd.Flags().VarP(&curOptions.Format, "format", "F", "select logfmt output format, vespa (default), json or raw are supported. The json output format is not stable, and will change in the future.") cmd.Flags().MarkHidden("tc") cmd.Flags().MarkHidden("ts") cmd.Flags().MarkHidden("dequotenewlines") diff --git a/client/go/cmd/logfmt/formatflags.go b/client/go/cmd/logfmt/formatflags.go new file mode 100644 index 00000000000..097746d696f --- /dev/null +++ b/client/go/cmd/logfmt/formatflags.go @@ -0,0 +1,41 @@ +package logfmt + +import ( + "fmt" + "strings" +) + +type OutputFormat int + +const ( + FormatVespa OutputFormat = iota //default is vespa + FormatRaw + FormatJSON +) + +func (v *OutputFormat) Type() string { + return "output format" +} + +func (v *OutputFormat) String() string { + flagNames := []string{ + "vespa", + "raw", + "json", + } + return flagNames[*v] +} + +func (v *OutputFormat) Set(val string) error { + switch strings.ToLower(val) { + case "vespa": + *v = FormatVespa + case "raw": + *v = FormatRaw + case "json": + *v = FormatJSON + default: + return fmt.Errorf("'%s' is not a valid format argument", val) + } + return nil +} diff --git a/client/go/cmd/logfmt/formatflags_test.go b/client/go/cmd/logfmt/formatflags_test.go new file mode 100644 index 00000000000..53c47d24208 --- /dev/null +++ b/client/go/cmd/logfmt/formatflags_test.go @@ -0,0 +1,30 @@ +package logfmt + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestOutputFormat(t *testing.T) { + type args struct { + val string + } + tests := []struct { + expected OutputFormat + arg string + wantErr assert.ErrorAssertionFunc + }{ + {FormatVespa, "vespa", assert.NoError}, + {FormatRaw, "raw", assert.NoError}, + {FormatJSON, "json", assert.NoError}, + {-1, "foo", assert.Error}, + } + for _, tt := range tests { + t.Run(tt.arg, func(t *testing.T) { + var v OutputFormat = -1 + tt.wantErr(t, v.Set(tt.arg), fmt.Sprintf("Set(%v)", tt.arg)) + assert.Equal(t, v, tt.expected) + }) + } +} diff --git a/client/go/cmd/logfmt/handleline.go b/client/go/cmd/logfmt/handleline.go index 33a1a1b386b..813ca82acb4 100644 --- a/client/go/cmd/logfmt/handleline.go +++ b/client/go/cmd/logfmt/handleline.go @@ -5,58 +5,122 @@ package logfmt import ( + "bytes" + "encoding/json" "fmt" "strconv" "strings" "time" ) -// handle a line in "vespa.log" format; do filtering and formatting as specified in opts +type logFields struct { + timestamp string // seconds, optional fractional seconds + host string + pid string // pid, optional tid + service string + component string + level string + messages []string +} -func handleLine(opts *Options, line string) (output string, err error) { - fields := strings.SplitN(line, "\t", 7) - if len(fields) < 7 { +// handle a line in "vespa.log" format; do filtering and formatting as specified in opts +func handleLine(opts *Options, line string) (string, error) { + fieldStrings := strings.SplitN(line, "\t", 7) + if len(fieldStrings) < 7 { return "", fmt.Errorf("not enough fields: '%s'", line) } - timestampfield := fields[0] // seconds, optional fractional seconds - hostfield := fields[1] - pidfield := fields[2] // pid, optional tid - servicefield := fields[3] - componentfield := fields[4] - levelfield := fields[5] - messagefields := fields[6:] + fields := logFields{ + timestamp: fieldStrings[0], + host: fieldStrings[1], + pid: fieldStrings[2], + service: fieldStrings[3], + component: fieldStrings[4], + level: fieldStrings[5], + messages: fieldStrings[6:], + } - if !opts.showLevel(levelfield) { + if !opts.showLevel(fields.level) { return "", nil } - if opts.OnlyHostname != "" && opts.OnlyHostname != hostfield { + if opts.OnlyHostname != "" && opts.OnlyHostname != fields.host { return "", nil } - if opts.OnlyPid != "" && opts.OnlyPid != pidfield { + if opts.OnlyPid != "" && opts.OnlyPid != fields.pid { return "", nil } - if opts.OnlyService != "" && opts.OnlyService != servicefield { + if opts.OnlyService != "" && opts.OnlyService != fields.service { return "", nil } - if opts.OnlyInternal && !isInternal(componentfield) { + if opts.OnlyInternal && !isInternal(fields.component) { return "", nil } - if opts.ComponentFilter.unmatched(componentfield) { + if opts.ComponentFilter.unmatched(fields.component) { return "", nil } - if opts.MessageFilter.unmatched(strings.Join(messagefields, "\t")) { + if opts.MessageFilter.unmatched(strings.Join(fields.messages, "\t")) { return "", nil } + switch opts.Format { + case FormatRaw: + return line + "\n", nil + case FormatJSON: + return handleLineJson(opts, &fields) + case FormatVespa: + fallthrough + default: + return handleLineVespa(opts, &fields) + } +} + +func parseTimestamp(timestamp string) (time.Time, error) { + secs, err := strconv.ParseFloat(timestamp, 64) + if err != nil { + return time.Time{}, err + } + nsecs := int64(secs * 1e9) + return time.Unix(0, nsecs), nil +} + +type logFieldsJson struct { + Timestamp string `json:"timestamp"` + Host string `json:"host"` + Pid string `json:"pid"` + Service string `json:"service"` + Component string `json:"component"` + Level string `json:"level"` + Messages []string `json:"messages"` +} + +func handleLineJson(_ *Options, fields *logFields) (string, error) { + timestamp, err := parseTimestamp(fields.timestamp) + if err != nil { + return "", err + } + outputFields := logFieldsJson{ + Timestamp: timestamp.Format(time.RFC3339Nano), + Host: fields.host, + Pid: fields.pid, + Service: fields.service, + Component: fields.component, + Level: fields.level, + Messages: fields.messages, + } + buf := bytes.Buffer{} + if err := json.NewEncoder(&buf).Encode(&outputFields); err != nil { + return "", err + } + return buf.String(), nil +} + +func handleLineVespa(opts *Options, fields *logFields) (string, error) { var buf strings.Builder if opts.showField("fmttime") { - secs, err := strconv.ParseFloat(timestampfield, 64) + timestamp, err := parseTimestamp(fields.timestamp) if err != nil { return "", err } - nsecs := int64(secs * 1e9) - timestamp := time.Unix(0, nsecs) if opts.showField("usecs") { buf.WriteString(timestamp.Format("[2006-01-02 15:04:05.000000] ")) } else if opts.showField("msecs") { @@ -65,36 +129,36 @@ func handleLine(opts *Options, line string) (output string, err error) { buf.WriteString(timestamp.Format("[2006-01-02 15:04:05] ")) } } else if opts.showField("time") { - buf.WriteString(timestampfield) + buf.WriteString(fields.timestamp) buf.WriteString(" ") } if opts.showField("host") { - buf.WriteString(fmt.Sprintf("%-8s ", hostfield)) + buf.WriteString(fmt.Sprintf("%-8s ", fields.host)) } if opts.showField("level") { - buf.WriteString(fmt.Sprintf("%-7s ", strings.ToUpper(levelfield))) + buf.WriteString(fmt.Sprintf("%-7s ", strings.ToUpper(fields.level))) } if opts.showField("pid") { // OnlyPid, _, _ := strings.Cut(pidfield, "/") - buf.WriteString(fmt.Sprintf("%6s ", pidfield)) + buf.WriteString(fmt.Sprintf("%6s ", fields.pid)) } if opts.showField("service") { if opts.TruncateService { - buf.WriteString(fmt.Sprintf("%-9.9s ", servicefield)) + buf.WriteString(fmt.Sprintf("%-9.9s ", fields.service)) } else { - buf.WriteString(fmt.Sprintf("%-16s ", servicefield)) + buf.WriteString(fmt.Sprintf("%-16s ", fields.service)) } } if opts.showField("component") { if opts.TruncateComponent { - buf.WriteString(fmt.Sprintf("%-15.15s ", componentfield)) + buf.WriteString(fmt.Sprintf("%-15.15s ", fields.component)) } else { - buf.WriteString(fmt.Sprintf("%s\t", componentfield)) + buf.WriteString(fmt.Sprintf("%s\t", fields.component)) } } if opts.showField("message") { var msgBuf strings.Builder - for idx, message := range messagefields { + for idx, message := range fields.messages { if idx > 0 { msgBuf.WriteString("\n\t") } @@ -111,6 +175,5 @@ func handleLine(opts *Options, line string) (output string, err error) { buf.WriteString(message) } buf.WriteString("\n") - output = buf.String() - return + return buf.String(), nil } diff --git a/client/go/cmd/logfmt/options.go b/client/go/cmd/logfmt/options.go index f564ffd4df0..864868d4ce5 100644 --- a/client/go/cmd/logfmt/options.go +++ b/client/go/cmd/logfmt/options.go @@ -24,6 +24,7 @@ type Options struct { TruncateComponent bool ComponentFilter regexFlag MessageFilter regexFlag + Format OutputFormat } func NewOptions() (ret Options) { diff --git a/client/pom.xml b/client/pom.xml index 065cb2c4317..1da3cbc68c5 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -25,7 +25,7 @@ <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> - <version>1.6</version> + <version>1.10.0</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> diff --git a/cloud-tenant-base-dependencies-enforcer/pom.xml b/cloud-tenant-base-dependencies-enforcer/pom.xml index 17b7c7b8995..10f66d355b9 100644 --- a/cloud-tenant-base-dependencies-enforcer/pom.xml +++ b/cloud-tenant-base-dependencies-enforcer/pom.xml @@ -21,7 +21,7 @@ <!-- MUST BE KEPT IN SYNC WITH parent/pom.xml --> <athenz.version>1.10.54</athenz.version> - <bouncycastle.version>1.68</bouncycastle.version> + <bouncycastle.version>1.72</bouncycastle.version> <felix.version>7.0.1</felix.version> <httpclient5.version>5.1.3</httpclient5.version> <httpclient.version>4.5.13</httpclient.version> @@ -195,8 +195,9 @@ <include>org.apache.httpcomponents:httpmime:${httpclient.version}:jar:test</include> <include>org.apache.opennlp:opennlp-tools:1.9.3:jar:test</include> <include>org.apiguardian:apiguardian-api:1.1.0:jar:test</include> - <include>org.bouncycastle:bcpkix-jdk15on:[${bouncycastle.version}]:jar:test</include> - <include>org.bouncycastle:bcprov-jdk15on:[${bouncycastle.version}]:jar:test</include> + <include>org.bouncycastle:bcpkix-jdk18on:[${bouncycastle.version}]:jar:test</include> + <include>org.bouncycastle:bcprov-jdk18on:[${bouncycastle.version}]:jar:test</include> + <include>org.bouncycastle:bcutil-jdk18on:[${bouncycastle.version}]:jar:test</include> <include>org.eclipse.jetty.alpn:alpn-api:[${jetty-alpn.version}]:jar:test</include> <include>org.eclipse.jetty.http2:http2-common:[${jetty.version}]:jar:test</include> <include>org.eclipse.jetty.http2:http2-hpack:[${jetty.version}]:jar:test</include> diff --git a/clustercontroller-apps/src/main/java/com/yahoo/vespa/clustercontroller/apps/clustercontroller/StateRestApiV2Handler.java b/clustercontroller-apps/src/main/java/com/yahoo/vespa/clustercontroller/apps/clustercontroller/StateRestApiV2Handler.java index 5ef4c544bc8..346e58b652f 100644 --- a/clustercontroller-apps/src/main/java/com/yahoo/vespa/clustercontroller/apps/clustercontroller/StateRestApiV2Handler.java +++ b/clustercontroller-apps/src/main/java/com/yahoo/vespa/clustercontroller/apps/clustercontroller/StateRestApiV2Handler.java @@ -15,6 +15,7 @@ import java.util.TreeMap; import java.util.logging.Logger; public class StateRestApiV2Handler extends JDiscHttpRequestHandler { + private static final Logger log = Logger.getLogger(StateRestApiV2Handler.class.getName()); @Inject diff --git a/clustercontroller-reindexer/src/main/java/ai/vespa/reindexing/ReindexingCurator.java b/clustercontroller-reindexer/src/main/java/ai/vespa/reindexing/ReindexingCurator.java index d5324a3f0b8..5c369d2508e 100644 --- a/clustercontroller-reindexer/src/main/java/ai/vespa/reindexing/ReindexingCurator.java +++ b/clustercontroller-reindexer/src/main/java/ai/vespa/reindexing/ReindexingCurator.java @@ -15,7 +15,6 @@ import com.yahoo.vespa.curator.Lock; import com.yahoo.yolean.Exceptions; import java.io.Closeable; -import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.util.List; diff --git a/component/src/main/java/com/yahoo/component/ComponentId.java b/component/src/main/java/com/yahoo/component/ComponentId.java index 7320d24b57c..b869202d3c7 100644 --- a/component/src/main/java/com/yahoo/component/ComponentId.java +++ b/component/src/main/java/com/yahoo/component/ComponentId.java @@ -16,19 +16,11 @@ public final class ComponentId implements Comparable<ComponentId> { private final Spec<Version> spec; private final boolean anonymous; - private static AtomicInteger threadIdCounter = new AtomicInteger(0); + private static final AtomicInteger threadIdCounter = new AtomicInteger(0); - private static ThreadLocal<Counter> threadLocalUniqueId = new ThreadLocal<Counter>() { - @Override protected Counter initialValue() { - return new Counter(); - } - }; + private static final ThreadLocal<Counter> threadLocalUniqueId = ThreadLocal.withInitial(Counter::new); - private static ThreadLocal<String> threadId = new ThreadLocal<String>() { - @Override protected String initialValue() { - return new String("_" + threadIdCounter.getAndIncrement() + "_"); - } - }; + private static final ThreadLocal<String> threadId = ThreadLocal.withInitial(() -> "_" + threadIdCounter.getAndIncrement() + "_"); /** Precomputed string value */ private final String stringValue; @@ -97,9 +89,8 @@ public final class ComponentId implements Comparable<ComponentId> { @Override public boolean equals(Object o) { if (o == this) return true; - if ( ! (o instanceof ComponentId)) return false; + if ( ! (o instanceof ComponentId c)) return false; - ComponentId c = (ComponentId) o; if (isAnonymous() || c.isAnonymous()) // TODO: Stop doing this return false; @@ -221,7 +212,7 @@ public final class ComponentId implements Comparable<ComponentId> { return new ComponentId(splitter.name, Version.fromString(splitter.version), splitter.namespace, true); } - private final class VersionHandler implements Spec.VersionHandler<Version> { + private static final class VersionHandler implements Spec.VersionHandler<Version> { @Override public Version emptyVersion() { diff --git a/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java b/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java index d7efca3b723..21bb193ef93 100644 --- a/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java +++ b/config-application-package/src/main/java/com/yahoo/config/application/OverrideProcessor.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.text.XML; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -23,7 +24,7 @@ import java.util.logging.Logger; import java.util.stream.Collectors; /** - * Handles overrides in a XML document according to the rules defined for multi environment application packages. + * Handles overrides in a XML document according to the rules defined for multi-environment application packages. * * Rules: * @@ -41,16 +42,19 @@ class OverrideProcessor implements PreProcessor { private final InstanceName instance; private final Environment environment; private final RegionName region; + private final Tags tags; private static final String ID_ATTRIBUTE = "id"; private static final String INSTANCE_ATTRIBUTE = "instance"; private static final String ENVIRONMENT_ATTRIBUTE = "environment"; private static final String REGION_ATTRIBUTE = "region"; + private static final String TAGS_ATTRIBUTE = "tags"; - public OverrideProcessor(InstanceName instance, Environment environment, RegionName region) { + public OverrideProcessor(InstanceName instance, Environment environment, RegionName region, Tags tags) { this.instance = instance; this.environment = environment; this.region = region; + this.tags = tags; } public Document process(Document input) throws TransformerException { @@ -80,6 +84,7 @@ class OverrideProcessor implements PreProcessor { child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, INSTANCE_ATTRIBUTE); child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, ENVIRONMENT_ATTRIBUTE); child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, REGION_ATTRIBUTE); + child.removeAttributeNS(XmlPreProcessor.deployNamespaceUri, TAGS_ATTRIBUTE); } } @@ -87,13 +92,16 @@ class OverrideProcessor implements PreProcessor { Set<InstanceName> instances = context.instances; Set<Environment> environments = context.environments; Set<RegionName> regions = context.regions; + Tags tags = context.tags; if (instances.isEmpty()) instances = getInstances(parent); if (environments.isEmpty()) environments = getEnvironments(parent); if (regions.isEmpty()) regions = getRegions(parent); - return Context.create(instances, environments, regions); + if (tags.isEmpty()) + tags = getTags(parent); + return Context.create(instances, environments, regions, tags); } /** @@ -128,17 +136,21 @@ class OverrideProcessor implements PreProcessor { throw new IllegalArgumentException("Regions in child (" + regions + ") are not a subset of those of the parent (" + context.regions + ") at " + child); } + + Tags tags = getTags(child); + if ( ! tags.isEmpty() && ! context.tags.isEmpty() && ! context.tags.containsAll(tags)) { + throw new IllegalArgumentException("Tags in child (" + environments + + ") are not a subset of those of the parent (" + context.tags + ") at " + child); + } } } - /** - * Prune elements that are not matching our environment and region - */ + /** Prune elements that are not matching our environment and region. */ private void pruneNonMatching(Element parent, List<Element> children) { Iterator<Element> elemIt = children.iterator(); while (elemIt.hasNext()) { Element child = elemIt.next(); - if ( ! matches(getInstances(child), getEnvironments(child), getRegions(child))) { + if ( ! matches(getInstances(child), getEnvironments(child), getRegions(child), getTags(child))) { parent.removeChild(child); elemIt.remove(); } @@ -147,7 +159,8 @@ class OverrideProcessor implements PreProcessor { private boolean matches(Set<InstanceName> elementInstances, Set<Environment> elementEnvironments, - Set<RegionName> elementRegions) { + Set<RegionName> elementRegions, + Tags elementTags) { if ( ! elementInstances.isEmpty()) { // match instance if ( ! elementInstances.contains(instance)) return false; } @@ -164,12 +177,14 @@ class OverrideProcessor implements PreProcessor { if ( ! environment.isMultiRegion() && elementEnvironments.isEmpty() ) return false; } + if ( ! elementTags.isEmpty()) { // match tags + if ( ! elementTags.intersects(tags)) return false; + } + return true; } - /** - * Find the most specific element and remove all others. - */ + /** Find the most specific element and remove all others. */ private void retainMostSpecific(Element parent, List<Element> children, Context context) { // Keep track of elements with highest number of matches (might be more than one element with same tag, need a list) List<Element> bestMatches = new ArrayList<>(); @@ -205,12 +220,15 @@ class OverrideProcessor implements PreProcessor { Set<InstanceName> elementInstances = hasInstance(child) ? getInstances(child) : context.instances; Set<Environment> elementEnvironments = hasEnvironment(child) ? getEnvironments(child) : context.environments; Set<RegionName> elementRegions = hasRegion(child) ? getRegions(child) : context.regions; + Tags elementTags = hasTag(child) ? getTags(child) : context.tags; if ( ! elementInstances.isEmpty() && elementInstances.contains(instance)) currentMatch++; if ( ! elementEnvironments.isEmpty() && elementEnvironments.contains(environment)) currentMatch++; if ( ! elementRegions.isEmpty() && elementRegions.contains(region)) currentMatch++; + if ( elementTags.intersects(tags)) + currentMatch++; return currentMatch; } @@ -233,16 +251,14 @@ class OverrideProcessor implements PreProcessor { return false; } - /** - * Retains all elements where at least one element is overridden. Removes non-overridden elements from map. - */ + /** Retains all elements where at least one element is overridden. Removes non-overridden elements from map. */ private void retainOverriddenElements(Map<String, List<Element>> elementsByTagName) { Iterator<Map.Entry<String, List<Element>>> it = elementsByTagName.entrySet().iterator(); while (it.hasNext()) { List<Element> elements = it.next().getValue(); boolean hasOverrides = false; for (Element element : elements) { - if (hasEnvironment(element) || hasRegion(element)) { + if (hasInstance(element) || hasEnvironment(element) || hasRegion(element) || hasTag(element)) { hasOverrides = true; } } @@ -264,24 +280,34 @@ class OverrideProcessor implements PreProcessor { return element.hasAttributeNS(XmlPreProcessor.deployNamespaceUri, ENVIRONMENT_ATTRIBUTE); } + private boolean hasTag(Element element) { + return element.hasAttributeNS(XmlPreProcessor.deployNamespaceUri, TAGS_ATTRIBUTE); + } + private Set<InstanceName> getInstances(Element element) { String instance = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, INSTANCE_ATTRIBUTE); - if (instance == null || instance.isEmpty()) return Collections.emptySet(); + if (instance == null || instance.isEmpty()) return Set.of(); return Arrays.stream(instance.split(" ")).map(InstanceName::from).collect(Collectors.toSet()); } private Set<Environment> getEnvironments(Element element) { String env = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, ENVIRONMENT_ATTRIBUTE); - if (env == null || env.isEmpty()) return Collections.emptySet(); + if (env == null || env.isEmpty()) return Set.of(); return Arrays.stream(env.split(" ")).map(Environment::from).collect(Collectors.toSet()); } private Set<RegionName> getRegions(Element element) { String reg = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, REGION_ATTRIBUTE); - if (reg == null || reg.isEmpty()) return Collections.emptySet(); + if (reg == null || reg.isEmpty()) return Set.of(); return Arrays.stream(reg.split(" ")).map(RegionName::from).collect(Collectors.toSet()); } + private Tags getTags(Element element) { + String env = element.getAttributeNS(XmlPreProcessor.deployNamespaceUri, TAGS_ATTRIBUTE); + if (env == null || env.isEmpty()) return Tags.empty(); + return Tags.fromString(env); + } + private Map<String, List<Element>> elementsByTagNameAndId(List<Element> children) { Map<String, List<Element>> elementsByTagName = new LinkedHashMap<>(); // Index by tag name @@ -336,21 +362,27 @@ class OverrideProcessor implements PreProcessor { final Set<InstanceName> instances; final Set<Environment> environments; final Set<RegionName> regions; + final Tags tags; - private Context(Set<InstanceName> instances, Set<Environment> environments, Set<RegionName> regions) { + private Context(Set<InstanceName> instances, + Set<Environment> environments, + Set<RegionName> regions, + Tags tags) { this.instances = Set.copyOf(instances); this.environments = Set.copyOf(environments); this.regions = Set.copyOf(regions); + this.tags = tags; } static Context empty() { - return new Context(Set.of(), Set.of(), Set.of()); + return new Context(Set.of(), Set.of(), Set.of(), Tags.empty()); } public static Context create(Set<InstanceName> instances, Set<Environment> environments, - Set<RegionName> regions) { - return new Context(instances, environments, regions); + Set<RegionName> regions, + Tags tags) { + return new Context(instances, environments, regions, tags); } } diff --git a/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java b/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java index ba68894c9f9..42333ea7662 100644 --- a/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java +++ b/config-application-package/src/main/java/com/yahoo/config/application/XmlPreProcessor.java @@ -5,6 +5,7 @@ import com.yahoo.config.application.FileSystemWrapper.FileWrapper; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.text.XML; import org.w3c.dom.Document; import org.xml.sax.InputSource; @@ -19,6 +20,7 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * A preprocessor for services.xml files that handles deploy:environment, deploy:region, preprocess:properties, preprocess:include @@ -38,22 +40,53 @@ public class XmlPreProcessor { private final InstanceName instance; private final Environment environment; private final RegionName region; + private final Tags tags; private final List<PreProcessor> chain; - public XmlPreProcessor(File applicationDir, File xmlInput, InstanceName instance, Environment environment, RegionName region) throws IOException { - this(applicationDir, new FileReader(xmlInput), instance, environment, region); + // TODO: Remove after November 2022 + public XmlPreProcessor(File applicationDir, + File xmlInput, + InstanceName instance, + Environment environment, + RegionName region) throws IOException { + this(applicationDir, new FileReader(xmlInput), instance, environment, region, Tags.empty()); } - public XmlPreProcessor(File applicationDir, Reader xmlInput, InstanceName instance, Environment environment, RegionName region) { - this(FileSystemWrapper.getDefault(applicationDir.toPath()).wrap(applicationDir.toPath()), xmlInput, instance, environment, region); + public XmlPreProcessor(File applicationDir, + File xmlInput, + InstanceName instance, + Environment environment, + RegionName region, + Tags tags) throws IOException { + this(applicationDir, new FileReader(xmlInput), instance, environment, region, tags); } - public XmlPreProcessor(FileWrapper applicationDir, Reader xmlInput, InstanceName instance, Environment environment, RegionName region) { + public XmlPreProcessor(File applicationDir, + Reader xmlInput, + InstanceName instance, + Environment environment, + RegionName region, + Tags tags) { + this(FileSystemWrapper.getDefault(applicationDir.toPath()).wrap(applicationDir.toPath()), + xmlInput, + instance, + environment, + region, + tags); + } + + public XmlPreProcessor(FileWrapper applicationDir, + Reader xmlInput, + InstanceName instance, + Environment environment, + RegionName region, + Tags tags) { this.applicationDir = applicationDir; this.xmlInput = xmlInput; this.instance = instance; this.environment = environment; this.region = region; + this.tags = tags; this.chain = setupChain(); } @@ -73,7 +106,7 @@ public class XmlPreProcessor { private List<PreProcessor> setupChain() { List<PreProcessor> chain = new ArrayList<>(); chain.add(new IncludeProcessor(applicationDir)); - chain.add(new OverrideProcessor(instance, environment, region)); + chain.add(new OverrideProcessor(instance, environment, region, tags)); chain.add(new PropertiesProcessor()); chain.add(new ValidationProcessor()); // must be last in chain return chain; diff --git a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java index 279af646a8c..c3e9b99f562 100644 --- a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java +++ b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/DeployData.java @@ -2,6 +2,9 @@ package com.yahoo.config.model.application.provider; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; + +import java.util.Set; /** * Data generated or computed during deployment @@ -12,6 +15,8 @@ public class DeployData { private final ApplicationId applicationId; + private final Tags tags; + /** The absolute path to the directory holding the application */ private final String deployedFromDir; @@ -25,25 +30,16 @@ public class DeployData { private final long generation; private final long currentlyActiveGeneration; - // TODO: Remove when oldest version in use is 8.13 - public DeployData(String ignored, - String deployedFromDir, - ApplicationId applicationId, - Long deployTimestamp, - boolean internalRedeploy, - Long generation, - long currentlyActiveGeneration) { - this(deployedFromDir, applicationId, deployTimestamp, internalRedeploy, generation, currentlyActiveGeneration); - } - public DeployData(String deployedFromDir, ApplicationId applicationId, + Tags tags, Long deployTimestamp, boolean internalRedeploy, Long generation, long currentlyActiveGeneration) { this.deployedFromDir = deployedFromDir; this.applicationId = applicationId; + this.tags = tags; this.deployTimestamp = deployTimestamp; this.internalRedeploy = internalRedeploy; this.generation = generation; @@ -62,4 +58,6 @@ public class DeployData { public ApplicationId getApplicationId() { return applicationId; } + public Tags getTags() { return tags; } + } diff --git a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java index 7b483d0603c..e61ea01a99a 100644 --- a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java +++ b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java @@ -17,6 +17,7 @@ import com.yahoo.config.model.application.AbstractApplicationPackage; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.io.HexDump; @@ -138,6 +139,7 @@ public class FilesApplicationPackage extends AbstractApplicationPackage { deployData.getDeployTimestamp(), deployData.isInternalRedeploy(), deployData.getApplicationId(), + deployData.getTags(), computeCheckSum(appDir), deployData.getGeneration(), deployData.getCurrentlyActiveGeneration()); @@ -484,6 +486,7 @@ public class FilesApplicationPackage extends AbstractApplicationPackage { ApplicationId.from(TenantName.defaultName(), ApplicationName.from(originalAppDir), InstanceName.defaultName()), + Tags.empty(), "", 0L, 0L); @@ -583,7 +586,8 @@ public class FilesApplicationPackage extends AbstractApplicationPackage { inputXml, metaData.getApplicationId().instance(), zone.environment(), - zone.region()) + zone.region(), + metaData.getTags()) .run(); try (FileOutputStream outputStream = new FileOutputStream(destination)) { diff --git a/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTagsTest.java b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTagsTest.java new file mode 100644 index 00000000000..8d7431d33b6 --- /dev/null +++ b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTagsTest.java @@ -0,0 +1,124 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.application; + +import com.yahoo.config.provision.Environment; +import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; +import org.custommonkey.xmlunit.XMLUnit; +import org.junit.Test; +import org.w3c.dom.Document; + +import javax.xml.transform.TransformerException; +import java.io.StringReader; + +/** + * @author bratseth + */ +public class HostedOverrideProcessorTagsTest { + + static { + XMLUnit.setIgnoreWhitespace(true); + } + + private static final String input = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='5' deploy:tags='a' deploy:environment='perf'/>" + + " <nodes count='10' deploy:tags='a b'/>" + + " <nodes count='20' deploy:tags='c'/>" + + " <search deploy:tags='b'/>" + + " <document-api deploy:tags='d'/>" + + " </container>" + + "</services>"; + + @Test + public void testParsingTagAPerf() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='5' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.perf, + RegionName.defaultName(), + Tags.fromString("a"), + expected); + } + + @Test + public void testParsingTagAProd() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='10' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("a"), + expected); + } + + @Test + public void testParsingTagB() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='10' required='true'/>" + + " <search/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("b"), + expected); + } + + @Test + public void testParsingTagC() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='20' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("c"), + expected); + } + + @Test + public void testParsingTagCAndD() throws TransformerException { + String expected = + "<?xml version='1.0' encoding='UTF-8' standalone='no'?>" + + "<services xmlns:deploy='vespa' xmlns:preprocess='?' version='1.0'>" + + " <container id='foo' version='1.0'>" + + " <nodes count='20' required='true'/>" + + " <document-api/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.fromString("c d"), + expected); + } + + private void assertOverride(InstanceName instance, Environment environment, RegionName region, Tags tags, String expected) throws TransformerException { + Document inputDoc = Xml.getDocument(new StringReader(input)); + Document newDoc = new OverrideProcessor(instance, environment, region, tags).process(inputDoc); + TestBase.assertDocument(expected, newDoc); + } + +} diff --git a/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java index 1a4dab01930..451c7a3c217 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/HostedOverrideProcessorTest.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Test; import org.w3c.dom.Document; @@ -14,6 +15,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.transform.TransformerException; import java.io.IOException; import java.io.StringReader; +import java.util.List; /** * @author bratseth @@ -48,7 +50,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1'/>" + " </container>" + "</services>"; - assertOverride(Environment.test, RegionName.defaultName(), expected); + assertOverride(InstanceName.defaultName(), + Environment.test, + RegionName.defaultName(), + Tags.empty(), + expected); } @Test @@ -60,7 +66,27 @@ public class HostedOverrideProcessorTest { " <nodes count='4' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("prod"), RegionName.from("us-west"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty(), + expected); + } + + @Test + public void testParsingSpecificTag() throws TransformerException { + String expected = + "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + + "<services xmlns:deploy=\"vespa\" xmlns:preprocess=\"?\" version=\"1.0\">" + + " <container id=\"foo\" version=\"1.0\">" + + " <nodes count='4' required='true'/>" + + " </container>" + + "</services>"; + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty(), + expected); } @Test @@ -72,7 +98,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1' required='true'/>" + " </container>" + "</services>"; - assertOverride(InstanceName.from("myinstance"), Environment.from("prod"), RegionName.from("us-west"), expected); + assertOverride(InstanceName.from("myinstance"), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty(), + expected); } @Test @@ -84,7 +114,11 @@ public class HostedOverrideProcessorTest { " <nodes count='5' flavor='v-8-8-100' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("prod"), RegionName.from("us-east-3"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.from("us-east-3"), + Tags.empty(), + expected); } @Test @@ -96,7 +130,11 @@ public class HostedOverrideProcessorTest { " <nodes count='3' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("perf"), RegionName.from("us-east-3"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("perf"), + RegionName.from("us-east-3"), + Tags.empty(), + expected); } @Test @@ -108,7 +146,11 @@ public class HostedOverrideProcessorTest { " <nodes count='3' flavor='v-4-8-100' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.valueOf("prod"), RegionName.from("unknown"), expected); + assertOverride(InstanceName.defaultName(), + Environment.valueOf("prod"), + RegionName.from("unknown"), + Tags.empty(), + expected); } @Test @@ -120,7 +162,11 @@ public class HostedOverrideProcessorTest { " <nodes count='3' flavor='v-4-8-100' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("prod"), RegionName.defaultName(), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("prod"), + RegionName.defaultName(), + Tags.empty(), + expected); } @Test @@ -132,7 +178,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("dev"), RegionName.defaultName(), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("dev"), + RegionName.defaultName(), + Tags.empty(), + expected); } @Test @@ -144,7 +194,11 @@ public class HostedOverrideProcessorTest { " <nodes count='1'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("test"), RegionName.from("us-west"), expected); + assertOverride(InstanceName.defaultName(), + Environment.from("test"), + RegionName.from("us-west"), + Tags.empty(), + expected); } @Test @@ -156,16 +210,16 @@ public class HostedOverrideProcessorTest { " <nodes count='2' required='true'/>" + " </container>" + "</services>"; - assertOverride(Environment.from("staging"), RegionName.from("us-west"), expected); - } - - private void assertOverride(Environment environment, RegionName region, String expected) throws TransformerException { - assertOverride(InstanceName.from("default"), environment, region, expected); + assertOverride(InstanceName.defaultName(), + Environment.from("staging"), + RegionName.from("us-west"), + Tags.empty(), + expected); } - private void assertOverride(InstanceName instance, Environment environment, RegionName region, String expected) throws TransformerException { + private void assertOverride(InstanceName instance, Environment environment, RegionName region, Tags tags, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(input)); - Document newDoc = new OverrideProcessor(instance, environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(instance, environment, region, tags).process(inputDoc); TestBase.assertDocument(expected, newDoc); } diff --git a/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java index 44bcb12957a..debde6c1438 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/MultiOverrideProcessorTest.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Test; import org.w3c.dom.Document; @@ -124,13 +125,13 @@ public class MultiOverrideProcessorTest { private void assertOverride(Environment environment, RegionName region, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(input)); - Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region, Tags.empty()).process(inputDoc); TestBase.assertDocument(expected, newDoc); } private void assertOverrideWithIds(Environment environment, RegionName region, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(inputWithIds)); - Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region, Tags.empty()).process(inputDoc); TestBase.assertDocument(expected, newDoc); } diff --git a/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java index e9ee7c97876..150999390d8 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/OverrideProcessorTest.java @@ -4,6 +4,7 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.custommonkey.xmlunit.XMLUnit; import org.junit.Test; import org.w3c.dom.Document; @@ -332,7 +333,10 @@ public class OverrideProcessorTest { " </admin>" + "</services>"; Document inputDoc = Xml.getDocument(new StringReader(in)); - new OverrideProcessor(InstanceName.from("default"), Environment.from("prod"), RegionName.from("us-west")).process(inputDoc); + new OverrideProcessor(InstanceName.from("default"), + Environment.from("prod"), + RegionName.from("us-west"), + Tags.empty()).process(inputDoc); } @Test(expected = IllegalArgumentException.class) @@ -344,7 +348,10 @@ public class OverrideProcessorTest { " </admin>" + "</services>"; Document inputDoc = Xml.getDocument(new StringReader(in)); - new OverrideProcessor(InstanceName.from("default"), Environment.defaultEnvironment(), RegionName.from("us-west")).process(inputDoc); + new OverrideProcessor(InstanceName.from("default"), + Environment.defaultEnvironment(), + RegionName.from("us-west"), + Tags.empty()).process(inputDoc); } @Test @@ -399,7 +406,7 @@ public class OverrideProcessorTest { private void assertOverride(String input, Environment environment, RegionName region, String expected) throws TransformerException { Document inputDoc = Xml.getDocument(new StringReader(input)); - Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region).process(inputDoc); + Document newDoc = new OverrideProcessor(InstanceName.from("default"), environment, region, Tags.empty()).process(inputDoc); TestBase.assertDocument(expected, newDoc); } diff --git a/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java b/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java index 92c2c2a820f..0da94b69e58 100644 --- a/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java +++ b/config-application-package/src/test/java/com/yahoo/config/application/XmlPreprocessorTest.java @@ -4,11 +4,13 @@ package com.yahoo.config.application; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import org.junit.Test; import org.w3c.dom.Document; import java.io.File; import java.io.StringReader; +import java.util.Set; /** * @author hmusum @@ -44,7 +46,13 @@ public class XmlPreprocessorTest { " </nodes>\n" + " </container>\n" + "</services>"; - TestBase.assertDocument(expectedDev, new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.dev, RegionName.defaultName()).run()); + TestBase.assertDocument(expectedDev, + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.dev, + RegionName.defaultName(), + Tags.empty()).run()); String expectedStaging = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + @@ -70,7 +78,13 @@ public class XmlPreprocessorTest { " </nodes>\n" + " </container>\n" + "</services>"; - TestBase.assertDocument(expectedStaging, new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.staging, RegionName.defaultName()).run()); + TestBase.assertDocument(expectedStaging, + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.staging, + RegionName.defaultName(), + Tags.empty()).run()); String expectedUsWest = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + @@ -104,7 +118,13 @@ public class XmlPreprocessorTest { " </nodes>\n" + " </container>\n" + "</services>"; - TestBase.assertDocument(expectedUsWest, new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.prod, RegionName.from("us-west")).run()); + TestBase.assertDocument(expectedUsWest, + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.prod, + RegionName.from("us-west"), + Tags.empty()).run()); String expectedUsEastAndCentral = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" + @@ -138,9 +158,19 @@ public class XmlPreprocessorTest { " </container>\n" + "</services>"; TestBase.assertDocument(expectedUsEastAndCentral, - new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.prod, RegionName.from("us-east")).run()); + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.prod, + RegionName.from("us-east"), + Tags.empty()).run()); TestBase.assertDocument(expectedUsEastAndCentral, - new XmlPreProcessor(appDir, services, InstanceName.defaultName(), Environment.prod, RegionName.from("us-central")).run()); + new XmlPreProcessor(appDir, + services, + InstanceName.defaultName(), + Environment.prod, + RegionName.from("us-central"), + Tags.empty()).run()); } @Test @@ -184,7 +214,12 @@ public class XmlPreprocessorTest { " <adminserver hostalias=\"node0\"/>" + " </admin>" + "</services>"; - Document docDev = (new XmlPreProcessor(appDir, new StringReader(input), InstanceName.defaultName(), Environment.prod, RegionName.defaultName()).run()); + Document docDev = (new XmlPreProcessor(appDir, + new StringReader(input), + InstanceName.defaultName(), + Environment.prod, + RegionName.defaultName(), + Tags.empty()).run()); TestBase.assertDocument(expectedProd, docDev); } diff --git a/config-model-api/abi-spec.json b/config-model-api/abi-spec.json index 7e17cd0f600..4f66ded727b 100644 --- a/config-model-api/abi-spec.json +++ b/config-model-api/abi-spec.json @@ -71,11 +71,13 @@ "public" ], "methods": [ + "public void <init>(java.lang.String, java.lang.Long, boolean, com.yahoo.config.provision.ApplicationId, com.yahoo.config.provision.Tags, java.lang.String, java.lang.Long, long)", "public void <init>(java.lang.String, java.lang.Long, boolean, com.yahoo.config.provision.ApplicationId, java.lang.String, java.lang.Long, long)", "public void <init>(java.lang.String, java.lang.String, java.lang.Long, boolean, com.yahoo.config.provision.ApplicationId, java.lang.String, java.lang.Long, long)", "public java.lang.String getDeployedByUser()", "public java.lang.String getDeployPath()", "public com.yahoo.config.provision.ApplicationId getApplicationId()", + "public com.yahoo.config.provision.Tags getTags()", "public java.lang.Long getDeployTimestamp()", "public java.lang.Long getGeneration()", "public boolean isInternalRedeploy()", @@ -194,8 +196,9 @@ "public" ], "methods": [ - "public void <init>(com.yahoo.config.provision.InstanceName, java.util.List, com.yahoo.config.application.api.DeploymentSpec$UpgradePolicy, com.yahoo.config.application.api.DeploymentSpec$RevisionTarget, com.yahoo.config.application.api.DeploymentSpec$RevisionChange, com.yahoo.config.application.api.DeploymentSpec$UpgradeRollout, int, int, int, java.util.List, java.util.Optional, java.util.Optional, java.util.Optional, com.yahoo.config.application.api.Notifications, java.util.List, java.time.Instant)", + "public void <init>(com.yahoo.config.provision.InstanceName, com.yahoo.config.provision.Tags, java.util.List, com.yahoo.config.application.api.DeploymentSpec$UpgradePolicy, com.yahoo.config.application.api.DeploymentSpec$RevisionTarget, com.yahoo.config.application.api.DeploymentSpec$RevisionChange, com.yahoo.config.application.api.DeploymentSpec$UpgradeRollout, int, int, int, java.util.List, java.util.Optional, java.util.Optional, java.util.Optional, com.yahoo.config.application.api.Notifications, java.util.List, java.time.Instant)", "public com.yahoo.config.provision.InstanceName name()", + "public com.yahoo.config.provision.Tags tags()", "public com.yahoo.config.application.api.DeploymentSpec$UpgradePolicy upgradePolicy()", "public com.yahoo.config.application.api.DeploymentSpec$RevisionTarget revisionTarget()", "public com.yahoo.config.application.api.DeploymentSpec$RevisionChange revisionChange()", diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java b/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java index a23afd994f2..c830c2baa1f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/ApplicationMetaData.java @@ -2,6 +2,7 @@ package com.yahoo.config.application.api; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.slime.Cursor; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -9,6 +10,9 @@ import com.yahoo.slime.SlimeUtils; import com.yahoo.text.Utf8; import java.io.IOException; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; /** * Metadata about an application package. @@ -21,32 +25,40 @@ public class ApplicationMetaData { private final long deployTimestamp; private final boolean internalRedeploy; private final ApplicationId applicationId; + private final Tags tags; private final String checksum; private final long generation; private final long previousActiveGeneration; public ApplicationMetaData(String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, - ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { - this("unknown", deployedFromDir, deployTimestamp, internalRedeploy, applicationId, checksum, generation, previousActiveGeneration); - } - - @Deprecated - // TODO: Remove in Vespa 9 - public ApplicationMetaData(String ignored, String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, - ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { + ApplicationId applicationId, Tags tags, + String checksum, Long generation, long previousActiveGeneration) { this.deployedFromDir = deployedFromDir; this.deployTimestamp = deployTimestamp; this.internalRedeploy = internalRedeploy; this.applicationId = applicationId; + this.tags = tags; this.checksum = checksum; this.generation = generation; this.previousActiveGeneration = previousActiveGeneration; } + @Deprecated // TODO: Remove on Vespa 9 + public ApplicationMetaData(String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, + ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { + this(deployedFromDir, deployTimestamp, internalRedeploy, applicationId, Tags.empty(), checksum, generation, previousActiveGeneration); + } + + @Deprecated // TODO: Remove on Vespa 9 + public ApplicationMetaData(String ignored, String deployedFromDir, Long deployTimestamp, boolean internalRedeploy, + ApplicationId applicationId, String checksum, Long generation, long previousActiveGeneration) { + this(deployedFromDir, deployTimestamp, internalRedeploy, applicationId, Tags.empty(), checksum, generation, previousActiveGeneration); + } + /** * Gets the user who deployed the application. * - * @return user name for the user who ran "deploy-application" + * @return username of the user who ran "deploy-application" */ @Deprecated // TODO: Remove in Vespa 9 public String getDeployedByUser() { return "unknown"; } @@ -61,6 +73,8 @@ public class ApplicationMetaData { public ApplicationId getApplicationId() { return applicationId; } + public Tags getTags() { return tags; } + /** * Gets the time the application was deployed. * Will return null if a problem occurred while getting metadata. @@ -103,6 +117,7 @@ public class ApplicationMetaData { deploy.field("timestamp").asLong(), booleanField("internalRedeploy", false, deploy), ApplicationId.fromSerializedForm(app.field("id").asString()), + Tags.fromString(deploy.field("tags").asString()), app.field("checksum").asString(), app.field("generation").asLong(), app.field("previousActiveGeneration").asLong()); @@ -118,6 +133,7 @@ public class ApplicationMetaData { deploy.setString("from", deployedFromDir); deploy.setLong("timestamp", deployTimestamp); deploy.setBool("internalRedeploy", internalRedeploy); + deploy.setString("tags", tags.asString()); Cursor app = meta.setObject("application"); app.setString("id", applicationId.serializedForm()); app.setString("checksum", checksum); diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java index cd20b5b8910..fdde4c38fb8 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java @@ -6,6 +6,7 @@ import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import java.time.Duration; import java.time.Instant; @@ -40,6 +41,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { /** The name of the instance this step deploys */ private final InstanceName name; + private final Tags tags; private final DeploymentSpec.UpgradePolicy upgradePolicy; private final DeploymentSpec.RevisionTarget revisionTarget; private final DeploymentSpec.RevisionChange revisionChange; @@ -55,6 +57,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { private final List<Endpoint> endpoints; public DeploymentInstanceSpec(InstanceName name, + Tags tags, List<DeploymentSpec.Step> steps, DeploymentSpec.UpgradePolicy upgradePolicy, DeploymentSpec.RevisionTarget revisionTarget, @@ -70,6 +73,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { Instant now) { super(steps); this.name = Objects.requireNonNull(name); + this.tags = Objects.requireNonNull(tags); this.upgradePolicy = Objects.requireNonNull(upgradePolicy); Objects.requireNonNull(revisionTarget); Objects.requireNonNull(revisionChange); @@ -94,6 +98,8 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { public InstanceName name() { return name; } + public Tags tags() { return tags; } + /** * Throws an IllegalArgumentException if any production deployment or test is declared multiple times, * or if any production test is declared not after its corresponding deployment. @@ -267,12 +273,13 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps { } int deployableHashCode() { - List<DeploymentSpec.DeclaredZone> zones = zones().stream().filter(zone -> zone.concerns(prod)).collect(toList()); - Object[] toHash = new Object[zones.size() + 3]; + List<DeploymentSpec.DeclaredZone> zones = zones().stream().filter(zone -> zone.concerns(prod)).toList(); + Object[] toHash = new Object[zones.size() + 4]; int i = 0; toHash[i++] = name; toHash[i++] = endpoints; toHash[i++] = globalServiceId; + toHash[i++] = tags; for (DeploymentSpec.DeclaredZone zone : zones) toHash[i++] = Objects.hash(zone, zone.athenzService()); diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java index b85150356e3..2df55ffce95 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java @@ -194,7 +194,7 @@ public class DeploymentSpec { /** Returns the instance names declared in this */ public List<InstanceName> instanceNames() { - return instances().stream().map(DeploymentInstanceSpec::name).collect(Collectors.toUnmodifiableList()); + return instances().stream().map(DeploymentInstanceSpec::name).toList(); } /** Returns the step descendants of this which are instances */ diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java index 83fba75325e..77894a8cf1f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java +++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java @@ -25,6 +25,7 @@ import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.io.IOUtils; import com.yahoo.text.XML; import org.w3c.dom.Element; @@ -55,6 +56,7 @@ public class DeploymentSpecXmlReader { private static final String deploymentTag = "deployment"; private static final String instanceTag = "instance"; + private static final String tagsTag = "tags"; private static final String testTag = "test"; private static final String stagingTag = "staging"; private static final String devTag = "dev"; @@ -155,48 +157,50 @@ public class DeploymentSpecXmlReader { * Reads the content of an (implicit or explicit) instance tag producing an instances step * * @param instanceNameString a comma-separated list of the names of the instances this is for - * @param instanceTag the element having the content of this instance + * @param instanceElement the element having the content of this instance * @param parentTag the parent of instanceTag (or the same, if this instance is implicitly defined, which means instanceTag is the root) * @return the instances specified, one for each instance name element */ private List<DeploymentInstanceSpec> readInstanceContent(String instanceNameString, - Element instanceTag, + Element instanceElement, Map<String, String> prodAttributes, Element parentTag) { if (instanceNameString.isBlank()) illegal("<instance> attribute 'id' must be specified, and not be blank"); // If this is an absolutely empty instance, or the implicit "default" instance but without content, ignore it - if (XML.getChildren(instanceTag).isEmpty() && (instanceTag.getAttributes().getLength() == 0 || instanceTag == parentTag)) + if (XML.getChildren(instanceElement).isEmpty() && (instanceElement.getAttributes().getLength() == 0 || instanceElement == parentTag)) return List.of(); if (validate) - validateTagOrder(instanceTag); + validateTagOrder(instanceElement); // Values where the parent may provide a default - DeploymentSpec.UpgradePolicy upgradePolicy = getWithFallback(instanceTag, parentTag, upgradeTag, "policy", this::readUpgradePolicy, UpgradePolicy.defaultPolicy); - DeploymentSpec.RevisionTarget revisionTarget = getWithFallback(instanceTag, parentTag, upgradeTag, "revision-target", this::readRevisionTarget, RevisionTarget.latest); - DeploymentSpec.RevisionChange revisionChange = getWithFallback(instanceTag, parentTag, upgradeTag, "revision-change", this::readRevisionChange, RevisionChange.whenFailing); - DeploymentSpec.UpgradeRollout upgradeRollout = getWithFallback(instanceTag, parentTag, upgradeTag, "rollout", this::readUpgradeRollout, UpgradeRollout.separate); - int minRisk = getWithFallback(instanceTag, parentTag, upgradeTag, "min-risk", Integer::parseInt, 0); - int maxRisk = getWithFallback(instanceTag, parentTag, upgradeTag, "max-risk", Integer::parseInt, 0); - int maxIdleHours = getWithFallback(instanceTag, parentTag, upgradeTag, "max-idle-hours", Integer::parseInt, 8); - List<DeploymentSpec.ChangeBlocker> changeBlockers = readChangeBlockers(instanceTag, parentTag); - Optional<AthenzService> athenzService = mostSpecificAttribute(instanceTag, athenzServiceAttribute).map(AthenzService::from); - Optional<CloudAccount> cloudAccount = mostSpecificAttribute(instanceTag, cloudAccountAttribute).map(CloudAccount::new); - Notifications notifications = readNotifications(instanceTag, parentTag); + DeploymentSpec.UpgradePolicy upgradePolicy = getWithFallback(instanceElement, parentTag, upgradeTag, "policy", this::readUpgradePolicy, UpgradePolicy.defaultPolicy); + DeploymentSpec.RevisionTarget revisionTarget = getWithFallback(instanceElement, parentTag, upgradeTag, "revision-target", this::readRevisionTarget, RevisionTarget.latest); + DeploymentSpec.RevisionChange revisionChange = getWithFallback(instanceElement, parentTag, upgradeTag, "revision-change", this::readRevisionChange, RevisionChange.whenFailing); + DeploymentSpec.UpgradeRollout upgradeRollout = getWithFallback(instanceElement, parentTag, upgradeTag, "rollout", this::readUpgradeRollout, UpgradeRollout.separate); + int minRisk = getWithFallback(instanceElement, parentTag, upgradeTag, "min-risk", Integer::parseInt, 0); + int maxRisk = getWithFallback(instanceElement, parentTag, upgradeTag, "max-risk", Integer::parseInt, 0); + int maxIdleHours = getWithFallback(instanceElement, parentTag, upgradeTag, "max-idle-hours", Integer::parseInt, 8); + List<DeploymentSpec.ChangeBlocker> changeBlockers = readChangeBlockers(instanceElement, parentTag); + Optional<AthenzService> athenzService = mostSpecificAttribute(instanceElement, athenzServiceAttribute).map(AthenzService::from); + Optional<CloudAccount> cloudAccount = mostSpecificAttribute(instanceElement, cloudAccountAttribute).map(CloudAccount::new); + Notifications notifications = readNotifications(instanceElement, parentTag); // Values where there is no default + Tags tags = XML.attribute(tagsTag, instanceElement).map(value -> Tags.fromString(value)).orElse(Tags.empty()); List<Step> steps = new ArrayList<>(); - for (Element instanceChild : XML.getChildren(instanceTag)) + for (Element instanceChild : XML.getChildren(instanceElement)) steps.addAll(readNonInstanceSteps(instanceChild, prodAttributes, instanceChild)); - List<Endpoint> endpoints = readEndpoints(instanceTag, Optional.of(instanceNameString), steps); + List<Endpoint> endpoints = readEndpoints(instanceElement, Optional.of(instanceNameString), steps); // Build and return instances with these values Instant now = clock.instant(); return Arrays.stream(instanceNameString.split(",")) .map(name -> name.trim()) .map(name -> new DeploymentInstanceSpec(InstanceName.from(name), + tags, steps, upgradePolicy, revisionTarget, diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java index ecb66910784..9ede27cb47f 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java @@ -73,14 +73,10 @@ public interface ModelContext { */ interface FeatureFlags { @ModelFeatureFlag(owners = {"baldersheim"}, comment = "Revisit in May or June 2021") default double defaultTermwiseLimit() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default boolean useThreePhaseUpdates() { return true; } @ModelFeatureFlag(owners = {"baldersheim"}, comment = "Select sequencer type use while feeding") default String feedSequencerType() { return "THROUGHPUT"; } @ModelFeatureFlag(owners = {"baldersheim"}) default String responseSequencerType() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"baldersheim"}) default String queryDispatchPolicy() { return "adaptive"; } @ModelFeatureFlag(owners = {"baldersheim"}) default int defaultNumResponseThreads() { return 2; } - @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean skipCommunicationManagerThread() { return true; } - @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean skipMbusRequestThread() { return true; } - @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean skipMbusReplyThread() { return true; } @ModelFeatureFlag(owners = {"baldersheim"}) default int mbusNetworkThreads() { return 1; } @ModelFeatureFlag(owners = {"baldersheim"}) default int mbusJavaRpcNumTargets() { return 1; } @ModelFeatureFlag(owners = {"baldersheim"}) default int mbusJavaEventsBeforeWakeup() { return 1; } @@ -91,11 +87,7 @@ public interface ModelContext { @ModelFeatureFlag(owners = {"baldersheim"}) default boolean useAsyncMessageHandlingOnSchedule() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"baldersheim"}) default double feedConcurrency() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"baldersheim"}) default double feedNiceness() { return 0.0; } - @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default int defaultPoolNumThreads() { return 1; } - @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default int availableProcessors() { return 1; } @ModelFeatureFlag(owners = {"baldersheim"}) default int maxUnCommittedMemory() { return 130000; } - @ModelFeatureFlag(owners = {"baldersheim"}) default int maxConcurrentMergesPerNode() { return 16; } - @ModelFeatureFlag(owners = {"baldersheim"}) default int maxMergeQueueSize() { return 100; } @ModelFeatureFlag(owners = {"baldersheim"}) default boolean sharedStringRepoNoReclaim() { return false; } @ModelFeatureFlag(owners = {"baldersheim"}) default boolean loadCodeAsHugePages() { return false; } @ModelFeatureFlag(owners = {"baldersheim"}) default boolean containerDumpHeapOnShutdownTimeout() { throw new UnsupportedOperationException("TODO specify default value"); } @@ -108,26 +100,37 @@ public interface ModelContext { @ModelFeatureFlag(owners = {"geirst", "vekterli"}) default double minNodeRatioPerGroup() { return 0.0; } @ModelFeatureFlag(owners = {"arnej"}) default boolean forwardIssuesAsErrors() { return true; } @ModelFeatureFlag(owners = {"arnej"}) default boolean ignoreThreadStackSizes() { return false; } - @ModelFeatureFlag(owners = {"vekterli", "geirst"}) default boolean unorderedMergeChaining() { return true; } @ModelFeatureFlag(owners = {"arnej"}) default boolean useV8GeoPositions() { return false; } @ModelFeatureFlag(owners = {"baldersheim", "geirst", "toregge"}) default int maxCompactBuffers() { return 1; } @ModelFeatureFlag(owners = {"arnej", "andreer"}) default List<String> ignoredHttpUserAgents() { return List.of(); } - @ModelFeatureFlag(owners = {"bjorncs"}, removeAfter="7.last") default boolean enableServerOcspStapling() { return true; } - @ModelFeatureFlag(owners = {"vekterli"}) default String mergeThrottlingPolicy() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"vekterli"}) default double persistenceThrottlingWsDecrementFactor() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"vekterli"}) default double persistenceThrottlingWsBackoff() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"vekterli"}) default int persistenceThrottlingWindowSize() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"vekterli"}) default double persistenceThrottlingWsResizeRate() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"vekterli"}) default boolean persistenceThrottlingOfMergeFeedOps() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"arnej"}, removeAfter="7.last") default boolean useQrserverServiceName() { return true; } - @ModelFeatureFlag(owners = {"arnej"}, removeAfter="7.last") default boolean avoidRenamingSummaryFeatures() { return false; } - @ModelFeatureFlag(owners = {"arnej"}, removeAfter="7.last") default boolean experimentalSdParsing() { return true; } // TODO: Remove after June 2022 - @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean enableBitVectors() { return true; } @ModelFeatureFlag(owners = {"hmusum"}) default Architecture adminClusterArchitecture() { return Architecture.getDefault(); } @ModelFeatureFlag(owners = {"tokle"}) default boolean enableProxyProtocolMixedMode() { return true; } @ModelFeatureFlag(owners = {"arnej"}) default String logFileCompressionAlgorithm(String defVal) { return defVal; } @ModelFeatureFlag(owners = {"vekterli"}) default boolean useTwoPhaseDocumentGc() { return false; } @ModelFeatureFlag(owners = {"tokle"}) default boolean useRestrictedDataPlaneBindings() { return false; } + @ModelFeatureFlag(owners = {"baldersheim", "vekterli"}, removeAfter="8.61") default boolean computeCoverageFromTargetActiveDocs() { return true; } + + //Below are all flags that must be kept until 7 is out of the door + @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default boolean useThreePhaseUpdates() { return true; } + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean skipCommunicationManagerThread() { return true; } + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean skipMbusRequestThread() { return true; } + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean skipMbusReplyThread() { return true; } + @ModelFeatureFlag(owners = {"arnej"}, removeAfter="7.last") default boolean useQrserverServiceName() { return true; } + @ModelFeatureFlag(owners = {"arnej"}, removeAfter="7.last") default boolean avoidRenamingSummaryFeatures() { return false; } + @ModelFeatureFlag(owners = {"arnej"}, removeAfter="7.last") default boolean experimentalSdParsing() { return true; } // TODO: Remove after June 2022 + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default boolean enableBitVectors() { return true; } + @ModelFeatureFlag(owners = {"bjorncs"}, removeAfter="7.last") default boolean enableServerOcspStapling() { return true; } + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default int defaultPoolNumThreads() { return 1; } + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default int availableProcessors() { return 1; } + @ModelFeatureFlag(owners = {"vekterli", "geirst"}, removeAfter="7.last") default boolean unorderedMergeChaining() { return true; } + @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default String mergeThrottlingPolicy() { return "STATIC"; } + @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default double persistenceThrottlingWsDecrementFactor() { return 1.2; } + @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default double persistenceThrottlingWsBackoff() { return 0.95; } + @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default int persistenceThrottlingWindowSize() { return -1; } + @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default double persistenceThrottlingWsResizeRate() { return 3; } + @ModelFeatureFlag(owners = {"vekterli"}, removeAfter="7.last") default boolean persistenceThrottlingOfMergeFeedOps() { return true; } + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default int maxConcurrentMergesPerNode() { return 16; } + @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter="7.last") default int maxMergeQueueSize() { return 100; } } /** Warning: As elsewhere in this package, do not make backwards incompatible changes that will break old config models! */ diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java index 87b0f709125..3870768ceb4 100644 --- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java +++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.test.ManualClock; import org.junit.Test; @@ -133,6 +134,29 @@ public class DeploymentSpecTest { } @Test + public void specWithTags() { + StringReader r = new StringReader( + "<deployment version='1.0'>" + + " <instance id='a' tags='tag1 tag2'>" + + " <prod>" + + " <region active='false'>us-east1</region>" + + " <region active='true'>us-west1</region>" + + " </prod>" + + " </instance>" + + " <instance id='b' tags='tag3'>" + + " <prod>" + + " <region active='false'>us-east1</region>" + + " <region active='true'>us-west1</region>" + + " </prod>" + + " </instance>" + + "</deployment>" + ); + DeploymentSpec spec = DeploymentSpec.fromXml(r); + assertEquals(Tags.fromString("tag1 tag2"), spec.requireInstance("a").tags()); + assertEquals(Tags.fromString("tag3"), spec.requireInstance("b").tags()); + } + + @Test public void maximalProductionSpec() { StringReader r = new StringReader( "<deployment version='1.0'>" + @@ -1402,7 +1426,7 @@ public class DeploymentSpecTest { </deployment>""").deployableHashCode(), DeploymentSpec.fromXml(""" <deployment> - <instance id='default'> + <instance id='default' tags=' '> <test /> <staging tester-flavor='2-8-50' /> <block-change days='mon' /> @@ -1423,7 +1447,8 @@ public class DeploymentSpecTest { <region>name</region> </prod> </instance> - <instance id='two' /> </parallel> + <instance id='two' /> + </parallel> </deployment>""").deployableHashCode(), DeploymentSpec.fromXml(""" <deployment> @@ -1459,6 +1484,16 @@ public class DeploymentSpecTest { assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(), DeploymentSpec.fromXml(""" <deployment> + <instance id='default' tags='tag1'> + <prod> + <region>name</region> + </prod> + </instance> + </deployment>""").deployableHashCode()); + + assertNotEquals(DeploymentSpec.fromXml(referenceSpec).deployableHashCode(), + DeploymentSpec.fromXml(""" + <deployment> <instance id='custom'> <prod> <region>name</region> diff --git a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java index b1b6870a004..eb628db6975 100644 --- a/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java +++ b/config-model/src/main/java/com/yahoo/config/model/deploy/TestProperties.java @@ -54,8 +54,6 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea private int maxActivationInhibitedOutOfSyncGroups = 0; private List<TenantSecretStore> tenantSecretStores = Collections.emptyList(); private String jvmOmitStackTraceInFastThrowOption; - private int maxConcurrentMergesPerNode = 16; - private int maxMergeQueueSize = 100; private boolean allowDisableMtls = true; private List<X509Certificate> operatorCertificates = Collections.emptyList(); private double resourceLimitDisk = 0.75; @@ -64,15 +62,8 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea private boolean containerDumpHeapOnShutdownTimeout = false; private double containerShutdownTimeout = 50.0; private int maxUnCommittedMemory = 123456; - private boolean unorderedMergeChaining = true; private List<String> zoneDnsSuffixes = List.of(); private int maxCompactBuffers = 1; - private String mergeThrottlingPolicy = "STATIC"; - private double persistenceThrottlingWsDecrementFactor = 1.2; - private double persistenceThrottlingWsBackoff = 0.95; - private int persistenceThrottlingWindowSize = -1; - private double persistenceThrottlingWsResizeRate = 3.0; - private boolean persistenceThrottlingOfMergeFeedOps = true; private boolean useV8GeoPositions = true; private List<String> environmentVariables = List.of(); private boolean loadCodeAsHugePages = false; @@ -117,23 +108,14 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea @Override public String jvmOmitStackTraceInFastThrowOption(ClusterSpec.Type type) { return jvmOmitStackTraceInFastThrowOption; } @Override public boolean allowDisableMtls() { return allowDisableMtls; } @Override public List<X509Certificate> operatorCertificates() { return operatorCertificates; } - @Override public int maxConcurrentMergesPerNode() { return maxConcurrentMergesPerNode; } - @Override public int maxMergeQueueSize() { return maxMergeQueueSize; } @Override public double resourceLimitDisk() { return resourceLimitDisk; } @Override public double resourceLimitMemory() { return resourceLimitMemory; } @Override public double minNodeRatioPerGroup() { return minNodeRatioPerGroup; } @Override public double containerShutdownTimeout() { return containerShutdownTimeout; } @Override public boolean containerDumpHeapOnShutdownTimeout() { return containerDumpHeapOnShutdownTimeout; } @Override public int maxUnCommittedMemory() { return maxUnCommittedMemory; } - @Override public boolean unorderedMergeChaining() { return unorderedMergeChaining; } @Override public List<String> zoneDnsSuffixes() { return zoneDnsSuffixes; } @Override public int maxCompactBuffers() { return maxCompactBuffers; } - @Override public String mergeThrottlingPolicy() { return mergeThrottlingPolicy; } - @Override public double persistenceThrottlingWsDecrementFactor() { return persistenceThrottlingWsDecrementFactor; } - @Override public double persistenceThrottlingWsBackoff() { return persistenceThrottlingWsBackoff; } - @Override public int persistenceThrottlingWindowSize() { return persistenceThrottlingWindowSize; } - @Override public double persistenceThrottlingWsResizeRate() { return persistenceThrottlingWsResizeRate; } - @Override public boolean persistenceThrottlingOfMergeFeedOps() { return persistenceThrottlingOfMergeFeedOps; } @Override public boolean useV8GeoPositions() { return useV8GeoPositions; } @Override public List<String> environmentVariables() { return environmentVariables; } @Override public Architecture adminClusterArchitecture() { return adminClusterNodeResourcesArchitecture; } @@ -214,15 +196,6 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea return this; } - public TestProperties setMaxConcurrentMergesPerNode(int maxConcurrentMergesPerNode) { - this.maxConcurrentMergesPerNode = maxConcurrentMergesPerNode; - return this; - } - public TestProperties setMaxMergeQueueSize(int maxMergeQueueSize) { - this.maxMergeQueueSize = maxMergeQueueSize; - return this; - } - public TestProperties setDefaultTermwiseLimit(double limit) { defaultTermwiseLimit = limit; return this; @@ -313,11 +286,6 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea return this; } - public TestProperties setUnorderedMergeChaining(boolean unordered) { - unorderedMergeChaining = unordered; - return this; - } - public TestProperties setZoneDnsSuffixes(List<String> zoneDnsSuffixes) { this.zoneDnsSuffixes = List.copyOf(zoneDnsSuffixes); return this; @@ -328,36 +296,6 @@ public class TestProperties implements ModelContext.Properties, ModelContext.Fea return this; } - public TestProperties setMergeThrottlingPolicy(String policy) { - this.mergeThrottlingPolicy = policy; - return this; - } - - public TestProperties setPersistenceThrottlingWsDecrementFactor(double factor) { - this.persistenceThrottlingWsDecrementFactor = factor; - return this; - } - - public TestProperties setPersistenceThrottlingWsBackoff(double backoff) { - this.persistenceThrottlingWsBackoff = backoff; - return this; - } - - public TestProperties setPersistenceThrottlingWindowSize(int windowSize) { - this.persistenceThrottlingWindowSize = windowSize; - return this; - } - - public TestProperties setPersistenceThrottlingWsResizeRate(double resizeRate) { - this.persistenceThrottlingWsResizeRate = resizeRate; - return this; - } - - public TestProperties setPersistenceThrottlingOfMergeFeedOps(boolean throttleOps) { - this.persistenceThrottlingOfMergeFeedOps = throttleOps; - return this; - } - public TestProperties setUseV8GeoPositions(boolean value) { this.useV8GeoPositions = value; return this; diff --git a/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java b/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java index 9ee279c68d3..2d98c8b35c0 100644 --- a/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java +++ b/config-model/src/main/java/com/yahoo/config/model/test/MockApplicationPackage.java @@ -10,6 +10,7 @@ import com.yahoo.config.application.api.UnparsedConfigDefinition; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.io.IOUtils; import com.yahoo.io.reader.NamedReader; @@ -85,6 +86,7 @@ public class MockApplicationPackage implements ApplicationPackage { ApplicationId.from(TenantName.defaultName(), ApplicationName.from(APPLICATION_NAME), InstanceName.defaultName()), + Tags.empty(), "checksum", APPLICATION_GENERATION, 0L); diff --git a/config-model/src/main/java/com/yahoo/schema/processing/PagedAttributeValidator.java b/config-model/src/main/java/com/yahoo/schema/processing/PagedAttributeValidator.java index 793505acd01..6f470cfdc56 100644 --- a/config-model/src/main/java/com/yahoo/schema/processing/PagedAttributeValidator.java +++ b/config-model/src/main/java/com/yahoo/schema/processing/PagedAttributeValidator.java @@ -41,19 +41,19 @@ public class PagedAttributeValidator extends Processor { private void validatePagedSetting(Field field, Attribute attribute) { if (!isSupportedType(attribute)) { - fail(schema, field, "The 'paged' attribute setting is not supported for non-dense tensor, predicate and reference types"); + fail(schema, field, "The 'paged' attribute setting is not supported for fast-rank tensor and predicate types"); } } private boolean isSupportedType(Attribute attribute) { var type = attribute.getType(); return (type != Attribute.Type.PREDICATE) && - (isSupportedTensorType(attribute.tensorType())); + (isSupportedTensorType(attribute.tensorType(), attribute.isFastRank())); } - private boolean isSupportedTensorType(Optional<TensorType> tensorType) { + private boolean isSupportedTensorType(Optional<TensorType> tensorType, boolean fastRank) { if (tensorType.isPresent()) { - return isDenseTensorType(tensorType.get()); + return !fastRank; } return true; } diff --git a/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java index e94518c988d..d1d22886642 100644 --- a/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java +++ b/config-model/src/main/java/com/yahoo/vespa/documentmodel/SummaryTransform.java @@ -37,33 +37,20 @@ public enum SummaryTransform { /** Returns the bolded version of this transform if possible, throws if not */ public SummaryTransform bold() { - switch (this) { - case NONE: - case BOLDED: - return BOLDED; - - case DYNAMICBOLDED: - case DYNAMICTEASER: - return DYNAMICBOLDED; - - default: - throw new IllegalArgumentException("Can not bold a '" + this + "' field."); - } + return switch (this) { + case NONE, BOLDED -> BOLDED; + case DYNAMICBOLDED, DYNAMICTEASER -> DYNAMICBOLDED; + default -> throw new IllegalArgumentException("Can not bold a '" + this + "' field."); + }; } /** Returns the unbolded version of this transform */ public SummaryTransform unbold() { - switch (this) { - case NONE: - case BOLDED: - return NONE; - - case DYNAMICBOLDED: - return DYNAMICTEASER; - - default: - return this; - } + return switch (this) { + case NONE, BOLDED -> NONE; + case DYNAMICBOLDED -> DYNAMICTEASER; + default -> this; + }; } /** Returns whether this value is bolded */ @@ -83,23 +70,16 @@ public enum SummaryTransform { /** Returns whether this transform always gets its value by accessing memory only */ public boolean isInMemory() { - switch (this) { - case ATTRIBUTE: - case DISTANCE: - case POSITIONS: - case GEOPOS: - case RANKFEATURES: - case SUMMARYFEATURES: - case ATTRIBUTECOMBINER: - case MATCHED_ATTRIBUTE_ELEMENTS_FILTER: - return true; - - default: - return false; - } + return switch (this) { + case ATTRIBUTE, DISTANCE, POSITIONS, GEOPOS, RANKFEATURES, SUMMARYFEATURES, ATTRIBUTECOMBINER, MATCHED_ATTRIBUTE_ELEMENTS_FILTER -> + true; + default -> false; + }; } + @Override public String toString() { return name; } + } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java index f01e7799e13..4d03944c4e9 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java @@ -202,6 +202,14 @@ public class VespaMetricSet { metrics.add(new Metric("jdisc.deactivated_containers.total.last")); metrics.add(new Metric("jdisc.deactivated_containers.with_retained_refs.last")); + metrics.add(new Metric("jdisc.singleton.is_active.last")); + metrics.add(new Metric("jdisc.singleton.activation.count.last")); + metrics.add(new Metric("jdisc.singleton.activation.failure.count.last")); + metrics.add(new Metric("jdisc.singleton.activation.millis.last")); + metrics.add(new Metric("jdisc.singleton.deactivation.count.last")); + metrics.add(new Metric("jdisc.singleton.deactivation.failure.count.last")); + metrics.add(new Metric("jdisc.singleton.deactivation.millis.last")); + metrics.add(new Metric("athenz-tenant-cert.expiry.seconds.last")); metrics.add(new Metric("container-iam-role.expiry.seconds")); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java index 4c74282c061..06453bffaaf 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/builder/xml/dom/DomAdminBuilderBase.java @@ -1,7 +1,6 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.builder.xml.dom; -import com.yahoo.config.application.api.DeployLogger; import com.yahoo.config.model.ConfigModelContext.ApplicationType; import com.yahoo.config.model.api.ConfigServerSpec; import com.yahoo.config.model.deploy.DeployState; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java index e316f826ad6..8907c21d39a 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ApplicationContainerCluster.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.model.container; import ai.vespa.metricsproxy.http.application.ApplicationMetricsHandler; +import com.yahoo.cloud.config.CuratorConfig; import com.yahoo.cloud.config.ZookeeperServerConfig; import com.yahoo.component.ComponentId; import com.yahoo.component.ComponentSpecification; @@ -305,6 +306,15 @@ public final class ApplicationContainerCluster extends ContainerCluster<Applicat } } + @Override + public void getConfig(CuratorConfig.Builder builder) { + if (getParent() instanceof ConfigserverCluster) return; // Produces its own config + super.getConfig(builder); + + // 12s is 2x the current ZookeeperServerConfig.tickTime() of 6s, and the default minimum the server will accept. + builder.zookeeperSessionTimeoutSeconds(12); // TODO jonmv: make configurable + } + public Optional<String> getTlsClientAuthority() { return tlsClientAuthority; } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index 5e09dd4732d..307e0e17955 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -246,6 +246,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder<ContainerModel> { ", have " + nonRetiredNodes + " non-retired"); } cluster.addSimpleComponent("com.yahoo.vespa.curator.Curator", null, "zkfacade"); + cluster.addSimpleComponent("com.yahoo.vespa.curator.CuratorWrapper", null, "zkfacade"); // These need to be setup so that they will use the container's config id, since each container // have different config (id of zookeeper server) diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java index b1258247e2e..dae823bcc9f 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/DistributorCluster.java @@ -32,7 +32,6 @@ public class DistributorCluster extends AbstractConfigProducer<Distributor> impl private final GcOptions gc; private final boolean hasIndexedDocumentType; private final int maxActivationInhibitedOutOfSyncGroups; - private final boolean unorderedMergeChaining; private final boolean useTwoPhaseDocumentGc; public static class Builder extends VespaDomBuilder.DomConfigProducerBuilder<DistributorCluster> { @@ -95,20 +94,18 @@ public class DistributorCluster extends AbstractConfigProducer<Distributor> impl final GcOptions gc = parseGcOptions(documentsNode); final boolean hasIndexedDocumentType = clusterContainsIndexedDocumentType(documentsNode); int maxInhibitedGroups = deployState.getProperties().featureFlags().maxActivationInhibitedOutOfSyncGroups(); - boolean unorderedMergeChaining = deployState.getProperties().featureFlags().unorderedMergeChaining(); boolean useTwoPhaseDocumentGc = deployState.getProperties().featureFlags().useTwoPhaseDocumentGc(); return new DistributorCluster(parent, new BucketSplitting.Builder().build(new ModelElement(producerSpec)), gc, hasIndexedDocumentType, - maxInhibitedGroups, unorderedMergeChaining, useTwoPhaseDocumentGc); + maxInhibitedGroups, useTwoPhaseDocumentGc); } } private DistributorCluster(ContentCluster parent, BucketSplitting bucketSplitting, GcOptions gc, boolean hasIndexedDocumentType, int maxActivationInhibitedOutOfSyncGroups, - boolean unorderedMergeChaining, boolean useTwoPhaseDocumentGc) { super(parent, "distributor"); @@ -117,7 +114,6 @@ public class DistributorCluster extends AbstractConfigProducer<Distributor> impl this.gc = gc; this.hasIndexedDocumentType = hasIndexedDocumentType; this.maxActivationInhibitedOutOfSyncGroups = maxActivationInhibitedOutOfSyncGroups; - this.unorderedMergeChaining = unorderedMergeChaining; this.useTwoPhaseDocumentGc = useTwoPhaseDocumentGc; } @@ -131,7 +127,6 @@ public class DistributorCluster extends AbstractConfigProducer<Distributor> impl builder.enable_revert(parent.getPersistence().supportRevert()); builder.disable_bucket_activation(!hasIndexedDocumentType); builder.max_activation_inhibited_out_of_sync_groups(maxActivationInhibitedOutOfSyncGroups); - builder.use_unordered_merge_chaining(unorderedMergeChaining); builder.enable_two_phase_garbage_collection(useTwoPhaseDocumentGc); bucketSplitting.getConfig(builder); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java index ff905187969..092b94d72dc 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/FileStorProducer.java @@ -46,11 +46,6 @@ public class FileStorProducer implements StorFilestorConfig.Producer { private final ContentCluster cluster; private final int responseNumThreads; private final StorFilestorConfig.Response_sequencer_type.Enum responseSequencerType; - private final double persistenceThrottlingWsDecrementFactor; - private final double persistenceThrottlingWsBackoff; - private final int persistenceThrottlingWindowSize; - private final double persistenceThrottlingWsResizeRate; - private final boolean persistenceThrottlingOfMergeFeedOps; private final boolean useAsyncMessageHandlingOnSchedule; private static StorFilestorConfig.Response_sequencer_type.Enum convertResponseSequencerType(String sequencerType) { @@ -66,11 +61,6 @@ public class FileStorProducer implements StorFilestorConfig.Producer { this.cluster = parent; this.responseNumThreads = featureFlags.defaultNumResponseThreads(); this.responseSequencerType = convertResponseSequencerType(featureFlags.responseSequencerType()); - this.persistenceThrottlingWsDecrementFactor = featureFlags.persistenceThrottlingWsDecrementFactor(); - this.persistenceThrottlingWsBackoff = featureFlags.persistenceThrottlingWsBackoff(); - this.persistenceThrottlingWindowSize = featureFlags.persistenceThrottlingWindowSize(); - this.persistenceThrottlingWsResizeRate = featureFlags.persistenceThrottlingWsResizeRate(); - this.persistenceThrottlingOfMergeFeedOps = featureFlags.persistenceThrottlingOfMergeFeedOps(); this.useAsyncMessageHandlingOnSchedule = featureFlags.useAsyncMessageHandlingOnSchedule(); } @@ -84,14 +74,6 @@ public class FileStorProducer implements StorFilestorConfig.Producer { builder.response_sequencer_type(responseSequencerType); builder.use_async_message_handling_on_schedule(useAsyncMessageHandlingOnSchedule); var throttleBuilder = new StorFilestorConfig.Async_operation_throttler.Builder(); - throttleBuilder.window_size_decrement_factor(persistenceThrottlingWsDecrementFactor); - throttleBuilder.window_size_backoff(persistenceThrottlingWsBackoff); - if (persistenceThrottlingWindowSize > 0) { - throttleBuilder.min_window_size(persistenceThrottlingWindowSize); - throttleBuilder.max_window_size(persistenceThrottlingWindowSize); - } - throttleBuilder.resize_rate(persistenceThrottlingWsResizeRate); - throttleBuilder.throttle_individual_merge_feed_ops(persistenceThrottlingOfMergeFeedOps); builder.async_operation_throttler(throttleBuilder); } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java index e66f2c48f26..4298488b1fd 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorServerProducer.java @@ -1,7 +1,6 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.content.storagecluster; -import com.yahoo.config.model.api.ModelContext; import com.yahoo.vespa.config.content.core.StorServerConfig; import com.yahoo.vespa.model.builder.xml.dom.ModelElement; import com.yahoo.vespa.model.content.cluster.ContentCluster; @@ -11,10 +10,10 @@ import com.yahoo.vespa.model.content.cluster.ContentCluster; */ public class StorServerProducer implements StorServerConfig.Producer { public static class Builder { - StorServerProducer build(ModelContext.Properties properties, ModelElement element) { + StorServerProducer build(ModelElement element) { ModelElement tuning = element.child("tuning"); - StorServerProducer producer = new StorServerProducer(ContentCluster.getClusterId(element), properties.featureFlags()); + StorServerProducer producer = new StorServerProducer(ContentCluster.getClusterId(element)); if (tuning == null) return producer; ModelElement merges = tuning.child("merges"); @@ -29,7 +28,6 @@ public class StorServerProducer implements StorServerConfig.Producer { private final String clusterName; private Integer maxMergesPerNode; private Integer queueSize; - private final StorServerConfig.Merge_throttling_policy.Type.Enum mergeThrottlingPolicyType; private StorServerProducer setMaxMergesPerNode(Integer value) { if (value != null) { @@ -44,19 +42,8 @@ public class StorServerProducer implements StorServerConfig.Producer { return this; } - private static StorServerConfig.Merge_throttling_policy.Type.Enum toThrottlePolicyType(String policyType) { - try { - return StorServerConfig.Merge_throttling_policy.Type.Enum.valueOf(policyType); - } catch (Throwable t) { - return StorServerConfig.Merge_throttling_policy.Type.STATIC; - } - } - - StorServerProducer(String clusterName, ModelContext.FeatureFlags featureFlags) { + StorServerProducer(String clusterName) { this.clusterName = clusterName; - maxMergesPerNode = featureFlags.maxConcurrentMergesPerNode(); - queueSize = featureFlags.maxMergeQueueSize(); - mergeThrottlingPolicyType = toThrottlePolicyType(featureFlags.mergeThrottlingPolicy()); } @Override @@ -73,7 +60,5 @@ public class StorServerProducer implements StorServerConfig.Producer { if (queueSize != null) { builder.max_merge_queue_size(queueSize); } - // TODO set throttle policy params based on existing or separate flags - builder.merge_throttling_policy(new StorServerConfig.Merge_throttling_policy.Builder().type(mergeThrottlingPolicyType)); } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java index da82a69842a..9b59f6db742 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/content/storagecluster/StorageCluster.java @@ -38,7 +38,7 @@ public class StorageCluster extends AbstractConfigProducer<StorageNode> ContentCluster.getClusterId(clusterElem), new FileStorProducer.Builder().build(deployState.getProperties(), cluster, clusterElem), new IntegrityCheckerProducer.Builder().build(cluster, clusterElem), - new StorServerProducer.Builder().build(deployState.getProperties(), clusterElem), + new StorServerProducer.Builder().build(clusterElem), new StorVisitorProducer.Builder().build(clusterElem), new PersistenceProducer.Builder().build(clusterElem)); } diff --git a/config-model/src/main/resources/schema/deployment.rnc b/config-model/src/main/resources/schema/deployment.rnc index 9723e531bd2..bf94ee3b750 100644 --- a/config-model/src/main/resources/schema/deployment.rnc +++ b/config-model/src/main/resources/schema/deployment.rnc @@ -35,6 +35,7 @@ PrimitiveStep = Instance = element instance { attribute id { xsd:string } & + attribute tags { xsd:string }? & attribute athenz-service { xsd:string }? & attribute cloud-account { xsd:string }? & StepExceptInstance diff --git a/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java b/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java index c1dd62316db..6507341670f 100644 --- a/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java +++ b/config-model/src/test/java/com/yahoo/config/model/ApplicationDeployTest.java @@ -10,6 +10,7 @@ import com.yahoo.config.model.application.provider.DeployData; import com.yahoo.config.model.application.provider.FilesApplicationPackage; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.document.DataType; import com.yahoo.io.IOUtils; import com.yahoo.path.Path; @@ -225,17 +226,19 @@ public class ApplicationDeployTest { IOUtils.copyDirectory(new File(appPkg), tmp); ApplicationId applicationId = ApplicationId.from("tenant1", "application1", "instance1"); DeployData deployData = new DeployData("bar", - applicationId, - 13L, - false, - 1337L, - 3L); + applicationId, + Tags.fromString("tag1 tag2"), + 13L, + false, + 1337L, + 3L); FilesApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData); app.writeMetaData(); FilesApplicationPackage newApp = FilesApplicationPackage.fromFileWithDeployData(tmp, deployData); ApplicationMetaData meta = newApp.getMetaData(); assertEquals("bar", meta.getDeployPath()); assertEquals(applicationId, meta.getApplicationId()); + assertEquals(Tags.fromString("tag1 tag2"), meta.getTags()); assertEquals(13L, (long) meta.getDeployTimestamp()); assertEquals(1337L, (long) meta.getGeneration()); assertEquals(3L, meta.getPreviousActiveGeneration()); diff --git a/config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java b/config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java index 719db2ffdcc..9de44d28c09 100644 --- a/config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java +++ b/config-model/src/test/java/com/yahoo/schema/processing/PagedAttributeValidatorTestCase.java @@ -71,8 +71,13 @@ public class PagedAttributeValidatorTestCase { } @Test - void non_dense_tensor_attribute_does_not_support_paged_setting() throws ParseException { - assertPagedSettingNotSupported("tensor(x{},y[2])"); + void non_dense_none_fast_rank_tensor_attribute_supports_paged_setting() throws ParseException { + assertPagedSupported("tensor(x{},y[2])"); + } + + @Test + void non_dense_fast_rank_tensor_attribute_does_not_support_paged_setting() throws ParseException { + assertPagedSettingNotSupported("tensor(x{},y[2])", true); } @Test @@ -82,36 +87,45 @@ public class PagedAttributeValidatorTestCase { @Test void reference_attribute_support_paged_setting() throws ParseException { - assertPagedSupported("reference<parent>", Optional.of(getSd("parent", "int"))); + assertPagedSupported("reference<parent>", Optional.of(getSd("parent", "int", false))); } private void assertPagedSettingNotSupported(String fieldType) throws ParseException { - assertPagedSettingNotSupported(fieldType, Optional.empty()); + assertPagedSettingNotSupported(fieldType, false); + } + + private void assertPagedSettingNotSupported(String fieldType, boolean fastRank) throws ParseException { + assertPagedSettingNotSupported(fieldType, fastRank, Optional.empty()); } - private void assertPagedSettingNotSupported(String fieldType, Optional<String> parentSd) throws ParseException { + private void assertPagedSettingNotSupported(String fieldType, boolean fastRank, Optional<String> parentSd) throws ParseException { try { if (parentSd.isPresent()) { - createFromStrings(new BaseDeployLogger(), parentSd.get(), getSd(fieldType)); + createFromStrings(new BaseDeployLogger(), parentSd.get(), getSd(fieldType, fastRank)); } else { - createFromString(getSd(fieldType)); + createFromString(getSd(fieldType, fastRank)); } fail("Expected exception"); } catch (IllegalArgumentException e) { - assertEquals("For schema 'test', field 'foo': The 'paged' attribute setting is not supported for non-dense tensor, predicate and reference types", + assertEquals("For schema 'test', field 'foo': The 'paged' attribute setting is not supported for fast-rank tensor and predicate types", e.getMessage()); } } private String getSd(String fieldType) { - return getSd("test", fieldType); + return getSd(fieldType, false); + } + + private String getSd(String fieldType, boolean fastRank) { + return getSd("test", fieldType, fastRank); } - private String getSd(String docType, String fieldType) { + private String getSd(String docType, String fieldType, boolean fastRank) { return joinLines( "schema " + docType + " {", " document " + docType + " {", " field foo type " + fieldType + "{", + (fastRank ? "attribute: fast-rank" : ""), " indexing: attribute", " attribute: paged", " }", diff --git a/config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java index 376cf49c396..eb77187014f 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/admin/ClusterControllerTestCase.java @@ -413,6 +413,11 @@ public class ClusterControllerTestCase extends DomBuilderTest { assertEquals(0, qrStartConfig.jvm().directMemorySizeCache()); assertEquals(16, qrStartConfig.jvm().baseMaxDirectMemorySize()); + CuratorConfig.Builder curatorBuilder = new CuratorConfig.Builder(); + model.getConfig(curatorBuilder, "foo"); + CuratorConfig curatorConfig = curatorBuilder.build(); + assertEquals(120, curatorConfig.zookeeperSessionTimeoutSeconds()); + assertReindexingConfigPresent(model); assertReindexingConfiguredOnAdminCluster(model); } diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java index cc6b84de698..da70daa2b4d 100755 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/ContainerClusterTest.java @@ -343,6 +343,7 @@ public class ContainerClusterTest { assertEquals(List.of("host-c1", "host-c2"), config.server().stream().map(CuratorConfig.Server::hostname).collect(Collectors.toList())); assertTrue(config.zookeeperLocalhostAffinity()); + assertEquals(12, config.zookeeperSessionTimeoutSeconds()); } @Test diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java index 272dfa19f64..cf6b6365792 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilderTest.java @@ -570,6 +570,7 @@ public class ContainerModelBuilderTest extends ContainerModelBuilderTestBase { ApplicationContainerCluster cluster = model.getContainerClusters().get("default"); assertNotNull(cluster); assertComponentConfigured(cluster, "com.yahoo.vespa.curator.Curator"); + assertComponentConfigured(cluster, "com.yahoo.vespa.curator.CuratorWrapper"); cluster.getContainers().forEach(container -> { assertComponentConfigured(container, "com.yahoo.vespa.zookeeper.ReconfigurableVespaZooKeeperServer"); assertComponentConfigured(container, "com.yahoo.vespa.zookeeper.Reconfigurer"); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/ContentClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentClusterTest.java index 0ba47d9c58b..c41840eaefa 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/content/ContentClusterTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/content/ContentClusterTest.java @@ -1132,23 +1132,6 @@ public class ContentClusterTest extends ContentBaseTest { assertEquals(4, resolveTunedNumDistributorStripesConfig(65)); } - @Test - void unordered_merge_chaining_config_controlled_by_properties() throws Exception { - assertFalse(resolveUnorderedMergeChainingConfig(Optional.of(false))); - assertTrue(resolveUnorderedMergeChainingConfig(Optional.empty())); - } - - private boolean resolveUnorderedMergeChainingConfig(Optional<Boolean> unorderedMergeChaining) throws Exception { - var props = new TestProperties(); - if (unorderedMergeChaining.isPresent()) { - props.setUnorderedMergeChaining(unorderedMergeChaining.get()); - } - var cluster = createOneNodeCluster(props); - var builder = new StorDistributormanagerConfig.Builder(); - cluster.getDistributorNodes().getConfig(builder); - return (new StorDistributormanagerConfig(builder)).use_unordered_merge_chaining(); - } - private boolean resolveTwoPhaseGcConfigWithFeatureFlag(Boolean flagEnableTwoPhase) { var props = new TestProperties(); if (flagEnableTwoPhase != null) { diff --git a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java index f7afcc281f9..f3a59733ece 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/content/StorageClusterTest.java @@ -120,9 +120,7 @@ public class StorageClusterTest { parse(cluster("foofighters", joinLines( "<tuning>", " <merges max-per-node=\"1K\" max-queue-size=\"10K\"/>", - "</tuning>")), - new TestProperties().setMaxMergeQueueSize(1919).setMaxConcurrentMergesPerNode(37) - ).getConfig(builder); + "</tuning>"))).getConfig(builder); StorServerConfig config = new StorServerConfig(builder); assertEquals(1024, config.max_merges_per_node()); @@ -174,9 +172,9 @@ public class StorageClusterTest { @Test void testMergeFeatureFlags() { - var config = configFromProperties(new TestProperties().setMaxMergeQueueSize(1919).setMaxConcurrentMergesPerNode(37)); - assertEquals(37, config.max_merges_per_node()); - assertEquals(1919, config.max_merge_queue_size()); + var config = configFromProperties(new TestProperties()); + assertEquals(16, config.max_merges_per_node()); + assertEquals(100, config.max_merge_queue_size()); } @Test @@ -186,19 +184,6 @@ public class StorageClusterTest { } @Test - void merge_throttling_policy_config_is_derived_from_flag() { - var config = configFromProperties(new TestProperties().setMergeThrottlingPolicy("STATIC")); - assertEquals(StorServerConfig.Merge_throttling_policy.Type.STATIC, config.merge_throttling_policy().type()); - - config = configFromProperties(new TestProperties().setMergeThrottlingPolicy("DYNAMIC")); - assertEquals(StorServerConfig.Merge_throttling_policy.Type.DYNAMIC, config.merge_throttling_policy().type()); - - // Invalid enum values fall back to the default - config = configFromProperties(new TestProperties().setMergeThrottlingPolicy("UKULELE")); - assertEquals(StorServerConfig.Merge_throttling_policy.Type.STATIC, config.merge_throttling_policy().type()); - } - - @Test void testVisitors() { StorVisitorConfig.Builder builder = new StorVisitorConfig.Builder(); parse(cluster("bees", @@ -340,23 +325,6 @@ public class StorageClusterTest { } @Test - void persistence_dynamic_throttling_parameters_can_be_set_through_feature_flags() { - var config = filestorConfigFromProducer(simpleCluster(new TestProperties() - .setPersistenceThrottlingWsDecrementFactor(1.5) - .setPersistenceThrottlingWsBackoff(0.8) - .setPersistenceThrottlingWindowSize(42) - .setPersistenceThrottlingWsResizeRate(2.5) - .setPersistenceThrottlingOfMergeFeedOps(false))); - assertEquals(1.5, config.async_operation_throttler().window_size_decrement_factor(), 0.0001); - assertEquals(0.8, config.async_operation_throttler().window_size_backoff(), 0.0001); - // If window size is set, min and max are locked to the same value - assertEquals(42, config.async_operation_throttler().min_window_size()); - assertEquals(42, config.async_operation_throttler().max_window_size()); - assertEquals(2.5, config.async_operation_throttler().resize_rate(), 0.0001); - assertFalse(config.async_operation_throttler().throttle_individual_merge_feed_ops()); - } - - @Test void integrity_checker_explicitly_disabled_when_not_running_with_vds_provider() { StorIntegritycheckerConfig.Builder builder = new StorIntegritycheckerConfig.Builder(); parse(cluster("bees", "")).getConfig(builder); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java b/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java index 8830e5484b3..ee885cdf43e 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/search/test/DocumentDatabaseTestCase.java @@ -19,7 +19,6 @@ import com.yahoo.vespa.model.content.utils.DocType; import com.yahoo.vespa.model.search.IndexedSearchCluster; import org.junit.jupiter.api.Test; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -55,7 +54,7 @@ public class DocumentDatabaseTestCase { @Test void requireThatMixedModeConcurrencyIsReflectedCorrectlyForDefault() { - verifyConcurrency(Arrays.asList(DocType.create("a", "index"), DocType.create("b", "streaming")), "", 1.0); + verifyConcurrency(List.of(DocType.create("a", "index"), DocType.create("b", "streaming")), "", 1.0); } @Test @@ -63,7 +62,7 @@ public class DocumentDatabaseTestCase { String feedTuning = "<feeding>" + " <concurrency>0.7</concurrency>" + "</feeding>\n"; - verifyConcurrency(Arrays.asList(DocType.create("a", "index"), DocType.create("b", "streaming")), feedTuning, 0.7); + verifyConcurrency(List.of(DocType.create("a", "index"), DocType.create("b", "streaming")), feedTuning, 0.7); } @Test @@ -77,11 +76,11 @@ public class DocumentDatabaseTestCase { } private void verifyConcurrency(String mode, String xmlTuning, double expectedConcurrency, double featureFlagConcurrency) { - verifyConcurrency(Arrays.asList(DocType.create("a", mode)), xmlTuning, expectedConcurrency, featureFlagConcurrency); + verifyConcurrency(List.of(DocType.create("a", mode)), xmlTuning, expectedConcurrency, featureFlagConcurrency); } private void verifyConcurrency(String mode, String xmlTuning, double expectedConcurrency) { - verifyConcurrency(Arrays.asList(DocType.create("a", mode)), xmlTuning, expectedConcurrency, null); + verifyConcurrency(List.of(DocType.create("a", mode)), xmlTuning, expectedConcurrency, null); } private void verifyConcurrency(List<DocType> nameAndModes, String xmlTuning, double expectedConcurrency) { @@ -114,14 +113,14 @@ public class DocumentDatabaseTestCase { @Test void requireFeedNicenessIsReflected() { - verifyFeedNiceness(Arrays.asList(DocType.create("a", "index")), 0.0, null); - verifyFeedNiceness(Arrays.asList(DocType.create("a", "index")), 0.32, 0.32); + verifyFeedNiceness(List.of(DocType.create("a", "index")), 0.0, null); + verifyFeedNiceness(List.of(DocType.create("a", "index")), 0.32, 0.32); } @Test void requireThatModeIsSet() { var tester = new SchemaTester(); - VespaModel model = tester.createModel(Arrays.asList(DocType.create("a", "index"), + VespaModel model = tester.createModel(List.of(DocType.create("a", "index"), DocType.create("b", "streaming"), DocType.create("c", "store-only")), ""); ContentSearchCluster contentSearchCluster = model.getContentClusters().get("test").getSearch(); @@ -150,8 +149,8 @@ public class DocumentDatabaseTestCase { @Test void requireThatMixedModeInitialDocumentCountIsReflectedCorrectlyForDefault() { final long DEFAULT = 1024L; - verifyInitialDocumentCount(Arrays.asList(DocType.create("a", "index"), DocType.create("b", "streaming")), - "", Arrays.asList(DEFAULT, DEFAULT)); + verifyInitialDocumentCount(List.of(DocType.create("a", "index"), DocType.create("b", "streaming")), + "", List.of(DEFAULT, DEFAULT)); } @Test @@ -160,8 +159,8 @@ public class DocumentDatabaseTestCase { String feedTuning = "<resizing>" + " <initialdocumentcount>1000000000</initialdocumentcount>" + "</resizing>\n"; - verifyInitialDocumentCount(Arrays.asList(DocType.create("a", "index"), DocType.create("b", "streaming")), - feedTuning, Arrays.asList(INITIAL, INITIAL)); + verifyInitialDocumentCount(List.of(DocType.create("a", "index"), DocType.create("b", "streaming")), + feedTuning, List.of(INITIAL, INITIAL)); } private void assertDocTypeConfig(VespaModel model, String configId, String indexField, String attributeField) { @@ -345,30 +344,30 @@ public class DocumentDatabaseTestCase { @Test void testThatAttributesMaxUnCommittedMemoryIsControlledByFeatureFlag() { - assertAttributesConfigIndependentOfMode("index", Arrays.asList("type1"), - Arrays.asList("test/search/cluster.test/type1"), - ImmutableMap.of("type1", Arrays.asList("f2", "f2_nfa")), + assertAttributesConfigIndependentOfMode("index", List.of("type1"), + List.of("test/search/cluster.test/type1"), + ImmutableMap.of("type1", List.of("f2", "f2_nfa")), new DeployState.Builder().properties(new TestProperties().maxUnCommittedMemory(193452)), 193452); } @Test void testThatAttributesConfigIsProducedForIndexed() { - assertAttributesConfigIndependentOfMode("index", Arrays.asList("type1"), - Arrays.asList("test/search/cluster.test/type1"), - ImmutableMap.of("type1", Arrays.asList("f2", "f2_nfa"))); + assertAttributesConfigIndependentOfMode("index", List.of("type1"), + List.of("test/search/cluster.test/type1"), + ImmutableMap.of("type1", List.of("f2", "f2_nfa"))); } @Test void testThatAttributesConfigIsProducedForStreamingForFastAccessFields() { - assertAttributesConfigIndependentOfMode("streaming", Arrays.asList("type1"), - Arrays.asList("test/search/type1"), - ImmutableMap.of("type1", Arrays.asList("f2"))); + assertAttributesConfigIndependentOfMode("streaming", List.of("type1"), + List.of("test/search/type1"), + ImmutableMap.of("type1", List.of("f2"))); } @Test void testThatAttributesConfigIsNotProducedForStoreOnlyEvenForFastAccessFields() { - assertAttributesConfigIndependentOfMode("store-only", Arrays.asList("type1"), - Arrays.asList("test/search"), Collections.emptyMap()); + assertAttributesConfigIndependentOfMode("store-only", List.of("type1"), + List.of("test/search"), Collections.emptyMap()); } } diff --git a/config-model/src/test/schema-test-files/deployment-with-instances.xml b/config-model/src/test/schema-test-files/deployment-with-instances.xml index 39771ca9d41..f37ff9f6cc6 100644 --- a/config-model/src/test/schema-test-files/deployment-with-instances.xml +++ b/config-model/src/test/schema-test-files/deployment-with-instances.xml @@ -35,7 +35,7 @@ <delay hours='2'/> <parallel> - <instance id="three"> + <instance id="three" tags="a b"> <test/> <staging/> </instance> diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/HostLivenessTracker.java b/config-provisioning/src/main/java/com/yahoo/config/provision/HostLivenessTracker.java deleted file mode 100644 index f3326ec2bc8..00000000000 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/HostLivenessTracker.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.config.provision; - -import java.time.Instant; -import java.util.Optional; - -/** - * Instances of this are used to keep track of (notify and query) - * which hosts are currently connected to the config system. - * - * @author bratseth - */ -public interface HostLivenessTracker { - - /** Called each time a config request is received from a client */ - void receivedRequestFrom(String hostname); - - /** - * Returns the epoch timestamp of the last request received from the given hostname, - * or empty if there is no memory of this host making a request - */ - Optional<Instant> lastRequestFrom(String hostname); - -} diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java index d568a61fc69..507d95c1d7b 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/NodeAllocationException.java @@ -9,8 +9,15 @@ package com.yahoo.config.provision; */ public class NodeAllocationException extends RuntimeException { - public NodeAllocationException(String message) { + private final boolean retryable; + + public NodeAllocationException(String message, boolean retryable) { super(message); + this.retryable = retryable; + } + + public boolean retryable() { + return retryable; } } diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/Tags.java b/config-provisioning/src/main/java/com/yahoo/config/provision/Tags.java new file mode 100644 index 00000000000..3a54a0b9ebb --- /dev/null +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/Tags.java @@ -0,0 +1,64 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A deployment may have a list of tags associated with it. Config files may have variants for these tags similar + * to how they may have variants for instance and zone. + * + * @author bratseth + */ +public class Tags { + + private final Set<String> tags; + + public Tags(Set<String> tags) { + this.tags = Set.copyOf(tags); + } + + public boolean contains(String tag) { + return tags.contains(tag); + } + + public boolean intersects(Tags other) { + return this.tags.stream().anyMatch(other::contains); + } + + public boolean isEmpty() { return tags.isEmpty(); } + + public boolean containsAll(Tags other) { return tags.containsAll(other.tags); } + + /** Returns this as a space-separated string which can be used to recreate this by calling fromString(). */ + public String asString() { + return tags.stream().sorted().collect(Collectors.joining(" ")); + } + + @Override + public String toString() { + return asString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) return true; + if (other == null || other.getClass() != getClass()) return false; + return tags.equals(((Tags)other).tags); + } + + @Override + public int hashCode() { + return tags.hashCode(); + } + + public static Tags empty() { return new Tags(Set.of()); } + + /** + * Creates this from a space-separated string or null. */ + public static Tags fromString(String tagsString) { + if (tagsString == null || tagsString.isBlank()) return empty(); + return new Tags(Set.of(tagsString.trim().split(" +"))); + } + +} diff --git a/config-provisioning/src/test/java/com/yahoo/config/provision/TagsTest.java b/config-provisioning/src/test/java/com/yahoo/config/provision/TagsTest.java new file mode 100644 index 00000000000..a20e674c66e --- /dev/null +++ b/config-provisioning/src/test/java/com/yahoo/config/provision/TagsTest.java @@ -0,0 +1,57 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.config.provision; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author bratseth + */ +public class TagsTest { + + @Test + public void testEmpty() { + assertEquals(Tags.empty(), Tags.fromString(null)); + assertEquals(Tags.empty(), Tags.fromString("")); + assertEquals(Tags.empty(), Tags.fromString(" ")); + } + + @Test + public void testDeserialization() { + assertEquals(new Tags(Set.of("tag1", "tag2")), Tags.fromString(" tag1 tag2 ")); + } + + @Test + public void testSerialization() { + Tags tags = new Tags(Set.of("a", "tag2", "3")); + assertEquals(tags, Tags.fromString(tags.toString())); // Required by automatic serialization + assertEquals(tags, Tags.fromString(tags.asString())); // Required by automatic serialization + } + + @Test + public void testContains() { + Tags tags = new Tags(Set.of("a", "tag2", "3")); + assertTrue(tags.contains("a")); + assertTrue(tags.contains("tag2")); + assertTrue(tags.contains("3")); + assertFalse(tags.contains("other")); + + Tags subTags = new Tags(Set.of("a", "3")); + assertTrue(tags.containsAll(subTags)); + assertFalse(subTags.containsAll(tags)); + } + + @Test + public void testIntersects() { + Tags tags1 = new Tags(Set.of("a", "tag2", "3")); + Tags tags2 = new Tags(Set.of("a", "tag3")); + assertTrue(tags1.intersects(tags2)); + assertTrue(tags2.intersects(tags1)); + } + +} diff --git a/config-proxy/pom.xml b/config-proxy/pom.xml index 476f5f99b86..e241f50e851 100644 --- a/config-proxy/pom.xml +++ b/config-proxy/pom.xml @@ -50,7 +50,7 @@ </dependency> <dependency> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>bcpkix-jdk18on</artifactId> <scope>compile</scope> </dependency> <dependency> diff --git a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/filedistribution/CachedFilesMaintainer.java b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/filedistribution/CachedFilesMaintainer.java index 3eafdf8f2b4..4ca5eb4ee90 100644 --- a/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/filedistribution/CachedFilesMaintainer.java +++ b/config-proxy/src/main/java/com/yahoo/vespa/config/proxy/filedistribution/CachedFilesMaintainer.java @@ -2,9 +2,7 @@ package com.yahoo.vespa.config.proxy.filedistribution; import com.yahoo.io.IOUtils; -import java.util.logging.Level; import com.yahoo.vespa.filedistribution.FileDownloader; - import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; @@ -31,7 +29,7 @@ class CachedFilesMaintainer implements Runnable { private static final File defaultUrlDownloadDir = UrlDownloadRpcServer.downloadDir; private static final File defaultFileReferencesDownloadDir = FileDownloader.defaultDownloadDirectory; - private static final Duration defaultDurationToKeepFiles = Duration.ofDays(30); + private static final Duration defaultDurationToKeepFiles = Duration.ofDays(14); private final File urlDownloadDir; private final File fileReferencesDownloadDir; diff --git a/configdefinitions/src/vespa/curator.def b/configdefinitions/src/vespa/curator.def index c762766a567..db7cea00882 100644 --- a/configdefinitions/src/vespa/curator.def +++ b/configdefinitions/src/vespa/curator.def @@ -7,3 +7,6 @@ server[].port int default=2181 # if true, only connect to server on localhost (must be in one of the servers above) zookeeperLocalhostAffinity bool default=false + +# session timeout, the high default is used by config servers +zookeeperSessionTimeoutSeconds int default=120
\ No newline at end of file diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java index 2a15f724b29..cf79005b1ab 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java @@ -23,6 +23,7 @@ import com.yahoo.config.provision.InfraDeployer; import com.yahoo.config.provision.Provisioner; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.config.provision.exception.ActivationConflictException; @@ -361,8 +362,11 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye } private PrepareResult deploy(File applicationDir, PrepareParams prepareParams, DeployHandlerLogger logger) { - ApplicationId applicationId = prepareParams.getApplicationId(); - long sessionId = createSession(applicationId, prepareParams.getTimeoutBudget(), applicationDir, logger); + long sessionId = createSession(prepareParams.getApplicationId(), + prepareParams.tags(), + prepareParams.getTimeoutBudget(), + applicationDir, + logger); Deployment deployment = prepare(sessionId, prepareParams, logger); if ( ! prepareParams.isDryRun()) @@ -870,21 +874,21 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye return sessionRepository.createSessionFromExisting(fromSession, internalRedeploy, timeoutBudget, deployLogger).getSessionId(); } - public long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, InputStream in, + public long createSession(ApplicationId applicationId, Tags tags, TimeoutBudget timeoutBudget, InputStream in, String contentType, DeployLogger logger) { File tempDir = uncheck(() -> Files.createTempDirectory("deploy")).toFile(); long sessionId; try { - sessionId = createSession(applicationId, timeoutBudget, decompressApplication(in, contentType, tempDir), logger); + sessionId = createSession(applicationId, tags, timeoutBudget, decompressApplication(in, contentType, tempDir), logger); } finally { cleanupTempDirectory(tempDir, logger); } return sessionId; } - public long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, File applicationDirectory, DeployLogger deployLogger) { + public long createSession(ApplicationId applicationId, Tags tags, TimeoutBudget timeoutBudget, File applicationDirectory, DeployLogger deployLogger) { SessionRepository sessionRepository = getTenant(applicationId).getSessionRepository(); - Session session = sessionRepository.createSessionFromApplicationPackage(applicationDirectory, applicationId, timeoutBudget, deployLogger); + Session session = sessionRepository.createSessionFromApplicationPackage(applicationDirectory, applicationId, tags, timeoutBudget, deployLogger); return session.getSessionId(); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java index 353c8c49053..e6c3eaabdfd 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java @@ -180,8 +180,6 @@ public class ModelContextImpl implements ModelContext { private final List<String> allowedAthenzProxyIdentities; private final int maxActivationInhibitedOutOfSyncGroups; private final ToIntFunction<ClusterSpec.Type> jvmOmitStackTraceInFastThrow; - private final int maxConcurrentMergesPerContentNode; - private final int maxMergeQueueSize; private final double resourceLimitDisk; private final double resourceLimitMemory; private final double minNodeRatioPerGroup; @@ -191,16 +189,9 @@ public class ModelContextImpl implements ModelContext { private final int maxUnCommittedMemory; private final boolean forwardIssuesAsErrors; private final boolean ignoreThreadStackSizes; - private final boolean unorderedMergeChaining; private final boolean useV8GeoPositions; private final int maxCompactBuffers; private final List<String> ignoredHttpUserAgents; - private final String mergeThrottlingPolicy; - private final double persistenceThrottlingWsDecrementFactor; - private final double persistenceThrottlingWsBackoff; - private final int persistenceThrottlingWindowSize; - private final double persistenceThrottlingWsResizeRate; - private final boolean persistenceThrottlingOfMergeFeedOps; private final boolean useQrserverServiceName; private final boolean avoidRenamingSummaryFeatures; private final Architecture adminClusterArchitecture; @@ -232,8 +223,6 @@ public class ModelContextImpl implements ModelContext { this.allowedAthenzProxyIdentities = flagValue(source, appId, version, Flags.ALLOWED_ATHENZ_PROXY_IDENTITIES); this.maxActivationInhibitedOutOfSyncGroups = flagValue(source, appId, version, Flags.MAX_ACTIVATION_INHIBITED_OUT_OF_SYNC_GROUPS); this.jvmOmitStackTraceInFastThrow = type -> flagValueAsInt(source, appId, version, type, PermanentFlags.JVM_OMIT_STACK_TRACE_IN_FAST_THROW); - this.maxConcurrentMergesPerContentNode = flagValue(source, appId, version, Flags.MAX_CONCURRENT_MERGES_PER_NODE); - this.maxMergeQueueSize = flagValue(source, appId, version, Flags.MAX_MERGE_QUEUE_SIZE); this.resourceLimitDisk = flagValue(source, appId, version, PermanentFlags.RESOURCE_LIMIT_DISK); this.resourceLimitMemory = flagValue(source, appId, version, PermanentFlags.RESOURCE_LIMIT_MEMORY); this.minNodeRatioPerGroup = flagValue(source, appId, version, Flags.MIN_NODE_RATIO_PER_GROUP); @@ -243,16 +232,9 @@ public class ModelContextImpl implements ModelContext { this.maxUnCommittedMemory = flagValue(source, appId, version, Flags.MAX_UNCOMMITTED_MEMORY); this.forwardIssuesAsErrors = flagValue(source, appId, version, PermanentFlags.FORWARD_ISSUES_AS_ERRORS); this.ignoreThreadStackSizes = flagValue(source, appId, version, Flags.IGNORE_THREAD_STACK_SIZES); - this.unorderedMergeChaining = flagValue(source, appId, version, Flags.UNORDERED_MERGE_CHAINING); this.useV8GeoPositions = flagValue(source, appId, version, Flags.USE_V8_GEO_POSITIONS); this.maxCompactBuffers = flagValue(source, appId, version, Flags.MAX_COMPACT_BUFFERS); this.ignoredHttpUserAgents = flagValue(source, appId, version, PermanentFlags.IGNORED_HTTP_USER_AGENTS); - this.mergeThrottlingPolicy = flagValue(source, appId, version, Flags.MERGE_THROTTLING_POLICY); - this.persistenceThrottlingWsDecrementFactor = flagValue(source, appId, version, Flags.PERSISTENCE_THROTTLING_WS_DECREMENT_FACTOR); - this.persistenceThrottlingWsBackoff = flagValue(source, appId, version, Flags.PERSISTENCE_THROTTLING_WS_BACKOFF); - this.persistenceThrottlingWindowSize = flagValue(source, appId, version, Flags.PERSISTENCE_THROTTLING_WINDOW_SIZE); - this.persistenceThrottlingWsResizeRate = flagValue(source, appId, version, Flags.PERSISTENCE_THROTTLING_WS_RESIZE_RATE); - this.persistenceThrottlingOfMergeFeedOps = flagValue(source, appId, version, Flags.PERSISTENCE_THROTTLING_OF_MERGE_FEED_OPS); this.useQrserverServiceName = flagValue(source, appId, version, Flags.USE_QRSERVER_SERVICE_NAME); this.avoidRenamingSummaryFeatures = flagValue(source, appId, version, Flags.AVOID_RENAMING_SUMMARY_FEATURES); this.adminClusterArchitecture = Architecture.valueOf(flagValue(source, appId, version, PermanentFlags.ADMIN_CLUSTER_NODE_ARCHITECTURE)); @@ -287,8 +269,6 @@ public class ModelContextImpl implements ModelContext { @Override public String jvmOmitStackTraceInFastThrowOption(ClusterSpec.Type type) { return translateJvmOmitStackTraceInFastThrowIntToString(jvmOmitStackTraceInFastThrow, type); } - @Override public int maxConcurrentMergesPerNode() { return maxConcurrentMergesPerContentNode; } - @Override public int maxMergeQueueSize() { return maxMergeQueueSize; } @Override public double resourceLimitDisk() { return resourceLimitDisk; } @Override public double resourceLimitMemory() { return resourceLimitMemory; } @Override public double minNodeRatioPerGroup() { return minNodeRatioPerGroup; } @@ -298,16 +278,9 @@ public class ModelContextImpl implements ModelContext { @Override public int maxUnCommittedMemory() { return maxUnCommittedMemory; } @Override public boolean forwardIssuesAsErrors() { return forwardIssuesAsErrors; } @Override public boolean ignoreThreadStackSizes() { return ignoreThreadStackSizes; } - @Override public boolean unorderedMergeChaining() { return unorderedMergeChaining; } @Override public boolean useV8GeoPositions() { return useV8GeoPositions; } @Override public int maxCompactBuffers() { return maxCompactBuffers; } @Override public List<String> ignoredHttpUserAgents() { return ignoredHttpUserAgents; } - @Override public String mergeThrottlingPolicy() { return mergeThrottlingPolicy; } - @Override public double persistenceThrottlingWsDecrementFactor() { return persistenceThrottlingWsDecrementFactor; } - @Override public double persistenceThrottlingWsBackoff() { return persistenceThrottlingWsBackoff; } - @Override public int persistenceThrottlingWindowSize() { return persistenceThrottlingWindowSize; } - @Override public double persistenceThrottlingWsResizeRate() { return persistenceThrottlingWsResizeRate; } - @Override public boolean persistenceThrottlingOfMergeFeedOps() { return persistenceThrottlingOfMergeFeedOps; } @Override public boolean useQrserverServiceName() { return useQrserverServiceName; } @Override public boolean avoidRenamingSummaryFeatures() { return avoidRenamingSummaryFeatures; } @Override public Architecture adminClusterArchitecture() { return adminClusterArchitecture; } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/host/ConfigRequestHostLivenessTracker.java b/configserver/src/main/java/com/yahoo/vespa/config/server/host/ConfigRequestHostLivenessTracker.java deleted file mode 100644 index 9195f78b23b..00000000000 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/host/ConfigRequestHostLivenessTracker.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.config.server.host; - -import com.yahoo.component.annotation.Inject; -import com.yahoo.config.provision.HostLivenessTracker; - -import java.time.Clock; -import java.time.Instant; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Keeps track of the last config request made by each hostname. - * This always remembers all requests forever since the moment is is constructed. - * This is the implementation which will be injected to components who request a HostLivenessTracker. - * - * @author bratseth - */ -public class ConfigRequestHostLivenessTracker implements HostLivenessTracker { - - private final Clock clock; - private final Map<String, Instant> lastRequestFromHost = new ConcurrentHashMap<>(); - - @Inject - @SuppressWarnings("unused") - public ConfigRequestHostLivenessTracker() { - this(Clock.systemUTC()); - } - - public ConfigRequestHostLivenessTracker(Clock clock) { - this.clock = clock; - } - - @Override - public void receivedRequestFrom(String hostname) { - lastRequestFromHost.put(hostname, clock.instant()); - } - - @Override - public Optional<Instant> lastRequestFrom(String hostname) { - return Optional.ofNullable(lastRequestFromHost.get(hostname)); - } - -} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java index 25ae21f3383..dc3a05e65f9 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java @@ -51,7 +51,8 @@ public class HttpHandler extends ThreadedHttpRequestHandler { } catch (IllegalArgumentException | UnsupportedOperationException e) { return HttpErrorResponse.badRequest(getMessage(e, request)); } catch (NodeAllocationException e) { - return HttpErrorResponse.nodeAllocationFailure(getMessage(e, request)); + return e.retryable() ? HttpErrorResponse.nodeAllocationFailure(getMessage(e, request)) + : HttpErrorResponse.invalidApplicationPackage(getMessage(e, request)); } catch (InternalServerException e) { return HttpErrorResponse.internalServerError(getMessage(e, request)); } catch (UnknownVespaVersionException e) { diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java index 70cc89c08af..49776ebdb1b 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java @@ -6,9 +6,9 @@ package com.yahoo.vespa.config.server.http; */ public class InvalidApplicationException extends IllegalArgumentException { - public InvalidApplicationException(String message) { - super(message); - } + public InvalidApplicationException(String message) { super(message); } + + public InvalidApplicationException(Throwable t) { super(t); } public InvalidApplicationException(String message, Throwable e) { super(message, e); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java index f29b05b66af..71702e2926c 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java @@ -6,6 +6,7 @@ import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; @@ -46,7 +47,7 @@ public class SessionCreateHandler extends SessionHandler { @Override protected HttpResponse handlePOST(HttpRequest request) { - final TenantName tenantName = Utils.getTenantNameFromSessionRequest(request); + TenantName tenantName = Utils.getTenantNameFromSessionRequest(request); Utils.checkThatTenantExists(applicationRepository.tenantRepository(), tenantName); TimeoutBudget timeoutBudget = SessionHandler.getTimeoutBudget(request, zookeeperBarrierTimeout); boolean verbose = request.getBooleanProperty("verbose"); @@ -62,8 +63,12 @@ public class SessionCreateHandler extends SessionHandler { logger = DeployHandlerLogger.forTenant(tenantName, verbose); // TODO: Avoid using application id here at all ApplicationId applicationId = ApplicationId.from(tenantName, ApplicationName.defaultName(), InstanceName.defaultName()); - sessionId = applicationRepository.createSession(applicationId, timeoutBudget, request.getData(), - request.getHeader(ApplicationApiHandler.contentTypeHeader), logger); + sessionId = applicationRepository.createSession(applicationId, + Tags.empty(), + timeoutBudget, + request.getData(), + request.getHeader(ApplicationApiHandler.contentTypeHeader), + logger); } return new SessionCreateResponse(logger.slime(), tenantName, request.getHost(), request.getPort(), sessionId); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/GetConfigProcessor.java b/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/GetConfigProcessor.java index b7327ef3aa7..1c419ce047a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/GetConfigProcessor.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/GetConfigProcessor.java @@ -20,6 +20,7 @@ import com.yahoo.vespa.config.protocol.VespaVersion; import com.yahoo.vespa.config.server.GetConfigContext; import com.yahoo.vespa.config.server.UnknownConfigDefinitionException; import com.yahoo.vespa.config.server.tenant.TenantRepository; + import java.util.Optional; import java.util.Set; import java.util.logging.Level; @@ -150,7 +151,6 @@ class GetConfigProcessor implements Runnable { @Override public void run() { - rpcServer.hostLivenessTracker().receivedRequestFrom(request.getClientHostName()); Pair<GetConfigContext, Long> delayed = getConfig(request); if (delayed != null) { diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/RpcServer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/RpcServer.java index b36967d76a4..a2461706f11 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/RpcServer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/rpc/RpcServer.java @@ -7,7 +7,6 @@ import com.yahoo.component.annotation.Inject; import com.yahoo.concurrent.ThreadFactoryFactory; import com.yahoo.config.FileReference; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.HostLivenessTracker; import com.yahoo.config.provision.TenantName; import com.yahoo.jrt.Acceptor; import com.yahoo.jrt.DataValue; @@ -27,8 +26,8 @@ import com.yahoo.vespa.config.protocol.ConfigResponse; import com.yahoo.vespa.config.protocol.JRTServerConfigRequest; import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3; import com.yahoo.vespa.config.protocol.Trace; -import com.yahoo.vespa.config.server.GetConfigContext; import com.yahoo.vespa.config.server.ConfigActivationListener; +import com.yahoo.vespa.config.server.GetConfigContext; import com.yahoo.vespa.config.server.RequestHandler; import com.yahoo.vespa.config.server.SuperModelRequestHandler; import com.yahoo.vespa.config.server.application.ApplicationSet; @@ -44,6 +43,7 @@ import com.yahoo.vespa.filedistribution.FileDownloader; import com.yahoo.vespa.filedistribution.FileReceiver; import com.yahoo.vespa.filedistribution.FileReferenceData; import com.yahoo.vespa.filedistribution.FileReferenceDownload; + import java.nio.ByteBuffer; import java.time.Duration; import java.util.Arrays; @@ -102,7 +102,6 @@ public class RpcServer implements Runnable, ConfigActivationListener, TenantList private final SuperModelRequestHandler superModelRequestHandler; private final MetricUpdater metrics; private final MetricUpdaterFactory metricUpdaterFactory; - private final HostLivenessTracker hostLivenessTracker; private final FileServer fileServer; private final RpcAuthorizer rpcAuthorizer; @@ -128,13 +127,12 @@ public class RpcServer implements Runnable, ConfigActivationListener, TenantList @Inject public RpcServer(ConfigserverConfig config, SuperModelRequestHandler superModelRequestHandler, MetricUpdaterFactory metrics, HostRegistry hostRegistry, - HostLivenessTracker hostLivenessTracker, FileServer fileServer, RpcAuthorizer rpcAuthorizer, + FileServer fileServer, RpcAuthorizer rpcAuthorizer, RpcRequestHandlerProvider handlerProvider) { this.superModelRequestHandler = superModelRequestHandler; metricUpdaterFactory = metrics; supervisor.setMaxOutputBufferSize(config.maxoutputbuffersize()); this.metrics = metrics.getOrCreateMetricUpdater(Collections.emptyMap()); - this.hostLivenessTracker = hostLivenessTracker; BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(config.maxgetconfigclients()); int rpcWorkerThreads = (config.numRpcThreads() == 0) ? threadsToUse() : config.numRpcThreads(); executorService = new ThreadPoolExecutor(rpcWorkerThreads, rpcWorkerThreads, @@ -613,8 +611,4 @@ public class RpcServer implements Runnable, ConfigActivationListener, TenantList req.returnValues().add(new Int32Value(0)); }); } - - HostLivenessTracker hostLivenessTracker() { - return hostLivenessTracker; - } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java index 175b6f6457f..ec673377af9 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java @@ -10,6 +10,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.security.X509CertificateUtils; @@ -41,6 +42,7 @@ public final class PrepareParams { static final String APPLICATION_NAME_PARAM_NAME = "applicationName"; static final String INSTANCE_PARAM_NAME = "instance"; + static final String TAGS_PARAM_NAME = "tags"; static final String IGNORE_VALIDATION_PARAM_NAME = "ignoreValidationErrors"; static final String DRY_RUN_PARAM_NAME = "dryRun"; static final String VERBOSE_PARAM_NAME = "verbose"; @@ -57,6 +59,7 @@ public final class PrepareParams { static final String CLOUD_ACCOUNT = "cloudAccount"; private final ApplicationId applicationId; + private final Tags tags; private final TimeoutBudget timeoutBudget; private final boolean ignoreValidationErrors; private final boolean dryRun; @@ -74,16 +77,27 @@ public final class PrepareParams { private final List<X509Certificate> operatorCertificates; private final Optional<CloudAccount> cloudAccount; - private PrepareParams(ApplicationId applicationId, TimeoutBudget timeoutBudget, boolean ignoreValidationErrors, - boolean dryRun, boolean verbose, boolean isBootstrap, Optional<Version> vespaVersion, + private PrepareParams(ApplicationId applicationId, + Tags tags, + TimeoutBudget timeoutBudget, + boolean ignoreValidationErrors, + boolean dryRun, + boolean verbose, + boolean isBootstrap, + Optional<Version> vespaVersion, List<ContainerEndpoint> containerEndpoints, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, - Optional<DockerImage> dockerImageRepository, Optional<AthenzDomain> athenzDomain, - Optional<Quota> quota, List<TenantSecretStore> tenantSecretStores, - boolean force, boolean waitForResourcesInPrepare, List<X509Certificate> operatorCertificates, + Optional<DockerImage> dockerImageRepository, + Optional<AthenzDomain> athenzDomain, + Optional<Quota> quota, + List<TenantSecretStore> tenantSecretStores, + boolean force, + boolean waitForResourcesInPrepare, + List<X509Certificate> operatorCertificates, Optional<CloudAccount> cloudAccount) { this.timeoutBudget = timeoutBudget; this.applicationId = Objects.requireNonNull(applicationId); + this.tags = tags; this.ignoreValidationErrors = ignoreValidationErrors; this.dryRun = dryRun; this.verbose = verbose; @@ -110,6 +124,7 @@ public final class PrepareParams { private boolean force = false; private boolean waitForResourcesInPrepare = false; private ApplicationId applicationId = null; + private Tags tags = Tags.empty(); private TimeoutBudget timeoutBudget = new TimeoutBudget(Clock.systemUTC(), Duration.ofSeconds(60)); private Optional<Version> vespaVersion = Optional.empty(); private List<ContainerEndpoint> containerEndpoints = null; @@ -128,6 +143,11 @@ public final class PrepareParams { return this; } + public Builder tags(Tags tags) { + this.tags = tags; + return this; + } + public Builder ignoreValidationErrors(boolean ignoreValidationErrors) { this.ignoreValidationErrors = ignoreValidationErrors; return this; @@ -258,11 +278,24 @@ public final class PrepareParams { } public PrepareParams build() { - return new PrepareParams(applicationId, timeoutBudget, ignoreValidationErrors, dryRun, - verbose, isBootstrap, vespaVersion, containerEndpoints, - endpointCertificateMetadata, dockerImageRepository, athenzDomain, - quota, tenantSecretStores, force, waitForResourcesInPrepare, - operatorCertificates, cloudAccount); + return new PrepareParams(applicationId, + tags, + timeoutBudget, + ignoreValidationErrors, + dryRun, + verbose, + isBootstrap, + vespaVersion, + containerEndpoints, + endpointCertificateMetadata, + dockerImageRepository, + athenzDomain, + quota, + tenantSecretStores, + force, + waitForResourcesInPrepare, + operatorCertificates, + cloudAccount); } } @@ -273,6 +306,7 @@ public final class PrepareParams { .verbose(request.getBooleanProperty(VERBOSE_PARAM_NAME)) .timeoutBudget(SessionHandler.getTimeoutBudget(request, barrierTimeout)) .applicationId(createApplicationId(request, tenant)) + .tags(Tags.fromString(request.getProperty(TAGS_PARAM_NAME))) .vespaVersion(request.getProperty(VESPA_VERSION_PARAM_NAME)) .containerEndpoints(request.getProperty(CONTAINER_ENDPOINTS_PARAM_NAME)) .endpointCertificateMetadata(request.getProperty(ENDPOINT_CERTIFICATE_METADATA_PARAM_NAME)) @@ -295,6 +329,7 @@ public final class PrepareParams { .verbose(booleanValue(params, VERBOSE_PARAM_NAME)) .timeoutBudget(SessionHandler.getTimeoutBudget(getTimeout(params, barrierTimeout))) .applicationId(createApplicationId(params, tenant)) + .tags(Tags.fromString(params.field(TAGS_PARAM_NAME).asString())) .vespaVersion(SlimeUtils.optionalString(params.field(VESPA_VERSION_PARAM_NAME)).orElse(null)) .containerEndpointList(deserialize(params.field(CONTAINER_ENDPOINTS_PARAM_NAME), ContainerEndpointSerializer::endpointListFromSlime, List.of())) .endpointCertificateMetadata(deserialize(params.field(ENDPOINT_CERTIFICATE_METADATA_PARAM_NAME), EndpointCertificateMetadataSerializer::fromSlime)) @@ -367,9 +402,9 @@ public final class PrepareParams { return applicationId.application().value(); } - public ApplicationId getApplicationId() { - return applicationId; - } + public ApplicationId getApplicationId() { return applicationId; } + + public Tags tags() { return tags; } /** Returns the Vespa version the nodes running the prepared system should have, or empty to use the system version */ public Optional<Version> vespaVersion() { return vespaVersion; } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java index 82faeae01e8..e4bbe120c11 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java @@ -12,6 +12,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.path.Path; import com.yahoo.transaction.Transaction; @@ -119,6 +120,10 @@ public abstract class Session implements Comparable<Session> { sessionZooKeeperClient.writeApplicationId(applicationId); } + public void setTags(Tags tags) { + sessionZooKeeperClient.writeTags(tags); + } + void setApplicationPackageReference(FileReference applicationPackageReference) { sessionZooKeeperClient.writeApplicationPackageReference(Optional.ofNullable(applicationPackageReference)); } @@ -153,6 +158,10 @@ public abstract class Session implements Comparable<Session> { .orElseThrow(() -> new RuntimeException("Unable to read application id for session " + sessionId)); } + public Tags getTags() { + return sessionZooKeeperClient.readTags(); + } + /** Returns application id read from ZooKeeper. Will return Optional.empty() if not found */ public Optional<ApplicationId> getOptionalApplicationId() { try { diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java index 69796e4d0f8..8d023cac88a 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java @@ -17,7 +17,6 @@ import com.yahoo.config.model.api.ContainerEndpoint; import com.yahoo.config.model.api.EndpointCertificateMetadata; import com.yahoo.config.model.api.EndpointCertificateSecrets; import com.yahoo.config.model.api.FileDistribution; -import com.yahoo.config.model.api.ModelContext; import com.yahoo.config.model.api.Quota; import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.provision.AllocatedHosts; @@ -25,6 +24,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.secretstore.SecretStore; import com.yahoo.net.HostName; @@ -35,13 +35,11 @@ import com.yahoo.vespa.config.server.TimeoutBudget; import com.yahoo.vespa.config.server.application.ApplicationSet; import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; -import com.yahoo.vespa.config.server.deploy.ModelContextImpl; import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer; import com.yahoo.vespa.config.server.filedistribution.FileDistributionFactory; import com.yahoo.vespa.config.server.host.HostValidator; import com.yahoo.vespa.config.server.http.InvalidApplicationException; import com.yahoo.vespa.config.server.modelfactory.AllocatedHostsFromAllModels; -import com.yahoo.vespa.config.server.modelfactory.LegacyFlags; import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; import com.yahoo.vespa.config.server.modelfactory.PreparedModelsBuilder; import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; @@ -73,8 +71,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.zip.ZipException; -import static com.yahoo.vespa.config.server.ConfigServerSpec.fromConfig; - /** * A SessionPreparer is responsible for preparing a session given an application package. * @@ -162,6 +158,7 @@ public class SessionPreparer { final PrepareParams params; final ApplicationId applicationId; + final Tags tags; /** The repository part of docker image to be used for this deployment */ final Optional<DockerImage> dockerImageRepository; @@ -193,6 +190,7 @@ public class SessionPreparer { this.applicationPackage = applicationPackage; this.sessionZooKeeperClient = sessionZooKeeperClient; this.applicationId = params.getApplicationId(); + this.tags = params.tags(); this.dockerImageRepository = params.dockerImageRepository(); this.vespaVersion = params.vespaVersion().orElse(Vtag.currentVersion); this.containerEndpointsCache = new ContainerEndpointsCache(tenantPath, curator); @@ -229,7 +227,7 @@ public class SessionPreparer { if (! timeoutBudget.hasTimeLeft(step)) { String used = timeoutBudget.timesUsed(); throw new UncheckedTimeoutException("prepare timed out " + used + " after " + step + - " step (timeout " + timeoutBudget.timeout() + "): " + applicationId); + " step (timeout " + timeoutBudget.timeout() + "): " + applicationId); } } @@ -253,7 +251,7 @@ public class SessionPreparer { this.preprocessedApplicationPackage = applicationPackage.preprocess(zone, logger); } catch (IOException | RuntimeException e) { throw new IllegalArgumentException("Error preprocessing application package for " + applicationId + - ", session " + sessionZooKeeperClient.sessionId(), e); + ", session " + sessionZooKeeperClient.sessionId(), e); } checkTimeout("preprocess"); } @@ -323,10 +321,11 @@ public class SessionPreparer { void vespaPreprocess(File appDir, File inputXml, ApplicationMetaData metaData) { try { new XmlPreProcessor(appDir, - inputXml, - metaData.getApplicationId().instance(), - zone.environment(), - zone.region()) + inputXml, + metaData.getApplicationId().instance(), + zone.environment(), + zone.region(), + metaData.getTags()) .run(); } catch (ParserConfigurationException | IOException | SAXException | TransformerException e) { throw new RuntimeException(e); @@ -351,6 +350,7 @@ public class SessionPreparer { writeStateToZooKeeper(sessionZooKeeperClient, preprocessedApplicationPackage, applicationId, + tags, filereference, dockerImageRepository, vespaVersion, @@ -392,6 +392,7 @@ public class SessionPreparer { private void writeStateToZooKeeper(SessionZooKeeperClient zooKeeperClient, ApplicationPackage applicationPackage, ApplicationId applicationId, + Tags tags, FileReference fileReference, Optional<DockerImage> dockerImageRepository, Version vespaVersion, @@ -408,6 +409,7 @@ public class SessionPreparer { zkDeployer.deploy(applicationPackage, fileRegistryMap, allocatedHosts); // Note: When changing the below you need to also change similar calls in SessionRepository.createSessionFromExisting() zooKeeperClient.writeApplicationId(applicationId); + zooKeeperClient.writeTags(tags); zooKeeperClient.writeApplicationPackageReference(Optional.of(fileReference)); zooKeeperClient.writeVespaVersion(vespaVersion); zooKeeperClient.writeDockerImageRepository(dockerImageRepository); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java index 9e7af5a44a3..15be909c069 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java @@ -13,6 +13,7 @@ import com.yahoo.config.model.api.ConfigDefinitionRepo; import com.yahoo.config.model.application.provider.DeployData; import com.yahoo.config.model.application.provider.FilesApplicationPackage; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.secretstore.SecretStore; @@ -30,6 +31,7 @@ import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; import com.yahoo.vespa.config.server.filedistribution.FileDirectory; import com.yahoo.vespa.config.server.filedistribution.FileDistributionFactory; +import com.yahoo.vespa.config.server.http.InvalidApplicationException; import com.yahoo.vespa.config.server.http.UnknownVespaVersionException; import com.yahoo.vespa.config.server.modelfactory.ActivatedModelsBuilder; import com.yahoo.vespa.config.server.modelfactory.AllocatedHostsFromAllModels; @@ -282,10 +284,17 @@ public class SessionRepository { TimeoutBudget timeoutBudget, DeployLogger deployLogger) { ApplicationId existingApplicationId = existingSession.getApplicationId(); + Tags existingTags = existingSession.getTags(); File existingApp = getSessionAppDir(existingSession.getSessionId()); - LocalSession session = createSessionFromApplication(existingApp, existingApplicationId, internalRedeploy, timeoutBudget, deployLogger); + LocalSession session = createSessionFromApplication(existingApp, + existingApplicationId, + existingTags, + internalRedeploy, + timeoutBudget, + deployLogger); // Note: Setters below need to be kept in sync with calls in SessionPreparer.writeStateToZooKeeper() session.setApplicationId(existingApplicationId); + session.setTags(existingTags); session.setApplicationPackageReference(existingSession.getApplicationPackageReference()); session.setVespaVersion(existingSession.getVespaVersion()); session.setDockerImageRepository(existingSession.getDockerImageRepository()); @@ -306,19 +315,20 @@ public class SessionRepository { */ public LocalSession createSessionFromApplicationPackage(File applicationDirectory, ApplicationId applicationId, + Tags tags, TimeoutBudget timeoutBudget, DeployLogger deployLogger) { applicationRepo.createApplication(applicationId); - return createSessionFromApplication(applicationDirectory, applicationId, false, timeoutBudget, deployLogger); + return createSessionFromApplication(applicationDirectory, applicationId, tags, false, timeoutBudget, deployLogger); } /** * Creates a local session based on a remote session and the distributed application package. * Does not wait for session being created on other servers. */ - private void createLocalSession(File applicationFile, ApplicationId applicationId, long sessionId) { + private void createLocalSession(File applicationFile, ApplicationId applicationId, Tags tags, long sessionId) { try { - ApplicationPackage applicationPackage = createApplicationPackage(applicationFile, applicationId, sessionId, false, Optional.empty()); + ApplicationPackage applicationPackage = createApplicationPackage(applicationFile, applicationId, tags, sessionId, false, Optional.empty()); createLocalSession(sessionId, applicationPackage); } catch (Exception e) { throw new RuntimeException("Error creating session " + sessionId, e); @@ -706,12 +716,13 @@ public class SessionRepository { private ApplicationPackage createApplication(File userDir, File configApplicationDir, ApplicationId applicationId, + Tags tags, long sessionId, Optional<Long> currentlyActiveSessionId, boolean internalRedeploy, Optional<DeployLogger> deployLogger) { long deployTimestamp = System.currentTimeMillis(); - DeployData deployData = new DeployData(userDir.getAbsolutePath(), applicationId, deployTimestamp, internalRedeploy, + DeployData deployData = new DeployData(userDir.getAbsolutePath(), applicationId, tags, deployTimestamp, internalRedeploy, sessionId, currentlyActiveSessionId.orElse(nonExistingActiveSessionId)); FilesApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(configApplicationDir, deployData); validateFileExtensions(applicationId, deployLogger, app); @@ -727,7 +738,7 @@ public class SessionRepository { UnboundStringFlag flag = PermanentFlags.APPLICATION_FILES_WITH_UNKNOWN_EXTENSION; String value = flag.bindTo(flagSource).with(APPLICATION_ID, applicationId.serializedForm()).value(); switch (value) { - case "FAIL" -> throw e; + case "FAIL" -> throw new InvalidApplicationException(e); case "LOG" -> deployLogger.ifPresent(logger -> logger.logApplicationPackage(Level.WARNING, e.getMessage())); default -> log.log(Level.WARNING, "Unknown value for flag " + flag.id() + ": " + value); } @@ -739,13 +750,14 @@ public class SessionRepository { private LocalSession createSessionFromApplication(File applicationDirectory, ApplicationId applicationId, + Tags tags, boolean internalRedeploy, TimeoutBudget timeoutBudget, DeployLogger deployLogger) { long sessionId = getNextSessionId(); try { ensureSessionPathDoesNotExist(sessionId); - ApplicationPackage app = createApplicationPackage(applicationDirectory, applicationId, sessionId, internalRedeploy, Optional.of(deployLogger)); + ApplicationPackage app = createApplicationPackage(applicationDirectory, applicationId, tags, sessionId, internalRedeploy, Optional.of(deployLogger)); log.log(Level.FINE, () -> TenantRepository.logPre(tenantName) + "Creating session " + sessionId + " in ZooKeeper"); SessionZooKeeperClient sessionZKClient = createSessionZooKeeperClient(sessionId); sessionZKClient.createNewSession(clock.instant()); @@ -754,13 +766,14 @@ public class SessionRepository { waiter.awaitCompletion(Duration.ofSeconds(Math.min(120, timeoutBudget.timeLeft().getSeconds()))); addLocalSession(session); return session; - } catch (Exception e) { + } catch (IOException e) { throw new RuntimeException("Error creating session " + sessionId, e); } } private ApplicationPackage createApplicationPackage(File applicationDirectory, ApplicationId applicationId, + Tags tags, long sessionId, boolean internalRedeploy, Optional<DeployLogger> deployLogger) throws IOException { @@ -773,6 +786,7 @@ public class SessionRepository { ApplicationPackage applicationPackage = createApplication(applicationDirectory, userApplicationDir, applicationId, + tags, sessionId, activeSessionId, internalRedeploy, @@ -884,7 +898,7 @@ public class SessionRepository { ApplicationId applicationId = sessionZKClient.readApplicationId() .orElseThrow(() -> new RuntimeException("Could not find application id for session " + sessionId)); log.log(Level.FINE, () -> "Creating local session for tenant '" + tenantName + "' with session id " + sessionId); - createLocalSession(sessionDir, applicationId, sessionId); + createLocalSession(sessionDir, applicationId, sessionZKClient.readTags(), sessionId); } } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java index 988d13b1978..9218b03af1e 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java @@ -14,6 +14,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.AthenzDomain; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.path.Path; import com.yahoo.slime.SlimeUtils; @@ -57,6 +58,7 @@ public class SessionZooKeeperClient { // NOTE: Any state added here MUST also be propagated in com.yahoo.vespa.config.server.deploy.Deployment.prepare() static final String APPLICATION_ID_PATH = "applicationId"; + static final String TAGS_PATH = "tags"; static final String APPLICATION_PACKAGE_REFERENCE_PATH = "applicationPackageReference"; private static final String VERSION_PATH = "version"; private static final String CREATE_TIME_PATH = "createTime"; @@ -171,6 +173,20 @@ public class SessionZooKeeperClient { return curator.getData(applicationIdPath()).map(d -> ApplicationId.fromSerializedForm(Utf8.toString(d))); } + private Path tagsPath() { + return sessionPath.append(TAGS_PATH); + } + + public void writeTags(Tags tags) { + curator.set(tagsPath(), Utf8.toBytes(tags.asString())); + } + + public Tags readTags() { + Optional<byte[]> data = curator.getData(tagsPath()); + if (data.isEmpty()) return Tags.empty(); + return Tags.fromString(Utf8.toString(data.get())); + } + void writeApplicationPackageReference(Optional<FileReference> applicationPackageReference) { applicationPackageReference.ifPresent( reference -> curator.set(applicationPackageReferencePath(), Utf8.toBytes(reference.value()))); diff --git a/configserver/src/main/resources/configserver-app/services.xml b/configserver/src/main/resources/configserver-app/services.xml index 650176829e6..b8397722b3d 100644 --- a/configserver/src/main/resources/configserver-app/services.xml +++ b/configserver/src/main/resources/configserver-app/services.xml @@ -30,7 +30,6 @@ <component id="com.yahoo.vespa.config.server.host.HostRegistry" bundle="configserver" /> <component id="com.yahoo.vespa.config.server.ApplicationRepository" bundle="configserver" /> <component id="com.yahoo.vespa.config.server.version.VersionState" bundle="configserver" /> - <component id="com.yahoo.vespa.config.server.host.ConfigRequestHostLivenessTracker" bundle="configserver" /> <component id="com.yahoo.config.provision.Zone" bundle="config-provisioning" /> <component id="com.yahoo.vespa.config.server.application.ConfigConvergenceChecker" bundle="configserver" /> <component id="com.yahoo.vespa.config.server.application.HttpProxy" bundle="configserver" /> diff --git a/configserver/src/test/apps/hosted-invalid-file-extension/deployment.xml b/configserver/src/test/apps/hosted-invalid-file-extension/deployment.xml new file mode 100644 index 00000000000..43bc1496b2c --- /dev/null +++ b/configserver/src/test/apps/hosted-invalid-file-extension/deployment.xml @@ -0,0 +1,7 @@ +<!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<deployment version='1.0'> + <prod> + <region active="true">us-north-1</region> + <region active="true">us-north-2</region> + </prod> +</deployment> diff --git a/configserver/src/test/apps/hosted-invalid-file-extension/schemas/file-with-invalid.extension b/configserver/src/test/apps/hosted-invalid-file-extension/schemas/file-with-invalid.extension new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/configserver/src/test/apps/hosted-invalid-file-extension/schemas/file-with-invalid.extension diff --git a/configserver/src/test/apps/hosted-invalid-file-extension/services.xml b/configserver/src/test/apps/hosted-invalid-file-extension/services.xml new file mode 100644 index 00000000000..08b1d5d01c2 --- /dev/null +++ b/configserver/src/test/apps/hosted-invalid-file-extension/services.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<services version="1.0"> + + <container version="1.0"> + <http> + <filtering> + <access-control domain="myDomain" write="true" /> + </filtering> + <server id="foo"/> + </http> + <search/> + <nodes count='1'/> + </container> + +</services> diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java index 99487230c5d..b185bb5915d 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ApplicationRepositoryTest.java @@ -17,6 +17,7 @@ import com.yahoo.config.provision.HostFilter; import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NetworkPorts; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.exception.ActivationConflictException; import com.yahoo.container.jdisc.HttpResponse; @@ -847,7 +848,7 @@ public class ApplicationRepositoryTest { } private long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, File app) { - return applicationRepository.createSession(applicationId, timeoutBudget, app, new BaseDeployLogger()); + return applicationRepository.createSession(applicationId, Tags.empty(), timeoutBudget, app, new BaseDeployLogger()); } private long createSessionFromExisting(ApplicationId applicationId, TimeoutBudget timeoutBudget) { diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java index 6d4217d3df4..b1b42465b00 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/HostedDeployTest.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.config.server.deploy; +import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.component.Version; import com.yahoo.config.application.api.ValidationId; import com.yahoo.config.model.api.ConfigChangeAction; @@ -36,6 +37,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import java.nio.file.Files; import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -49,6 +51,7 @@ import java.util.stream.IntStream; import static com.yahoo.vespa.config.server.deploy.DeployTester.CountingModelFactory; import static com.yahoo.vespa.config.server.deploy.DeployTester.createFailingModelFactory; import static com.yahoo.vespa.config.server.deploy.DeployTester.createHostedModelFactory; +import static com.yahoo.yolean.Exceptions.uncheck; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -489,6 +492,25 @@ public class HostedDeployTest { } @Test + public void testThatAppWithFilesWithInvalidFileExtensionFails() { + DeployTester tester = new DeployTester.Builder(temporaryFolder) + .configserverConfig(new ConfigserverConfig(new ConfigserverConfig.Builder() + .hostedVespa(true) + .configServerDBDir(uncheck(() -> Files.createTempDirectory("serverdb")).toString()) + .configDefinitionsDir(uncheck(() -> Files.createTempDirectory("configdefinitions")).toString()) + .fileReferencesDir(uncheck(() -> Files.createTempDirectory("configdefinitions")).toString()))) + .modelFactory(createHostedModelFactory(Version.fromString("8.7.6"), Clock.systemUTC())) + .build(); + try { + tester.deployApp("src/test/apps/hosted-invalid-file-extension/", "8.7.6"); + fail(); + } catch (InvalidApplicationException e) { + assertEquals("java.lang.IllegalArgumentException: File in application package with unknown extension: schemas/file-with-invalid.extension, please delete or move file to another directory.", + e.getMessage()); + } + } + + @Test public void testRedeployWithCloudAccount() { CloudAccount cloudAccount = new CloudAccount("012345678912"); DeployTester tester = new DeployTester.Builder(temporaryFolder) diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java index fd6440a9632..653753c97e7 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClientTest.java @@ -13,6 +13,7 @@ import com.yahoo.config.model.application.provider.MockFileRegistry; import com.yahoo.config.provision.AllocatedHosts; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.HostSpec; +import com.yahoo.config.provision.Tags; import com.yahoo.path.Path; import com.yahoo.text.Utf8; import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage; @@ -59,6 +60,7 @@ public class ZooKeeperClientTest { ApplicationPackage app = FilesApplicationPackage.fromFileWithDeployData(new File("src/test/apps/zkfeed"), new DeployData("/bar/baz", ApplicationId.from("default", "appName", "default"), + Tags.fromString("tag1 tag2"), 1345L, true, 3L, @@ -121,6 +123,7 @@ public class ZooKeeperClientTest { assertTrue(metaData.getChecksum().length() > 0); assertTrue(metaData.isInternalRedeploy()); assertEquals("/bar/baz", metaData.getDeployPath()); + assertEquals(Tags.fromString("tag1 tag2"), metaData.getTags()); assertEquals(1345, metaData.getDeployTimestamp().longValue()); assertEquals(3, metaData.getGeneration().longValue()); assertEquals(2, metaData.getPreviousActiveGeneration()); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java index 1c71ef0b7fb..816f7e3dcec 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandlerTest.java @@ -6,6 +6,7 @@ import com.yahoo.component.Version; import com.yahoo.config.application.api.ApplicationMetaData; import com.yahoo.config.model.application.provider.BaseDeployLogger; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.container.jdisc.HttpResponse; @@ -131,6 +132,7 @@ public class SessionActiveHandlerTest { void invoke() { long sessionId = applicationRepository.createSession(applicationId(), + Tags.empty(), new TimeoutBudget(clock, Duration.ofSeconds(10)), testApp, new BaseDeployLogger()); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java index 2b07cffffce..525a969ed1e 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandlerTest.java @@ -10,6 +10,7 @@ import com.yahoo.config.provision.ApplicationName; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NodeAllocationException; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.jdisc.http.HttpRequest; @@ -187,7 +188,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { } @Test - public void require_that_preparing_with_multiple_tenants_work() throws Exception { + public void prepare_with_multiple_tenants() throws Exception { SessionHandler handler = createHandler(); TenantName defaultTenant = TenantName.from("test2"); @@ -206,7 +207,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { assertEquals(sessionId, sessionId2); // Want to test when they are equal (but for different tenants) pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId2 + - "/prepared?applicationName=" + applicationName; + "/prepared?applicationName=" + applicationName; response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix)); assertNotNull(response); assertEquals(SessionHandlerTest.getRenderedString(response), OK, response.getStatus()); @@ -214,7 +215,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { ApplicationId applicationId3 = ApplicationId.from(tenant.value(), applicationName, "quux"); long sessionId3 = createSession(applicationId3); pathPrefix = "/application/v2/tenant/" + tenant + "/session/" + sessionId3 + - "/prepared?applicationName=" + applicationName + "&instance=quux"; + "/prepared?applicationName=" + applicationName + "&instance=quux"; response = handler.handle(SessionHandlerTest.createTestRequest(pathPrefix)); assertNotNull(response); assertEquals(SessionHandlerTest.getRenderedString(response), OK, response.getStatus()); @@ -243,7 +244,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { FailingSessionPrepareHandler handler = new FailingSessionPrepareHandler(SessionPrepareHandler.testContext(), applicationRepository, configserverConfig, - new NodeAllocationException(exceptionMessage)); + new NodeAllocationException(exceptionMessage, true)); HttpResponse response = handler.handle(createTestRequest(pathPrefix, HttpRequest.Method.PUT, Cmd.PREPARED, sessionId)); assertEquals(400, response.getStatus()); Slime data = getData(response); @@ -324,7 +325,7 @@ public class SessionPrepareHandlerTest extends SessionHandlerTest { } private long createSession(ApplicationId applicationId) { - return applicationRepository.createSession(applicationId, timeoutBudget, app, new BaseDeployLogger()); + return applicationRepository.createSession(applicationId, Tags.empty(), timeoutBudget, app, new BaseDeployLogger()); } private static class FailingSessionPrepareHandler extends SessionPrepareHandler { diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/MockRpcServer.java b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/MockRpcServer.java index 3272689473e..0f9ce9eff13 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/MockRpcServer.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/MockRpcServer.java @@ -7,7 +7,6 @@ import com.yahoo.vespa.config.protocol.ConfigResponse; import com.yahoo.vespa.config.protocol.JRTServerConfigRequest; import com.yahoo.vespa.config.server.GetConfigContext; import com.yahoo.vespa.config.server.filedistribution.FileServer; -import com.yahoo.vespa.config.server.host.ConfigRequestHostLivenessTracker; import com.yahoo.vespa.config.server.host.HostRegistry; import com.yahoo.vespa.config.server.monitoring.Metrics; import com.yahoo.vespa.config.server.rpc.security.NoopRpcAuthorizer; @@ -38,7 +37,6 @@ public class MockRpcServer extends RpcServer { null, Metrics.createTestMetrics(), new HostRegistry(), - new ConfigRequestHostLivenessTracker(), new FileServer(tempDir), new NoopRpcAuthorizer(), new RpcRequestHandlerProvider()); diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcTester.java b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcTester.java index 441f6c3a6ce..e5ed4e4673d 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcTester.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcTester.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.config.server.rpc; import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.HostLivenessTracker; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; import com.yahoo.jrt.Request; @@ -21,7 +20,6 @@ import com.yahoo.vespa.config.server.SuperModelRequestHandler; import com.yahoo.vespa.config.server.TestConfigDefinitionRepo; import com.yahoo.vespa.config.server.application.OrchestratorMock; import com.yahoo.vespa.config.server.filedistribution.FileServer; -import com.yahoo.vespa.config.server.host.ConfigRequestHostLivenessTracker; import com.yahoo.vespa.config.server.host.HostRegistry; import com.yahoo.vespa.config.server.monitoring.Metrics; import com.yahoo.vespa.config.server.rpc.security.NoopRpcAuthorizer; @@ -51,7 +49,6 @@ public class RpcTester implements AutoCloseable { private final ManualClock clock = new ManualClock(Instant.ofEpochMilli(100)); private final String myHostname = HostName.getLocalhost(); - private final HostLivenessTracker hostLivenessTracker = new ConfigRequestHostLivenessTracker(clock); private final Spec spec; private final RpcServer rpcServer; @@ -95,7 +92,6 @@ public class RpcTester implements AutoCloseable { .withProvisioner(new MockProvisioner()) .withOrchestrator(new OrchestratorMock()) .build(); - assertFalse(hostLivenessTracker.lastRequestFrom(myHostname).isPresent()); } public void close() { @@ -122,7 +118,6 @@ public class RpcTester implements AutoCloseable { new InMemoryFlagSource())), Metrics.createTestMetrics(), hostRegistry, - hostLivenessTracker, new FileServer(temporaryFolder.newFolder()), new NoopRpcAuthorizer(), new RpcRequestHandlerProvider()); @@ -167,8 +162,6 @@ public class RpcTester implements AutoCloseable { void performRequest(Request req) { clock.advance(Duration.ofMillis(10)); sup.connect(spec).invokeSync(req, Duration.ofSeconds(10)); - if (req.methodName().equals(RpcServer.getConfigMethodName)) - assertEquals(clock.instant(), hostLivenessTracker.lastRequestFrom(myHostname).get()); } RpcServer rpcServer() { diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java index e5b550fdc1a..34921db1bb7 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/PrepareParamsTest.java @@ -7,6 +7,7 @@ import com.yahoo.config.model.api.EndpointCertificateMetadata; import com.yahoo.config.model.api.TenantSecretStore; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.CloudAccount; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.container.jdisc.HttpRequest; @@ -62,6 +63,7 @@ public class PrepareParamsTest { PrepareParams prepareParams = createParams("http://foo:19071/application/v2/", TenantName.defaultName()); assertEquals(ApplicationId.defaultId(), prepareParams.getApplicationId()); + assertTrue(prepareParams.tags().isEmpty()); assertFalse(prepareParams.isDryRun()); assertFalse(prepareParams.isVerbose()); assertFalse(prepareParams.ignoreValidationErrors()); @@ -72,6 +74,18 @@ public class PrepareParamsTest { } @Test + public void testTagsParsing() throws IOException { + var prepareParams = createParams(request + "&" + PrepareParams.TAGS_PARAM_NAME + "=tag1%20tag2", TenantName.from("foo")); + assertEquals(Tags.fromString("tag1 tag2"), prepareParams.tags()); + + // Verify using json object + var slime = SlimeUtils.jsonToSlime(json); + slime.get().setString(PrepareParams.TAGS_PARAM_NAME, "tag1 tag2"); + PrepareParams prepareParamsJson = PrepareParams.fromJson(SlimeUtils.toJsonBytes(slime), TenantName.from("foo"), Duration.ofSeconds(60)); + assertPrepareParamsEqual(prepareParams, prepareParamsJson); + } + + @Test public void testCorrectParsingWithContainerEndpoints() throws IOException { var endpoints = List.of(new ContainerEndpoint("qrs1", ApplicationClusterEndpoint.Scope.global, List.of("c1.example.com", @@ -207,6 +221,7 @@ public class PrepareParamsTest { assertEquals(urlParams.force(), jsonParams.force()); assertEquals(urlParams.waitForResourcesInPrepare(), jsonParams.waitForResourcesInPrepare()); assertEquals(urlParams.getApplicationId(), jsonParams.getApplicationId()); + assertEquals(urlParams.tags(), jsonParams.tags()); assertEquals(urlParams.getTimeoutBudget().timeout(), jsonParams.getTimeoutBudget().timeout()); assertEquals(urlParams.vespaVersion(), jsonParams.vespaVersion()); assertEquals(urlParams.containerEndpoints(), jsonParams.containerEndpoints()); diff --git a/container-core/abi-spec.json b/container-core/abi-spec.json index 5985e79b786..d324656abf2 100644 --- a/container-core/abi-spec.json +++ b/container-core/abi-spec.json @@ -1051,8 +1051,6 @@ "public com.yahoo.jdisc.http.ConnectorConfig$Builder healthCheckProxy(java.util.function.Consumer)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder proxyProtocol(com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder proxyProtocol(java.util.function.Consumer)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder secureRedirect(com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder secureRedirect(java.util.function.Consumer)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder maxRequestsPerConnection(int)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder maxConnectionLife(double)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder http2Enabled(boolean)", @@ -1074,7 +1072,6 @@ "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder tlsClientAuthEnforcer", "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder healthCheckProxy", "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder proxyProtocol", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder secureRedirect", "public com.yahoo.jdisc.http.ConnectorConfig$Http2$Builder http2", "public com.yahoo.jdisc.http.ConnectorConfig$ServerName$Builder serverName" ] @@ -1193,37 +1190,6 @@ ], "fields": [] }, - "com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void <init>()", - "public void <init>(com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect)", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder enabled(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder port(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void <init>(com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder)", - "public boolean enabled()", - "public int port()" - ], - "fields": [] - }, "com.yahoo.jdisc.http.ConnectorConfig$ServerName$Builder": { "superClass": "java.lang.Object", "interfaces": [ @@ -1236,9 +1202,13 @@ "public void <init>()", "public void <init>(com.yahoo.jdisc.http.ConnectorConfig$ServerName)", "public com.yahoo.jdisc.http.ConnectorConfig$ServerName$Builder fallback(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$ServerName$Builder allowed(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$ServerName$Builder allowed(java.util.Collection)", "public com.yahoo.jdisc.http.ConnectorConfig$ServerName build()" ], - "fields": [] + "fields": [ + "public java.util.List allowed" + ] }, "com.yahoo.jdisc.http.ConnectorConfig$ServerName": { "superClass": "com.yahoo.config.InnerNode", @@ -1249,7 +1219,9 @@ ], "methods": [ "public void <init>(com.yahoo.jdisc.http.ConnectorConfig$ServerName$Builder)", - "public java.lang.String fallback()" + "public java.lang.String fallback()", + "public java.util.List allowed()", + "public java.lang.String allowed(int)" ], "fields": [] }, @@ -1443,7 +1415,6 @@ "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer tlsClientAuthEnforcer()", "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy healthCheckProxy()", "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol proxyProtocol()", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect secureRedirect()", "public int maxRequestsPerConnection()", "public double maxConnectionLife()", "public boolean http2Enabled()", diff --git a/container-core/src/main/java/com/yahoo/container/handler/LogReader.java b/container-core/src/main/java/com/yahoo/container/handler/LogReader.java index 2890cbfb5ab..9f270acce5f 100644 --- a/container-core/src/main/java/com/yahoo/container/handler/LogReader.java +++ b/container-core/src/main/java/com/yahoo/container/handler/LogReader.java @@ -29,6 +29,7 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.PriorityQueue; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,13 +44,13 @@ import static java.nio.charset.StandardCharsets.UTF_8; * @author jonmv */ class LogReader { + static final Pattern logArchivePathPattern = Pattern.compile("(\\d{4})/(\\d{2})/(\\d{2})/(\\d{2})-\\d+(\\.gz|\\.zst)?"); static final Pattern vespaLogPathPattern = Pattern.compile("vespa\\.log(?:-(\\d{4})-(\\d{2})-(\\d{2})\\.(\\d{2})-(\\d{2})-(\\d{2})(?:\\.gz|\\.zst)?)?"); private final Path logDirectory; private final Pattern logFilePattern; - LogReader(String logDirectory, String logFilePattern) { this(Paths.get(Defaults.getDefaults().underVespaHome(logDirectory)), Pattern.compile(logFilePattern)); } @@ -73,10 +74,18 @@ class LogReader { Iterator<LineWithTimestamp> lines = Iterators.mergeSorted(logLineIterators, Comparator.comparingDouble(LineWithTimestamp::timestamp)); + PriorityQueue<LineWithTimestamp> heap = new PriorityQueue<>(Comparator.comparingDouble(LineWithTimestamp::timestamp)); while (lines.hasNext()) { + heap.offer(lines.next()); + if (heap.size() > 1000) { + if (linesWritten++ >= maxLines) return; + writer.write(heap.poll().line); + writer.newLine(); + } + } + while ( ! heap.isEmpty()) { if (linesWritten++ >= maxLines) return; - String line = lines.next().line(); - writer.write(line); + writer.write(heap.poll().line); writer.newLine(); } } @@ -170,7 +179,7 @@ class LogReader { if (parts.length != 7) continue; - if (hostname.map(host -> !host.equals(parts[1])).orElse(false)) + if (hostname.map(host -> ! host.equals(parts[1])).orElse(false)) continue; double timestamp = Double.parseDouble(parts[0]); diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java index cfd2244bd70..25fe775082d 100644 --- a/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java +++ b/container-core/src/main/java/com/yahoo/container/jdisc/state/StateHandler.java @@ -102,22 +102,14 @@ public class StateHandler extends AbstractRequestHandler { private ByteBuffer buildContent(URI requestUri) { String suffix = resolvePath(requestUri); - switch (suffix) { - case "": - return ByteBuffer.wrap(apiLinks(requestUri)); - case CONFIG_GENERATION_PATH: - return ByteBuffer.wrap(config); - case HISTOGRAMS_PATH: - return ByteBuffer.wrap(buildHistogramsOutput()); - case HEALTH_PATH: - case METRICS_PATH: - return ByteBuffer.wrap(buildMetricOutput(suffix)); - case VERSION_PATH: - return ByteBuffer.wrap(buildVersionOutput()); - default: - // XXX should possibly do something else here - return ByteBuffer.wrap(buildMetricOutput(suffix)); - } + return switch (suffix) { + case "" -> ByteBuffer.wrap(apiLinks(requestUri)); + case CONFIG_GENERATION_PATH -> ByteBuffer.wrap(config); + case HISTOGRAMS_PATH -> ByteBuffer.wrap(buildHistogramsOutput()); + case HEALTH_PATH, METRICS_PATH -> ByteBuffer.wrap(buildMetricOutput(suffix)); + case VERSION_PATH -> ByteBuffer.wrap(buildVersionOutput()); + default -> ByteBuffer.wrap(buildMetricOutput(suffix)); // XXX should possibly do something else here + }; } private byte[] apiLinks(URI requestUri) { @@ -228,13 +220,11 @@ public class StateHandler extends AbstractRequestHandler { for (Tuple tuple : collapseMetrics(metricSnapshot, consumer)) { ObjectNode jsonTuple = jsonMapper.createObjectNode(); jsonTuple.put("name", tuple.key); - if (tuple.val instanceof CountMetric) { - CountMetric count = (CountMetric)tuple.val; + if (tuple.val instanceof CountMetric count) { jsonTuple.set("values", jsonMapper.createObjectNode() .put("count", count.getCount()) .put("rate", sanitizeDouble(count.getCount() * 1000.0) / periodInMillis)); - } else if (tuple.val instanceof GaugeMetric) { - GaugeMetric gauge = (GaugeMetric) tuple.val; + } else if (tuple.val instanceof GaugeMetric gauge) { ObjectNode valueFields = jsonMapper.createObjectNode(); valueFields.put("average", sanitizeDouble(gauge.getAverage())) .put("sum", sanitizeDouble(gauge.getSum())) @@ -274,15 +264,11 @@ public class StateHandler extends AbstractRequestHandler { } private static List<Tuple> collapseMetrics(MetricSnapshot snapshot, String consumer) { - switch (consumer) { - case HEALTH_PATH: - return collapseHealthMetrics(snapshot); - case "all": // deprecated name - case METRICS_PATH: - return flattenAllMetrics(snapshot); - default: - throw new IllegalArgumentException("Unknown consumer '" + consumer + "'."); - } + return switch (consumer) { + case HEALTH_PATH -> collapseHealthMetrics(snapshot); + case "all", METRICS_PATH -> flattenAllMetrics(snapshot); // TODO: Remove "all" on Vespa 9 + default -> throw new IllegalArgumentException("Unknown consumer '" + consumer + "'."); + }; } private static List<Tuple> collapseHealthMetrics(MetricSnapshot snapshot) { @@ -291,8 +277,7 @@ public class StateHandler extends AbstractRequestHandler { for (Map.Entry<MetricDimensions, MetricSet> entry : snapshot) { MetricSet metricSet = entry.getValue(); MetricValue val = metricSet.get("serverTotalSuccessfulResponseLatency"); - if (val instanceof GaugeMetric) { - GaugeMetric gauge = (GaugeMetric)val; + if (val instanceof GaugeMetric gauge) { latencySeconds.add(GaugeMetric.newInstance(gauge.getLast() / 1000, gauge.getMax() / 1000, gauge.getMin() / 1000, diff --git a/container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java b/container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java index c469c90f6ab..2639239d23f 100644 --- a/container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java +++ b/container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java @@ -65,16 +65,14 @@ public class AccessLogEntry { return null; } - final Map<String, List<String>> newMapWithImmutableValues = mapValues( + Map<String, List<String>> newMapWithImmutableValues = mapValues( keyValues.entrySet(), valueList -> Collections.unmodifiableList(new ArrayList<>(valueList))); return Collections.unmodifiableMap(newMapWithImmutableValues); } } - private static <K, V1, V2> Map<K, V2> mapValues( - final Set<Map.Entry<K, V1>> entrySet, - final Function<V1, V2> valueConverter) { + private static <K, V1, V2> Map<K, V2> mapValues(Set<Map.Entry<K, V1>> entrySet, Function<V1, V2> valueConverter) { return entrySet.stream() .collect(toMap( entry -> entry.getKey(), diff --git a/container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java b/container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java index 23804613f4e..b4692a43890 100644 --- a/container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java +++ b/container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java @@ -14,6 +14,9 @@ import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.OptionalLong; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; import static java.util.Objects.requireNonNull; @@ -47,7 +50,7 @@ public class RequestLogEntry { private final Principal sslPrincipal; private final HitCounts hitCounts; private final TraceNode traceNode; - private final Map<String, Collection<String>> extraAttributes; + private final SortedMap<String, Collection<String>> extraAttributes; private RequestLogEntry(Builder builder) { this.connectionId = builder.connectionId; @@ -99,7 +102,7 @@ public class RequestLogEntry { public Optional<Principal> sslPrincipal() { return Optional.ofNullable(sslPrincipal); } public Optional<HitCounts> hitCounts() { return Optional.ofNullable(hitCounts); } public Optional<TraceNode> traceNode() { return Optional.ofNullable(traceNode); } - public Collection<String> extraAttributeKeys() { return Collections.unmodifiableCollection(extraAttributes.keySet()); } + public SortedSet<String> extraAttributeKeys() { return Collections.unmodifiableSortedSet((SortedSet<String>)extraAttributes.keySet()); } public Collection<String> extraAttributeValues(String key) { return Collections.unmodifiableCollection(extraAttributes.get(key)); } private static OptionalInt optionalInt(int value) { @@ -112,8 +115,8 @@ public class RequestLogEntry { return OptionalLong.of(value); } - private static Map<String, Collection<String>> copyExtraAttributes(Map<String, Collection<String>> extraAttributes) { - Map<String, Collection<String>> copy = new HashMap<>(); + private static SortedMap<String, Collection<String>> copyExtraAttributes(Map<String, Collection<String>> extraAttributes) { + SortedMap<String, Collection<String>> copy = new TreeMap<>(); extraAttributes.forEach((key, value) -> copy.put(key, new ArrayList<>(value))); return copy; } diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java index bf278981b69..6282e334409 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java @@ -9,7 +9,6 @@ import com.yahoo.jdisc.http.ssl.impl.DefaultConnectorSsl; import com.yahoo.security.tls.MixedMode; import com.yahoo.security.tls.TransportSecurityUtils; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; -import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; @@ -57,7 +56,6 @@ public class ConnectorFactory { // e.g. due to TLS configuration through environment variables. private static void runtimeConnectorConfigValidation(ConnectorConfig config) { validateProxyProtocolConfiguration(config); - validateSecureRedirectConfig(config); } private static void validateProxyProtocolConfiguration(ConnectorConfig config) { @@ -70,28 +68,15 @@ public class ConnectorFactory { } } - private static void validateSecureRedirectConfig(ConnectorConfig config) { - if (config.secureRedirect().enabled() && isSslEffectivelyEnabled(config)) { - throw new IllegalArgumentException("Secure redirect can only be enabled on connectors without HTTPS"); - } - } - public ConnectorConfig getConnectorConfig() { return connectorConfig; } public ServerConnector createConnector(final Metric metric, final Server server, JettyConnectionLogger connectionLogger, ConnectionMetricAggregator connectionMetricAggregator) { - ServerConnector connector = new JDiscServerConnector( + return new JDiscServerConnector( connectorConfig, metric, server, connectionLogger, connectionMetricAggregator, createConnectionFactories(metric).toArray(ConnectionFactory[]::new)); - connector.setPort(connectorConfig.listenPort()); - connector.setName(connectorConfig.name()); - connector.setAcceptQueueSize(connectorConfig.acceptQueueSize()); - connector.setReuseAddress(connectorConfig.reuseAddress()); - connector.setIdleTimeout(toMillis(connectorConfig.idleTimeout())); - connector.addBean(HttpCompliance.RFC7230); - return connector; } private List<ConnectionFactory> createConnectionFactories(Metric metric) { diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorSpecificContextHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorSpecificContextHandler.java new file mode 100644 index 00000000000..3554d371cf8 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorSpecificContextHandler.java @@ -0,0 +1,35 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.ContextHandler; + +import java.util.List; + +/** + * @author bjorncs + */ +class ConnectorSpecificContextHandler extends ContextHandler { + + private final JDiscServerConnector connector; + + ConnectorSpecificContextHandler(JDiscServerConnector c) { + this.connector = c; + List<String> allowedServerNames = c.connectorConfig().serverName().allowed(); + if (allowedServerNames.isEmpty()) { + setVirtualHosts(new String[]{"@%s".formatted(c.getName())}); + } else { + String[] virtualHosts = allowedServerNames.stream() + .map(name -> "%s@%s".formatted(name, c.getName())) + .toArray(String[]::new); + setVirtualHosts(virtualHosts); + } + } + + @Override + public boolean checkVirtualHost(Request req) { + // Accept health checks independently of virtual host configuration when connector matches + if (req.getRequestURI().equals("/status.html") && req.getHttpChannel().getConnector() == connector) return true; + return super.checkVirtualHost(req); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java index 72057563e36..d2dbfaa3514 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java @@ -54,17 +54,14 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh * @param delegateHandler the "real" request handler that this handler wraps * @param contentCharsetName name of the charset to use when interpreting the content data */ - public FormPostRequestHandler( - final RequestHandler delegateHandler, - final String contentCharsetName, - final boolean removeBody) { + public FormPostRequestHandler(RequestHandler delegateHandler, String contentCharsetName, boolean removeBody) { this.delegateHandler = Objects.requireNonNull(delegateHandler); this.contentCharsetName = Objects.requireNonNull(contentCharsetName); this.removeBody = removeBody; } @Override - public ContentChannel handleRequest(final Request request, final ResponseHandler responseHandler) { + public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) { Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request); Objects.requireNonNull(responseHandler, "responseHandler"); @@ -77,24 +74,24 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh } @Override - public void write(final ByteBuffer buf, final CompletionHandler completionHandler) { + public void write(ByteBuffer buf, CompletionHandler completionHandler) { assert buf.hasArray(); accumulatedRequestContent.write(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); completionHandler.completed(); } @Override - public void close(final CompletionHandler completionHandler) { - try (final ResourceReference ref = requestReference) { - final byte[] requestContentBytes = accumulatedRequestContent.toByteArray(); - final String content = new String(requestContentBytes, contentCharset); + public void close(CompletionHandler completionHandler) { + try (ResourceReference ref = requestReference) { + byte[] requestContentBytes = accumulatedRequestContent.toByteArray(); + String content = new String(requestContentBytes, contentCharset); completionHandler.completed(); - final Map<String, List<String>> parameterMap = parseFormParameters(content); + Map<String, List<String>> parameterMap = parseFormParameters(content); mergeParameters(parameterMap, request.parameters()); - final ContentChannel contentChannel = delegateHandler.handleRequest(request, responseHandler); + ContentChannel contentChannel = delegateHandler.handleRequest(request, responseHandler); if (contentChannel != null) { if (!removeBody) { - final ByteBuffer byteBuffer = ByteBuffer.wrap(requestContentBytes); + ByteBuffer byteBuffer = ByteBuffer.wrap(requestContentBytes); contentChannel.write(byteBuffer, NOOP_COMPLETION_HANDLER); } contentChannel.close(NOOP_COMPLETION_HANDLER); @@ -109,14 +106,10 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh * @return a valid Charset for the charset name (never returns null) * @throws RequestException if the charset name is invalid or unsupported */ - private static Charset getCharsetByName(final String charsetName) throws RequestException { + private static Charset getCharsetByName(String charsetName) throws RequestException { try { - final Charset charset = Charset.forName(charsetName); - if (charset == null) { - throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName); - } - return charset; - } catch (final IllegalCharsetNameException |UnsupportedCharsetException e) { + return Charset.forName(charsetName); + } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName, e); } } @@ -127,17 +120,17 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh * @param formContent raw form content data (body) * @return map of decoded parameters */ - private static Map<String, List<String>> parseFormParameters(final String formContent) { + private static Map<String, List<String>> parseFormParameters(String formContent) { if (formContent.isEmpty()) { return Collections.emptyMap(); } - final Map<String, List<String>> parameterMap = new HashMap<>(); - final String[] params = formContent.split("&"); - for (final String param : params) { - final String[] parts = param.split("="); - final String paramName = urlDecode(parts[0]); - final String paramValue = parts.length > 1 ? urlDecode(parts[1]) : ""; + Map<String, List<String>> parameterMap = new HashMap<>(); + String[] params = formContent.split("&"); + for (String param : params) { + String[] parts = param.split("="); + String paramName = urlDecode(parts[0]); + String paramValue = parts.length > 1 ? urlDecode(parts[1]) : ""; List<String> currentValues = parameterMap.get(paramName); if (currentValues == null) { currentValues = new LinkedList<>(); @@ -159,7 +152,7 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh // Regardless of the charset used to transfer the request body, // all percent-escaping of non-ascii characters should use UTF-8 code points. return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name()); - } catch (final UnsupportedEncodingException e) { + } catch (UnsupportedEncodingException e) { // Unfortunately, there is no URLDecoder.decode() method that takes a Charset, so we have to deal // with this exception. throw new IllegalStateException("Whoa, JVM doesn't support UTF-8 today.", e); @@ -172,11 +165,9 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh * @param source containing the parameters to copy into the destination * @param destination receiver of parameters, possibly already containing data */ - private static void mergeParameters( - final Map<String,List<String>> source, - final Map<String,List<String>> destination) { + private static void mergeParameters(Map<String,List<String>> source, Map<String,List<String>> destination) { for (Map.Entry<String, List<String>> entry : source.entrySet()) { - final List<String> destinationValues = destination.get(entry.getKey()); + List<String> destinationValues = destination.get(entry.getKey()); if (destinationValues != null) { destinationValues.addAll(entry.getValue()); } else { @@ -189,4 +180,5 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh public RequestHandler getDelegate() { return delegateHandler; } + } diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java index 22c5b2ebfdb..3fb81cb5352 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java @@ -3,6 +3,7 @@ package com.yahoo.jdisc.http.server.jetty; import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.ServerConfig; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.AsyncContextEvent; @@ -22,7 +23,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -52,13 +52,20 @@ class HttpResponseStatisticsCollector extends HandlerWrapper implements Graceful private final AtomicReference<FutureCallback> shutdown = new AtomicReference<>(); private final List<String> monitoringHandlerPaths; private final List<String> searchHandlerPaths; + private final Set<String> ignoredUserAgents; private final AtomicLong inFlight = new AtomicLong(); private final ConcurrentMap<StatusCodeMetric, LongAdder> statistics = new ConcurrentHashMap<>(); - HttpResponseStatisticsCollector(List<String> monitoringHandlerPaths, List<String> searchHandlerPaths) { + HttpResponseStatisticsCollector(ServerConfig.Metric cfg) { + this(cfg.monitoringHandlerPaths(), cfg.searchHandlerPaths(), cfg.ignoredUserAgents()); + } + + HttpResponseStatisticsCollector(List<String> monitoringHandlerPaths, List<String> searchHandlerPaths, + Collection<String> ignoredUserAgents) { this.monitoringHandlerPaths = monitoringHandlerPaths; this.searchHandlerPaths = searchHandlerPaths; + this.ignoredUserAgents = Set.copyOf(ignoredUserAgents); } private final AsyncListener completionWatcher = new AsyncListener() { @@ -108,12 +115,6 @@ class HttpResponseStatisticsCollector extends HandlerWrapper implements Graceful } } - void ignoreUserAgent(String agentName) { - ignoredUserAgents.add(agentName); - } - - private Set<String> ignoredUserAgents = new HashSet<>(); - private boolean shouldLogMetricsFor(Request request) { String agent = request.getHeader(HttpHeader.USER_AGENT.toString()); if (agent == null) return true; diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java index 79cdb8f67cf..49db22c3e38 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java @@ -3,6 +3,7 @@ package com.yahoo.jdisc.http.server.jetty; import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.http.ConnectorConfig; +import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.io.ConnectionStatistics; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Server; @@ -50,6 +51,12 @@ class JDiscServerConnector extends ServerConnector { } addBean(connectionLogger); addBean(connectionMetricAggregator); + setPort(config.listenPort()); + setName(config.name()); + setAcceptQueueSize(config.acceptQueueSize()); + setReuseAddress(config.reuseAddress()); + setIdleTimeout((long) (config.idleTimeout() * 1000)); + addBean(HttpCompliance.RFC7230); } @Override diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java index 96c5bac335b..775c903f5f8 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java @@ -18,7 +18,10 @@ import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.server.handler.gzip.GzipHttpOutputInterceptor; @@ -83,20 +86,13 @@ public class JettyHttpServer extends AbstractServerProvider { listenedPorts.add(connectorConfig.listenPort()); } - JDiscContext jDiscContext = new JDiscContext(filterBindings, - container, - janitor, - metric, - serverConfig); + JDiscContext jDiscContext = new JDiscContext(filterBindings, container, janitor, metric, serverConfig); ServletHolder jdiscServlet = new ServletHolder(new JDiscHttpServlet(jDiscContext)); List<JDiscServerConnector> connectors = Arrays.stream(server.getConnectors()) .map(JDiscServerConnector.class::cast) .collect(toList()); - - server.setHandler(getHandlerCollection(serverConfig, - connectors, - jdiscServlet)); + server.setHandler(createRootHandler(serverConfig, connectors, jdiscServlet)); this.metricsReporter = new ServerMetricReporter(metric, server); } @@ -136,46 +132,36 @@ public class JettyHttpServer extends AbstractServerProvider { } } - private HandlerCollection getHandlerCollection(ServerConfig serverConfig, - List<JDiscServerConnector> connectors, - ServletHolder jdiscServlet) { - ServletContextHandler servletContextHandler = createServletContextHandler(); - servletContextHandler.addServlet(jdiscServlet, "/*"); - - List<ConnectorConfig> connectorConfigs = connectors.stream().map(JDiscServerConnector::connectorConfig).collect(toList()); - var secureRedirectHandler = new SecuredRedirectHandler(connectorConfigs); - secureRedirectHandler.setHandler(servletContextHandler); - - var proxyHandler = new HealthCheckProxyHandler(connectors); - proxyHandler.setHandler(secureRedirectHandler); - - var authEnforcer = new TlsClientAuthenticationEnforcer(connectorConfigs); - authEnforcer.setHandler(proxyHandler); - - GzipHandler gzipHandler = newGzipHandler(serverConfig); - gzipHandler.setHandler(authEnforcer); - - HttpResponseStatisticsCollector statisticsCollector = - new HttpResponseStatisticsCollector(serverConfig.metric().monitoringHandlerPaths(), - serverConfig.metric().searchHandlerPaths()); - statisticsCollector.setHandler(gzipHandler); - for (String agent : serverConfig.metric().ignoredUserAgents()) { - statisticsCollector.ignoreUserAgent(agent); + private Handler createRootHandler( + ServerConfig serverCfg, List<JDiscServerConnector> connectors, ServletHolder jdiscServlet) { + HandlerCollection perConnectorHandlers = new ContextHandlerCollection(); + for (JDiscServerConnector connector : connectors) { + ConnectorConfig connectorCfg = connector.connectorConfig(); + List<Handler> connectorChain = new ArrayList<>(); + if (connectorCfg.tlsClientAuthEnforcer().enable()) { + connectorChain.add(newTlsClientAuthEnforcerHandler(connectorCfg)); + } + if (connectorCfg.healthCheckProxy().enable()) { + connectorChain.add(newHealthCheckProxyHandler(connectors)); + } else { + connectorChain.add(newServletHandler(jdiscServlet)); + } + ContextHandler connectorRoot = newConnectorContextHandler(connector); + addChainToRoot(connectorRoot, connectorChain); + perConnectorHandlers.addHandler(connectorRoot); } - - StatisticsHandler statisticsHandler = newStatisticsHandler(); - statisticsHandler.setHandler(statisticsCollector); - - HandlerCollection handlerCollection = new HandlerCollection(); - handlerCollection.setHandlers(new Handler[] { statisticsHandler }); - return handlerCollection; + StatisticsHandler root = newGenericStatisticsHandler(); + addChainToRoot(root, List.of( + newResponseStatisticsHandler(serverCfg), newGzipHandler(serverCfg), perConnectorHandlers)); + return root; } - private ServletContextHandler createServletContextHandler() { - ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); - servletContextHandler.setContextPath("/"); - servletContextHandler.setDisplayName(getDisplayName(listenedPorts)); - return servletContextHandler; + private static void addChainToRoot(Handler root, List<Handler> chain) { + Handler parent = root; + for (Handler h : chain) { + ((HandlerWrapper)parent).setHandler(h); + parent = h; + } } private static String getDisplayName(List<Integer> ports) { @@ -237,13 +223,37 @@ public class JettyHttpServer extends AbstractServerProvider { Server server() { return server; } - private StatisticsHandler newStatisticsHandler() { + private ServletContextHandler newServletHandler(ServletHolder servlet) { + var h = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + h.setContextPath("/"); + h.setDisplayName(getDisplayName(listenedPorts)); + h.addServlet(servlet, "/*"); + return h; + } + + private static ContextHandler newConnectorContextHandler(JDiscServerConnector c) { + return new ConnectorSpecificContextHandler(c); + } + + private static HealthCheckProxyHandler newHealthCheckProxyHandler(List<JDiscServerConnector> connectors) { + return new HealthCheckProxyHandler(connectors); + } + + private static TlsClientAuthenticationEnforcer newTlsClientAuthEnforcerHandler(ConnectorConfig cfg) { + return new TlsClientAuthenticationEnforcer(cfg.tlsClientAuthEnforcer()); + } + + private static HttpResponseStatisticsCollector newResponseStatisticsHandler(ServerConfig cfg) { + return new HttpResponseStatisticsCollector(cfg.metric()); + } + + private static StatisticsHandler newGenericStatisticsHandler() { StatisticsHandler statisticsHandler = new StatisticsHandler(); statisticsHandler.statsReset(); return statisticsHandler; } - private GzipHandler newGzipHandler(ServerConfig serverConfig) { + private static GzipHandler newGzipHandler(ServerConfig serverConfig) { GzipHandler gzipHandler = new GzipHandlerWithVaryHeaderFixed(); gzipHandler.setCompressionLevel(serverConfig.responseCompressionLevel()); gzipHandler.setInflateBufferSize(8 * 1024); diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java deleted file mode 100644 index e5dddf285ef..00000000000 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.http.ConnectorConfig; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.util.URIUtil; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.yahoo.jdisc.http.server.jetty.RequestUtils.getConnectorLocalPort; - -/** - * A secure redirect handler inspired by {@link org.eclipse.jetty.server.handler.SecuredRedirectHandler}. - * - * @author bjorncs - */ -class SecuredRedirectHandler extends HandlerWrapper { - - private static final String HEALTH_CHECK_PATH = "/status.html"; - - private final Map<Integer, Integer> redirectMap; - - SecuredRedirectHandler(List<ConnectorConfig> connectorConfigs) { - this.redirectMap = createRedirectMap(connectorConfigs); - } - - @Override - public void handle(String target, Request request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { - int localPort = getConnectorLocalPort(request); - if (!redirectMap.containsKey(localPort)) { - _handler.handle(target, request, servletRequest, servletResponse); - return; - } - servletResponse.setContentLength(0); - if (!servletRequest.getRequestURI().equals(HEALTH_CHECK_PATH)) { - servletResponse.sendRedirect( - URIUtil.newURI("https", request.getServerName(), redirectMap.get(localPort), request.getRequestURI(), request.getQueryString())); - } - request.setHandled(true); - } - - private static Map<Integer, Integer> createRedirectMap(List<ConnectorConfig> connectorConfigs) { - var redirectMap = new HashMap<Integer, Integer>(); - for (ConnectorConfig connectorConfig : connectorConfigs) { - if (connectorConfig.secureRedirect().enabled()) { - redirectMap.put(connectorConfig.listenPort(), connectorConfig.secureRedirect().port()); - } - } - return redirectMap; - } -} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java index ce949074bfa..b420aabc598 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java @@ -11,11 +11,6 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.yahoo.jdisc.http.server.jetty.RequestUtils.getConnectorLocalPort; /** * A Jetty handler that enforces TLS client authentication with configurable white list. @@ -24,10 +19,11 @@ import static com.yahoo.jdisc.http.server.jetty.RequestUtils.getConnectorLocalPo */ class TlsClientAuthenticationEnforcer extends HandlerWrapper { - private final Map<Integer, List<String>> portToWhitelistedPathsMapping; + private final ConnectorConfig.TlsClientAuthEnforcer cfg; - TlsClientAuthenticationEnforcer(List<ConnectorConfig> connectorConfigs) { - portToWhitelistedPathsMapping = createWhitelistMapping(connectorConfigs); + TlsClientAuthenticationEnforcer(ConnectorConfig.TlsClientAuthEnforcer cfg) { + if (!cfg.enable()) throw new IllegalArgumentException(); + this.cfg = cfg; } @Override @@ -44,36 +40,11 @@ class TlsClientAuthenticationEnforcer extends HandlerWrapper { } } - private static Map<Integer, List<String>> createWhitelistMapping(List<ConnectorConfig> connectorConfigs) { - var mapping = new HashMap<Integer, List<String>>(); - for (ConnectorConfig connectorConfig : connectorConfigs) { - var enforcerConfig = connectorConfig.tlsClientAuthEnforcer(); - if (enforcerConfig.enable()) { - mapping.put(connectorConfig.listenPort(), enforcerConfig.pathWhitelist()); - } - } - return mapping; - } - - private boolean isRequest(Request request) { - return request.getDispatcherType() == DispatcherType.REQUEST; - } + private boolean isRequest(Request request) { return request.getDispatcherType() == DispatcherType.REQUEST; } private boolean isRequestToWhitelistedBinding(Request jettyRequest) { - int localPort = getConnectorLocalPort(jettyRequest); - List<String> whiteListedPaths = getWhitelistedPathsForPort(localPort); - if (whiteListedPaths == null) { - return true; // enforcer not enabled - } // Note: Same path definition as HttpRequestFactory.getUri() - return whiteListedPaths.contains(jettyRequest.getRequestURI()); - } - - private List<String> getWhitelistedPathsForPort(int localPort) { - if (portToWhitelistedPathsMapping.containsKey(0) && portToWhitelistedPathsMapping.size() == 1) { - return portToWhitelistedPathsMapping.get(0); // for unit tests which uses 0 for listen port - } - return portToWhitelistedPathsMapping.get(localPort); + return cfg.pathWhitelist().contains(jettyRequest.getRequestURI()); } private boolean isClientAuthenticated(HttpServletRequest servletRequest) { diff --git a/container-core/src/main/java/com/yahoo/processing/handler/AbstractProcessingHandler.java b/container-core/src/main/java/com/yahoo/processing/handler/AbstractProcessingHandler.java index 2c15c994bb7..f4db1c5085a 100644 --- a/container-core/src/main/java/com/yahoo/processing/handler/AbstractProcessingHandler.java +++ b/container-core/src/main/java/com/yahoo/processing/handler/AbstractProcessingHandler.java @@ -187,7 +187,7 @@ public abstract class AbstractProcessingHandler<COMPONENT extends Processor> ext private void populate(String prefixName,Map<String,?> parameters,Properties properties) { CompoundName prefix = new CompoundName(prefixName); for (Map.Entry<String,?> entry : parameters.entrySet()) - properties.set(prefix.append(entry.getKey()),entry.getValue()); + properties.set(prefix.append(entry.getKey()), entry.getValue()); } private static class FreezeListener implements Runnable, ResponseReceiver { diff --git a/container-core/src/main/java/com/yahoo/processing/request/CompoundName.java b/container-core/src/main/java/com/yahoo/processing/request/CompoundName.java index 5e52f8d8b37..6af4811fa1b 100644 --- a/container-core/src/main/java/com/yahoo/processing/request/CompoundName.java +++ b/container-core/src/main/java/com/yahoo/processing/request/CompoundName.java @@ -175,6 +175,8 @@ public final class CompoundName { if (compounds.size() < n) throw new IllegalArgumentException("Asked for the first " + n + " components but '" + this + "' only have " + compounds.size() + " components."); + if (compounds.size() == n) return this; + if (compounds.size() == 0) return empty; return new CompoundName(compounds.subList(0, n)); } diff --git a/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def index 1f4763d32a7..ecbc451ead1 100644 --- a/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def +++ b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def @@ -116,12 +116,6 @@ proxyProtocol.enabled bool default=false # Allow https in parallel with proxy protocol proxyProtocol.mixedMode bool default=false -# Redirect all requests to https port -secureRedirect.enabled bool default=false - -# Target port for redirect -secureRedirect.port int default=443 - # Maximum number of request per connection before server marks connections as non-persistent. Set to '0' to disable. maxRequestsPerConnection int default=0 @@ -138,3 +132,5 @@ http2.maxConcurrentStreams int default=4096 # Override the default server name when authority is missing from request. serverName.fallback string default="" +# The list of accepted server names. Empty list to accept any. Elements follows format of 'serverName.default'. +serverName.allowed[] string diff --git a/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java b/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java index e98f8cac276..749033cd1bf 100644 --- a/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java +++ b/container-core/src/test/java/com/yahoo/container/handler/LogReaderTest.java @@ -3,8 +3,8 @@ package com.yahoo.container.handler; import com.yahoo.compress.ZstdCompressor; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.api.io.TempDir; import java.io.ByteArrayOutputStream; @@ -30,7 +30,8 @@ public class LogReaderTest { private static final String logv11 = "3600.2\tnode1.com\t5480\tcontainer\tstdout\tinfo\tfourth\n"; private static final String logv = "90000.1\tnode1.com\t5480\tcontainer\tstdout\tinfo\tlast\n"; - private static final String log100 = "0.2\tnode2.com\t5480\tcontainer\tstdout\tinfo\tsecond\n"; + private static final String log100a = "0.2\tnode2.com\t5480\tcontainer\tstdout\tinfo\tsecond\n"; + private static final String log100b = "0.15\tnode2.com\t5480\tcontainer\tstdout\tinfo\tfirst\n"; private static final String log101 = "0.1\tnode2.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n"; private static final String log110 = "3600.1\tnode1.com\t5480\tcontainer\tstderr\twarning\tthird\n"; private static final String log200 = "86400.1\tnode2.com\t5480\tcontainer\tstderr\twarning\tjava.lang.NullPointerException\\n\\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\\n\\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)\n"; @@ -41,27 +42,37 @@ public class LogReaderTest { // Log archive paths and file names indicate what hour they contain logs for, with the start of that hour. // Multiple entries may exist for each hour. Files.createDirectories(logDirectory.resolve("1970/01/01")); - Files.write(logDirectory.resolve("1970/01/01/00-0.gz"), compress1(log100)); - Files.write(logDirectory.resolve("1970/01/01/00-1"), log101.getBytes(UTF_8)); + // Files may contain out-of-order entries. + Files.write(logDirectory.resolve("1970/01/01/00-0.gz"), compress1(log100a + log100b)); + Files.writeString(logDirectory.resolve("1970/01/01/00-1"), log101); Files.write(logDirectory.resolve("1970/01/01/01-0.zst"), compress2(log110)); Files.createDirectories(logDirectory.resolve("1970/01/02")); - Files.write(logDirectory.resolve("1970/01/02/00-0"), log200.getBytes(UTF_8)); + Files.writeString(logDirectory.resolve("1970/01/02/00-0"), log200); // Vespa log file names are the second-truncated timestamp of the last entry. // The current log file has no timestamp suffix. - Files.write(logDirectory.resolve("vespa.log-1970-01-01.01-00-00"), logv11.getBytes(UTF_8)); - Files.write(logDirectory.resolve("vespa.log"), logv.getBytes(UTF_8)); + Files.writeString(logDirectory.resolve("vespa.log-1970-01-01.01-00-00"), logv11); + Files.writeString(logDirectory.resolve("vespa.log"), logv); } - @Disabled + private static boolean hasZstdcat() { + try { + return new ProcessBuilder("zstdcat", "--version").start().waitFor() == 0; + } + catch (Exception e) { + return false; + } + } + + @EnabledIf("hasZstdcat") @Test void testThatLogsOutsideRangeAreExcluded() { ByteArrayOutputStream baos = new ByteArrayOutputStream(); LogReader logReader = new LogReader(logDirectory, Pattern.compile(".*")); logReader.writeLogs(baos, Instant.ofEpochMilli(150), Instant.ofEpochMilli(3601050), 100, Optional.empty()); - assertEquals(log100 + logv11 + log110, baos.toString(UTF_8)); + assertEquals(log100b + log100a + logv11 + log110, baos.toString(UTF_8)); } @Test @@ -73,14 +84,14 @@ public class LogReaderTest { assertEquals(log101 + logv11, baos.toString(UTF_8)); } - @Disabled // TODO: zts log line missing on Mac + @EnabledIf("hasZstdcat") @Test void testZippedStreaming() { ByteArrayOutputStream zippedBaos = new ByteArrayOutputStream(); LogReader logReader = new LogReader(logDirectory, Pattern.compile(".*")); logReader.writeLogs(zippedBaos, Instant.EPOCH, Instant.EPOCH.plus(Duration.ofDays(2)), 100, Optional.empty()); - assertEquals(log101 + log100 + logv11 + log110 + log200 + logv, zippedBaos.toString(UTF_8)); + assertEquals(log101 + log100b + log100a + logv11 + log110 + log200 + logv, zippedBaos.toString(UTF_8)); } @Test @@ -89,7 +100,7 @@ public class LogReaderTest { LogReader logReader = new LogReader(logDirectory, Pattern.compile(".*")); logReader.writeLogs(baos, Instant.EPOCH, Instant.EPOCH.plus(Duration.ofDays(2)), 100, Optional.of("node2.com")); - assertEquals(log101 + log100 + log200, baos.toString(UTF_8)); + assertEquals(log101 + log100b + log100a + log200, baos.toString(UTF_8)); } @Test @@ -98,7 +109,7 @@ public class LogReaderTest { LogReader logReader = new LogReader(logDirectory, Pattern.compile(".*")); logReader.writeLogs(baos, Instant.EPOCH, Instant.EPOCH.plus(Duration.ofDays(2)), 2, Optional.of("node2.com")); - assertEquals(log101 + log100, baos.toString(UTF_8)); + assertEquals(log101 + log100b, baos.toString(UTF_8)); } private byte[] compress1(String input) throws IOException { diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java index 1f65bc4f582..165659389ec 100644 --- a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; +import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -38,7 +39,7 @@ public class HttpResponseStatisticsCollectorTest { private Connector connector; private List<String> monitoringPaths = List.of("/status.html"); private List<String> searchPaths = List.of("/search"); - private HttpResponseStatisticsCollector collector = new HttpResponseStatisticsCollector(monitoringPaths, searchPaths); + private HttpResponseStatisticsCollector collector = new HttpResponseStatisticsCollector(monitoringPaths, searchPaths, Set.of()); private int httpResponseCode = 500; @Test diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java index 2c5d36bd776..318067ac634 100644 --- a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java @@ -743,7 +743,7 @@ public class HttpServerTest { } @Test - void requestThatFallbackServerNameCanBeOverridden() throws Exception { + void fallbackServerNameCanBeOverridden() throws Exception { String fallbackHostname = "myhostname"; JettyTestDriver driver = JettyTestDriver.newConfiguredInstance( new UriRequestHandler(), @@ -752,13 +752,29 @@ public class HttpServerTest { .serverName(new ConnectorConfig.ServerName.Builder().fallback(fallbackHostname))); int listenPort = driver.server().getListenPort(); HttpGet req = new HttpGet("http://localhost:" + listenPort + "/"); - req.addHeader("Host", null); + req.setHeader("Host", null); driver.client().execute(req) .expectStatusCode(is(OK)) .expectContent(containsString("http://" + fallbackHostname + ":" + listenPort + "/")); assertTrue(driver.close()); } + @Test + void acceptedServerNamesCanBeRestricted() throws Exception { + String requiredServerName = "myhostname"; + JettyTestDriver driver = JettyTestDriver.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .serverName(new ConnectorConfig.ServerName.Builder().allowed(requiredServerName))); + int listenPort = driver.server().getListenPort(); + HttpGet req = new HttpGet("http://localhost:" + listenPort + "/"); + req.setHeader("Host", requiredServerName); + driver.client().execute(req).expectStatusCode(is(OK)); + driver.client().get("/").expectStatusCode(is(NOT_FOUND)); + assertTrue(driver.close()); + } + private static JettyTestDriver createSslWithTlsClientAuthenticationEnforcer(Path certificateFile, Path privateKeyFile) { ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder() .tlsClientAuthEnforcer( diff --git a/container-core/src/test/java/com/yahoo/metrics/simple/BucketTest.java b/container-core/src/test/java/com/yahoo/metrics/simple/BucketTest.java index 11cc8b86a09..75d1c37c5c1 100644 --- a/container-core/src/test/java/com/yahoo/metrics/simple/BucketTest.java +++ b/container-core/src/test/java/com/yahoo/metrics/simple/BucketTest.java @@ -23,9 +23,10 @@ import com.yahoo.metrics.simple.UntypedMetric.AssumedType; * Functional tests for the value buckets, as implemented in the class Bucket, * and by extension the value store itself, UntypedValue. * - * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + * @author Steinar Knutsen */ public class BucketTest { + private Bucket bucket; @BeforeEach @@ -55,23 +56,12 @@ public class BucketTest { for (Entry<Identifier, UntypedMetric> x : bucket.entrySet()) { String metricName = x.getKey().getName(); switch (metricName) { - case "nalle": - ++nalle; - break; - case "nalle_0": - ++nalle0; - break; - case "nalle_1": - ++nalle1; - break; - case "nalle_2": - ++nalle2; - break; - case "nalle_3": - ++nalle3; - break; - default: - throw new IllegalStateException(); + case "nalle" -> ++nalle; + case "nalle_0" -> ++nalle0; + case "nalle_1" -> ++nalle1; + case "nalle_2" -> ++nalle2; + case "nalle_3" -> ++nalle3; + default -> throw new IllegalStateException(); } } assertEquals(4, nalle); diff --git a/container-core/src/test/java/com/yahoo/metrics/simple/CounterTest.java b/container-core/src/test/java/com/yahoo/metrics/simple/CounterTest.java index 45a76078619..074c0c7b2e5 100644 --- a/container-core/src/test/java/com/yahoo/metrics/simple/CounterTest.java +++ b/container-core/src/test/java/com/yahoo/metrics/simple/CounterTest.java @@ -33,7 +33,7 @@ public class CounterTest { } @Test - final void testAdd() throws InterruptedException { + final void testAdd() { final String metricName = "unitTestCounter"; Counter c = receiver.declareCounter(metricName); c.add(); @@ -47,7 +47,7 @@ public class CounterTest { } @Test - final void testAddLong() throws InterruptedException { + final void testAddLong() { final String metricName = "unitTestCounter"; Counter c = receiver.declareCounter(metricName); final long twoToThePowerOfFourtyeight = 65536L * 65536L * 65536L; @@ -62,7 +62,7 @@ public class CounterTest { } @Test - final void testAddPoint() throws InterruptedException { + final void testAddPoint() { final String metricName = "unitTestCounter"; Point p = receiver.pointBuilder().set("x", 2L).set("y", 3.0d).set("z", "5").build(); Counter c = receiver.declareCounter(metricName, p); @@ -77,7 +77,7 @@ public class CounterTest { } @Test - final void testAddLongPoint() throws InterruptedException { + final void testAddLongPoint() { final String metricName = "unitTestCounter"; Point p = receiver.pointBuilder().set("x", 2L).set("y", 3.0d).set("z", "5").build(); Counter c = receiver.declareCounter(metricName, p); diff --git a/container-core/src/test/java/com/yahoo/metrics/simple/MetricsTest.java b/container-core/src/test/java/com/yahoo/metrics/simple/MetricsTest.java index f64998f0be4..dd949627f30 100644 --- a/container-core/src/test/java/com/yahoo/metrics/simple/MetricsTest.java +++ b/container-core/src/test/java/com/yahoo/metrics/simple/MetricsTest.java @@ -17,9 +17,10 @@ import com.yahoo.metrics.simple.jdisc.SimpleMetricConsumer; /** * Functional test for simple metric implementation. * - * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + * @author Steinar Knutsen */ public class MetricsTest extends UnitTestSetup { + SimpleMetricConsumer metricApi; @BeforeEach @@ -36,7 +37,7 @@ public class MetricsTest extends UnitTestSetup { @Test final void smokeTest() throws InterruptedException { final String metricName = "testMetric"; - metricApi.set(metricName, Double.valueOf(1.0d), null); + metricApi.set(metricName, 1.0d, null); updater.gotData.await(10, TimeUnit.SECONDS); Bucket s = getUpdatedSnapshot(); Collection<Entry<Point, UntypedMetric>> values = s.getValuesForMetric(metricName); diff --git a/container-core/src/test/java/com/yahoo/metrics/simple/jdisc/SnapshotConverterTest.java b/container-core/src/test/java/com/yahoo/metrics/simple/jdisc/SnapshotConverterTest.java index 7981e5904f3..1d5cf264964 100644 --- a/container-core/src/test/java/com/yahoo/metrics/simple/jdisc/SnapshotConverterTest.java +++ b/container-core/src/test/java/com/yahoo/metrics/simple/jdisc/SnapshotConverterTest.java @@ -20,6 +20,7 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /** * @author bratseth @@ -50,7 +51,7 @@ public class SnapshotConverterTest { for (Map.Entry<MetricDimensions, MetricSet> entry : snapshot) { for (Map.Entry<String, String> dv : entry.getKey()) { - assertTrue(false); + fail(); } int cnt = 0; @@ -67,7 +68,7 @@ public class SnapshotConverterTest { assertEquals(42.25, ((GaugeMetric) mv.getValue()).getLast(), 0.001); assertEquals(1, ((GaugeMetric) mv.getValue()).getCount()); } else { - assertTrue(false); + fail(); } } assertEquals(3, cnt); diff --git a/container-disc/pom.xml b/container-disc/pom.xml index 273270b208b..173979fbe81 100644 --- a/container-disc/pom.xml +++ b/container-disc/pom.xml @@ -195,11 +195,11 @@ hosted-zone-api-jar-with-dependencies.jar, container-apache-http-client-bundle-jar-with-dependencies.jar, security-utils.jar, - bcprov-jdk15on-${bouncycastle.version}.jar, <!-- Used by security-utils --> + bcprov-jdk18on-${bouncycastle.version}.jar, <!-- Used by security-utils --> + bcpkix-jdk18on-${bouncycastle.version}.jar, <!-- Used by security-utils --> + bcutil-jdk18on-${bouncycastle.version}.jar, <!-- Used by security-utils --> <!-- END Bundles needed to retrieve config, or used by container-disc --> - bcpkix-jdk15on-${bouncycastle.version}.jar, <!-- Used by security-utils --> - jackson-annotations-${jackson2.version}.jar, jackson-core-${jackson2.version}.jar, jackson-databind-${jackson-databind.version}.jar, diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java b/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java index 7ea91726673..625bd87c2db 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/ConfiguredApplication.java @@ -366,6 +366,12 @@ public final class ConfiguredApplication implements Application { synchronized (monitor) { Set<ServerProvider> serversToClose = createIdentityHashSet(startedServers); serversToClose.removeAll(currentServers); + for (ServerProvider server : currentServers) { + if ( ! startedServers.contains(server) && server.isMultiplexed()) { + server.start(); + startedServers.add(server); + } + } if (serversToClose.size() > 0) { log.info(String.format("Closing %d server instances", serversToClose.size())); for (ServerProvider server : serversToClose) { @@ -374,7 +380,7 @@ public final class ConfiguredApplication implements Application { } } for (ServerProvider server : currentServers) { - if (!startedServers.contains(server)) { + if ( ! startedServers.contains(server)) { server.start(); startedServers.add(server); } diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/SystemInfoProvider.java b/container-disc/src/main/java/com/yahoo/container/jdisc/SystemInfoProvider.java index d98ea0ffc25..d8298491944 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/SystemInfoProvider.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/SystemInfoProvider.java @@ -36,7 +36,7 @@ public class SystemInfoProvider extends AbstractComponent implements Provider<Sy applicationIdConfig.instance()), new Zone(Environment.valueOf(csConfig.environment()), csConfig.region()), new Cloud(csConfig.cloud()), - new Cluster(ciConfig.nodeCount(), ciConfig.nodeIndices()), + new Cluster(ciConfig.clusterId(), ciConfig.nodeCount(), ciConfig.nodeIndices()), new Node(qrConfig.nodeIndex())); } diff --git a/container-messagebus/src/main/java/com/yahoo/messagebus/jdisc/MbusServer.java b/container-messagebus/src/main/java/com/yahoo/messagebus/jdisc/MbusServer.java index 8f21cb227d8..3cf3b4bf8b0 100644 --- a/container-messagebus/src/main/java/com/yahoo/messagebus/jdisc/MbusServer.java +++ b/container-messagebus/src/main/java/com/yahoo/messagebus/jdisc/MbusServer.java @@ -60,6 +60,11 @@ public final class MbusServer extends AbstractResource implements ServerProvider } @Override + public boolean isMultiplexed() { + return true; + } + + @Override protected void destroy() { log.log(Level.FINE, "Destroying message bus server."); runState.set(State.STOPPED); diff --git a/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastHit.java b/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastHit.java index a094be943a2..23cdff15ad9 100644 --- a/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastHit.java +++ b/container-search/src/main/java/com/yahoo/prelude/fastsearch/FastHit.java @@ -4,6 +4,7 @@ package com.yahoo.prelude.fastsearch; import com.yahoo.data.access.ObjectTraverser; import com.yahoo.document.GlobalId; import com.yahoo.net.URI; +import com.yahoo.search.dispatch.LeanHit; import com.yahoo.search.query.Sorting; import com.yahoo.search.result.FeatureData; import com.yahoo.search.result.Hit; @@ -11,7 +12,6 @@ import com.yahoo.search.result.Relevance; import com.yahoo.data.access.Inspector; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -31,18 +31,18 @@ import java.util.function.BiConsumer; */ public class FastHit extends Hit { - private static final byte [] emptyGID = new byte[GlobalId.LENGTH]; + private static final byte[] emptyGID = new byte[GlobalId.LENGTH]; /** The index of the content node this hit originated at */ - private int distributionKey = 0; + private int distributionKey; /** The local identifier of the content store for this hit on the node it originated at */ - private int partId; + private final int partId; /** The global id of this document in the backend node which produced it */ - private byte [] globalId; + private byte[] globalId; private transient byte[] sortData = null; - // TODO I suspect this one can be dropped. + // TODO: I suspect this one can be dropped. private transient Sorting sortDataSorting = null; /** @@ -73,10 +73,10 @@ public class FastHit extends Hit { distributionKey = 0; } - public FastHit(byte [] gid, double relevance, int partId, int distributionKey) { + public FastHit(byte[] gid, double relevance, int partId, int distributionKey) { this(gid, new Relevance(relevance), partId, distributionKey); } - public FastHit(byte [] gid, Relevance relevance, int partId, int distributionKey) { + public FastHit(byte[] gid, Relevance relevance, int partId, int distributionKey) { super(relevance); this.globalId = gid; this.partId = partId; @@ -109,8 +109,8 @@ public class FastHit extends Hit { */ @Override public URI getId() { - URI uri = super.getId(); - if (uri != null) return uri; + URI id = super.getId(); + if (id != null) return id; // Fallback to index:[source]/[partid]/[id] StringBuilder sb = new StringBuilder(64); @@ -129,16 +129,6 @@ public class FastHit extends Hit { public int getPartId() { return partId; } - /** - * Sets the part id number, which specifies the node where this hit is - * found. The row count is used to decode the part id into a column and a - * row number: the number of n least significant bits required to hold the - * highest row number are the row bits, the rest are column bits. - * - * Note: Remove partId when all dispatching happens from the container dispatcher, not fdispatch - */ - public void setPartId(int partId) { this.partId = partId; } - /** Returns the index of the node this hit originated at */ public int getDistributionKey() { return distributionKey; } @@ -167,17 +157,7 @@ public class FastHit extends Hit { if (!left.hasSortData(sorting) || !right.hasSortData(sorting)) { return 0; // cannot sort } - int i = Arrays.mismatch(left.sortData, right.sortData); - if (i < 0) { - return 0; - } - int max = Integer.min(left.sortData.length, right.sortData.length); - if (i >= max) { - return left.sortData.length - right.sortData.length; - } - int vl = (int) left.sortData[i] & 0xFF; - int vr = (int) right.sortData[i] & 0xFF; - return vl - vr; + return LeanHit.compareData(left.sortData, right.sortData); } /** For internal use */ @@ -188,11 +168,6 @@ public class FastHit extends Hit { summaries.add(0, new SummaryData(this, docsumDef, value, 1 + summaries.size())); } - /** Returns the raw summary data available in this as an unmodifiable list */ - public List<SummaryData> summaryData() { - return Collections.unmodifiableList(summaries); - } - /** * Returns values for the features listed in * <a href="https://docs.vespa.ai/en/reference/schema-reference.html#summary-features">summary-features</a> diff --git a/container-search/src/main/java/com/yahoo/prelude/fastsearch/SortDataHitSorter.java b/container-search/src/main/java/com/yahoo/prelude/fastsearch/SortDataHitSorter.java index 6067f85df9b..722e7155dc8 100644 --- a/container-search/src/main/java/com/yahoo/prelude/fastsearch/SortDataHitSorter.java +++ b/container-search/src/main/java/com/yahoo/prelude/fastsearch/SortDataHitSorter.java @@ -5,7 +5,6 @@ import com.yahoo.search.query.Sorting; import com.yahoo.search.result.Hit; import com.yahoo.search.result.HitGroup; -import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -18,15 +17,14 @@ public class SortDataHitSorter { return; } var fallbackComparator = fallbackOrderer.getComparator(); - Collections.sort(hits, getComparator(sorting, fallbackComparator)); + hits.sort(getComparator(sorting, fallbackComparator)); } public static boolean isSortable(Hit hit, Sorting sorting) { if (sorting == null) { return false; } - if (hit instanceof FastHit) { - var fhit = (FastHit) hit; + if (hit instanceof FastHit fhit) { return fhit.hasSortData(sorting); } else { return false; @@ -42,20 +40,14 @@ public class SortDataHitSorter { } private static int compareTwo(Hit left, Hit right, Sorting sorting) { - if (left == null || right == null || !(left instanceof FastHit) || !(right instanceof FastHit)) { - return 0; - } - FastHit fl = (FastHit) left; - FastHit fr = (FastHit) right; + if (!(left instanceof FastHit fl) || !(right instanceof FastHit fr)) return 0; return FastHit.compareSortData(fl, fr, sorting); } private static int compareWithFallback(Hit left, Hit right, Sorting sorting, Comparator<Hit> fallback) { - if (left == null || right == null || !(left instanceof FastHit) || !(right instanceof FastHit)) { + if (!(left instanceof FastHit fl) || !(right instanceof FastHit fr)) { return fallback.compare(left, right); } - FastHit fl = (FastHit) left; - FastHit fr = (FastHit) right; if (fl.hasSortData(sorting) && fr.hasSortData(sorting)) { return FastHit.compareSortData(fl, fr, sorting); } else { diff --git a/container-search/src/main/java/com/yahoo/prelude/query/Item.java b/container-search/src/main/java/com/yahoo/prelude/query/Item.java index 7f421832d5f..8c154072a42 100644 --- a/container-search/src/main/java/com/yahoo/prelude/query/Item.java +++ b/container-search/src/main/java/com/yahoo/prelude/query/Item.java @@ -1,7 +1,6 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.prelude.query; - import com.yahoo.collections.CopyOnWriteHashMap; import com.yahoo.compress.IntegerCompressor; import com.yahoo.language.Language; diff --git a/container-search/src/main/java/com/yahoo/prelude/query/RegExpItem.java b/container-search/src/main/java/com/yahoo/prelude/query/RegExpItem.java index 29cf7803d61..79fbeb99119 100644 --- a/container-search/src/main/java/com/yahoo/prelude/query/RegExpItem.java +++ b/container-search/src/main/java/com/yahoo/prelude/query/RegExpItem.java @@ -2,6 +2,7 @@ package com.yahoo.prelude.query; import java.nio.ByteBuffer; +import java.util.Objects; import java.util.regex.Pattern; /** @@ -70,43 +71,26 @@ public class RegExpItem extends TermItem { putString(getIndexedString(), buffer); } + public Pattern getRegexp() { return regexp; } + @Override public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("RegExpItem [expression=").append(expression).append("]"); - return builder.toString(); + return "RegExpItem [expression=" + expression + "]"; } @Override public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((expression == null) ? 0 : expression.hashCode()); - return result; + return Objects.hash(super.hashCode(), expression); } @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - RegExpItem other = (RegExpItem) obj; - if (expression == null) { - if (other.expression != null) { - return false; - } - } else if (!expression.equals(other.expression)) { - return false; - } - return true; - } + public boolean equals(Object o) { + if (this == o) return true; + if ( ! super.equals(o)) return false; + if (getClass() != o.getClass()) return false; - public Pattern getRegexp() { return regexp; } + RegExpItem other = (RegExpItem)o; + return Objects.equals(this.expression, other.expression); + } } diff --git a/container-search/src/main/java/com/yahoo/prelude/searcher/ValidateSortingSearcher.java b/container-search/src/main/java/com/yahoo/prelude/searcher/ValidateSortingSearcher.java index 6ecf9cd906f..47125d198e1 100644 --- a/container-search/src/main/java/com/yahoo/prelude/searcher/ValidateSortingSearcher.java +++ b/container-search/src/main/java/com/yahoo/prelude/searcher/ValidateSortingSearcher.java @@ -20,7 +20,6 @@ import java.util.Map; import static com.yahoo.prelude.querytransform.NormalizingSearcher.ACCENT_REMOVAL; - /** * Check sorting specification makes sense to the search cluster before * passing it on to the backend. @@ -35,6 +34,13 @@ public class ValidateSortingSearcher extends Searcher { private String clusterName = ""; private final QrSearchersConfig.Searchcluster.Indexingmode.Enum indexingMode; + public ValidateSortingSearcher(QrSearchersConfig qrsConfig, ClusterConfig clusterConfig, + AttributesConfig attributesConfig) { + initAttributeNames(attributesConfig); + setClusterName(qrsConfig.searchcluster(clusterConfig.clusterId()).name()); + indexingMode = qrsConfig.searchcluster(clusterConfig.clusterId()).indexingmode(); + } + public String getClusterName() { return clusterName; } @@ -63,14 +69,6 @@ public class ValidateSortingSearcher extends Searcher { setAttributeNames(attributes); } - public ValidateSortingSearcher(QrSearchersConfig qrsConfig, ClusterConfig clusterConfig, - AttributesConfig attributesConfig) - { - initAttributeNames(attributesConfig); - setClusterName(qrsConfig.searchcluster(clusterConfig.clusterId()).name()); - indexingMode = qrsConfig.searchcluster(clusterConfig.clusterId()).indexingmode(); - } - @Override public Result search(Query query, Execution execution) { if (indexingMode != QrSearchersConfig.Searchcluster.Indexingmode.STREAMING) { @@ -157,8 +155,7 @@ public class ValidateSortingSearcher extends Searcher { } } } - if (f.getSorter() instanceof Sorting.UcaSorter) { - Sorting.UcaSorter sorter = (Sorting.UcaSorter) f.getSorter(); + if (f.getSorter() instanceof Sorting.UcaSorter sorter) { String locale = sorter.getLocale(); if (locale == null || locale.isEmpty()) { diff --git a/container-search/src/main/java/com/yahoo/search/dispatch/InterleavedSearchInvoker.java b/container-search/src/main/java/com/yahoo/search/dispatch/InterleavedSearchInvoker.java index b4f04da5986..39be91cf3e8 100644 --- a/container-search/src/main/java/com/yahoo/search/dispatch/InterleavedSearchInvoker.java +++ b/container-search/src/main/java/com/yahoo/search/dispatch/InterleavedSearchInvoker.java @@ -210,17 +210,17 @@ public class InterleavedSearchInvoker extends SearchInvoker implements ResponseM indexPartial++; } } - while ((indexCurrent < current.size()) && (merged.size() < needed)) { - LeanHit currentHit = current.get(indexCurrent++); - merged.add(currentHit); - } - while ((indexPartial < partial.size()) && (merged.size() < needed)) { - LeanHit incomingHit = partial.get(indexPartial++); - merged.add(incomingHit); - } + appendRemainingIfNeeded(merged, needed, current, indexCurrent); + appendRemainingIfNeeded(merged, needed, partial, indexPartial); return merged; } + private void appendRemainingIfNeeded(List<LeanHit> merged, int needed, List<LeanHit> hits, int index) { + while ((index < hits.size()) && (merged.size() < needed)) { + merged.add(hits.get(index++)); + } + } + private void ejectInvoker(SearchInvoker invoker) { invokers.remove(invoker); invoker.release(); diff --git a/container-search/src/main/java/com/yahoo/search/dispatch/LeanHit.java b/container-search/src/main/java/com/yahoo/search/dispatch/LeanHit.java index bd0415fa449..f80cebe90c4 100644 --- a/container-search/src/main/java/com/yahoo/search/dispatch/LeanHit.java +++ b/container-search/src/main/java/com/yahoo/search/dispatch/LeanHit.java @@ -51,7 +51,7 @@ public class LeanHit implements Comparable<LeanHit> { return (res != 0) ? res : compareData(gid, o.gid); } - private static int compareData(byte[] left, byte[] right) { + public static int compareData(byte[] left, byte[] right) { int i = Arrays.mismatch(left, right); if (i < 0) { return 0; diff --git a/container-search/src/main/java/com/yahoo/search/dispatch/rpc/MapConverter.java b/container-search/src/main/java/com/yahoo/search/dispatch/rpc/MapConverter.java index a71ec5d8345..7e54afcc070 100644 --- a/container-search/src/main/java/com/yahoo/search/dispatch/rpc/MapConverter.java +++ b/container-search/src/main/java/com/yahoo/search/dispatch/rpc/MapConverter.java @@ -4,10 +4,11 @@ package com.yahoo.search.dispatch.rpc; import ai.vespa.searchlib.searchprotocol.protobuf.SearchProtocol.StringProperty; import ai.vespa.searchlib.searchprotocol.protobuf.SearchProtocol.TensorProperty; import com.google.protobuf.ByteString; +import com.yahoo.io.GrowableByteBuffer; import com.yahoo.tensor.Tensor; import com.yahoo.tensor.serialization.TypedBinaryFormat; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -17,12 +18,13 @@ import java.util.function.Consumer; */ public class MapConverter { - public static void convertMapTensors(Map<String, Object> map, Consumer<TensorProperty.Builder> inserter) { + public static void convertMapTensors(GrowableByteBuffer buffer, Map<String, Object> map, Consumer<TensorProperty.Builder> inserter) { for (var entry : map.entrySet()) { var value = entry.getValue(); - if (value instanceof Tensor) { - byte[] tensor = TypedBinaryFormat.encode((Tensor) value); - inserter.accept(TensorProperty.newBuilder().setName(entry.getKey()).setValue(ByteString.copyFrom(tensor))); + if (value instanceof Tensor tensor) { + buffer.clear(); + TypedBinaryFormat.encode(tensor, buffer); + inserter.accept(TensorProperty.newBuilder().setName(entry.getKey()).setValue(ByteString.copyFrom(buffer.getByteBuffer().flip()))); } } } @@ -45,18 +47,20 @@ public class MapConverter { } } - public static void convertMultiMap(Map<String, List<Object>> map, + public static void convertMultiMap(GrowableByteBuffer buffer, + Map<String, List<Object>> map, Consumer<StringProperty.Builder> stringInserter, Consumer<TensorProperty.Builder> tensorInserter) { for (var entry : map.entrySet()) { if (entry.getValue() != null) { var key = entry.getKey(); - var stringValues = new LinkedList<String>(); + var stringValues = new ArrayList<String>(entry.getValue().size()); for (var value : entry.getValue()) { if (value != null) { - if (value instanceof Tensor) { - byte[] tensor = TypedBinaryFormat.encode((Tensor) value); - tensorInserter.accept(TensorProperty.newBuilder().setName(key).setValue(ByteString.copyFrom(tensor))); + if (value instanceof Tensor tensor) { + buffer.clear(); + TypedBinaryFormat.encode(tensor, buffer); + tensorInserter.accept(TensorProperty.newBuilder().setName(key).setValue(ByteString.copyFrom(buffer.getByteBuffer().flip()))); } else { stringValues.add(value.toString()); } diff --git a/container-search/src/main/java/com/yahoo/search/dispatch/rpc/ProtobufSerialization.java b/container-search/src/main/java/com/yahoo/search/dispatch/rpc/ProtobufSerialization.java index de4f4f45eed..90e16e9dd44 100644 --- a/container-search/src/main/java/com/yahoo/search/dispatch/rpc/ProtobufSerialization.java +++ b/container-search/src/main/java/com/yahoo/search/dispatch/rpc/ProtobufSerialization.java @@ -38,7 +38,14 @@ import java.util.function.Consumer; public class ProtobufSerialization { - private static final int INITIAL_SERIALIZATION_BUFFER_SIZE = 10 * 1024; + /* + * This is a thread local buffer that is used as scratchpad during serialization. + * - avoids the unnecessary cost of allocating and initializing a buffer that is too large. + * - avoids resizing for large queries. + * - Reduces garbage creation. + * There is a limited number of threads that will use this so the upper bound should be fine. + */ + private static final ThreadLocal<GrowableByteBuffer> threadLocalBuffer = ThreadLocal.withInitial(() -> new GrowableByteBuffer(4096)); static byte[] serializeSearchRequest(Query query, int hits, String serverId, double requestTimeout) { return convertFromQuery(query, hits, serverId, requestTimeout).toByteArray(); @@ -58,7 +65,8 @@ public class ProtobufSerialization { if (documentDb != null) { builder.setDocumentType(documentDb); } - builder.setQueryTreeBlob(serializeQueryTree(query.getModel().getQueryTree())); + GrowableByteBuffer scratchPad = threadLocalBuffer.get(); + builder.setQueryTreeBlob(serializeQueryTree(query.getModel().getQueryTree(), scratchPad)); if (query.getGroupingSessionCache() || query.getRanking().getQueryCache()) { // TODO verify that the session key is included whenever rank properties would have been @@ -69,7 +77,8 @@ public class ProtobufSerialization { } if (GroupingExecutor.hasGroupingList(query)) { List<Grouping> groupingList = GroupingExecutor.getGroupingList(query); - BufferSerializer gbuf = new BufferSerializer(new GrowableByteBuffer()); + scratchPad.clear(); + BufferSerializer gbuf = new BufferSerializer(scratchPad); gbuf.putInt(null, groupingList.size()); for (Grouping g : groupingList) { g.serialize(gbuf); @@ -84,7 +93,7 @@ public class ProtobufSerialization { builder.setTraceLevel(getTraceLevelForBackend(query)); builder.setProfileDepth(query.getTrace().getProfileDepth()); - mergeToSearchRequestFromRanking(query.getRanking(), builder); + mergeToSearchRequestFromRanking(query.getRanking(), scratchPad, builder); return builder.build(); } @@ -100,7 +109,7 @@ public class ProtobufSerialization { return traceLevel; } - private static void mergeToSearchRequestFromRanking(Ranking ranking, SearchProtocol.SearchRequest.Builder builder) { + private static void mergeToSearchRequestFromRanking(Ranking ranking, GrowableByteBuffer scratchPad, SearchProtocol.SearchRequest.Builder builder) { builder.setRankProfile(ranking.getProfile()); if (ranking.getQueryCache()) { @@ -115,8 +124,8 @@ public class ProtobufSerialization { var featureMap = ranking.getFeatures().asMap(); MapConverter.convertMapPrimitives(featureMap, builder::addFeatureOverrides); - MapConverter.convertMapTensors(featureMap, builder::addTensorFeatureOverrides); - mergeRankProperties(ranking, builder::addRankProperties, builder::addTensorRankProperties); + MapConverter.convertMapTensors(scratchPad, featureMap, builder::addTensorFeatureOverrides); + mergeRankProperties(ranking, scratchPad, builder::addRankProperties, builder::addTensorRankProperties); } private static void mergeToSearchRequestFromSorting(Sorting sorting, SearchProtocol.SearchRequest.Builder builder) { @@ -160,8 +169,9 @@ public class ProtobufSerialization { if (ranking.getLocation() != null) { builder.setGeoLocation(ranking.getLocation().backendString()); } + GrowableByteBuffer scratchPad = threadLocalBuffer.get(); if (includeQueryData) { - mergeQueryDataToDocsumRequest(query, builder); + mergeQueryDataToDocsumRequest(query, scratchPad, builder); } if (query.getTrace().getLevel() >= 3) { query.trace((includeQueryData ? "ProtoBuf: Resending " : "Not resending ") + "query during document summary fetching", 3); @@ -178,18 +188,18 @@ public class ProtobufSerialization { return builder.build().toByteArray(); } - private static void mergeQueryDataToDocsumRequest(Query query, SearchProtocol.DocsumRequest.Builder builder) { + private static void mergeQueryDataToDocsumRequest(Query query, GrowableByteBuffer scratchPad, SearchProtocol.DocsumRequest.Builder builder) { var ranking = query.getRanking(); var featureMap = ranking.getFeatures().asMap(); - builder.setQueryTreeBlob(serializeQueryTree(query.getModel().getQueryTree())); + builder.setQueryTreeBlob(serializeQueryTree(query.getModel().getQueryTree(), scratchPad)); MapConverter.convertMapPrimitives(featureMap, builder::addFeatureOverrides); - MapConverter.convertMapTensors(featureMap, builder::addTensorFeatureOverrides); + MapConverter.convertMapTensors(scratchPad, featureMap, builder::addTensorFeatureOverrides); if (query.getPresentation().getHighlight() != null) { MapConverter.convertStringMultiMap(query.getPresentation().getHighlight().getHighlightTerms(), builder::addHighlightTerms); } - mergeRankProperties(ranking, builder::addRankProperties, builder::addTensorRankProperties); + mergeRankProperties(ranking, scratchPad, builder::addRankProperties, builder::addTensorRankProperties); } static byte[] serializeResult(Result searchResult) { return convertFromResult(searchResult).toByteArray(); @@ -297,24 +307,25 @@ public class ProtobufSerialization { return builder.build(); } - private static ByteString serializeQueryTree(QueryTree queryTree) { - int bufferSize = INITIAL_SERIALIZATION_BUFFER_SIZE; + private static ByteString serializeQueryTree(QueryTree queryTree, GrowableByteBuffer scratchPad) { while (true) { try { - ByteBuffer treeBuffer = ByteBuffer.allocate(bufferSize); + scratchPad.clear(); + ByteBuffer treeBuffer = scratchPad.getByteBuffer(); queryTree.encode(treeBuffer); - treeBuffer.flip(); - return ByteString.copyFrom(treeBuffer); + return ByteString.copyFrom(treeBuffer.flip()); } catch (java.nio.BufferOverflowException e) { - bufferSize *= 2; + scratchPad.clear(); + scratchPad.grow(scratchPad.capacity()*2); } } } private static void mergeRankProperties(Ranking ranking, + GrowableByteBuffer scratchPad, Consumer<StringProperty.Builder> stringProperties, Consumer<TensorProperty.Builder> tensorProperties) { - MapConverter.convertMultiMap(ranking.getProperties().asMap(), propB -> { + MapConverter.convertMultiMap(scratchPad, ranking.getProperties().asMap(), propB -> { if (!GetDocSumsPacket.sessionIdKey.equals(propB.getName())) { stringProperties.accept(propB); } diff --git a/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java b/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java index bf0272f4f66..7dd1772a53e 100644 --- a/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java +++ b/container-search/src/main/java/com/yahoo/search/handler/Json2SingleLevelMap.java @@ -22,9 +22,11 @@ import java.util.Map; * @author baldersheim */ class Json2SingleLevelMap { + private static final ObjectMapper jsonMapper = new ObjectMapper().configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); private final byte [] buf; private final JsonParser parser; + Json2SingleLevelMap(InputStream data) { try { buf = data.readAllBytes(); @@ -33,6 +35,7 @@ class Json2SingleLevelMap { throw new RuntimeException("Problem reading POSTed data", e); } } + Map<String, String> parse() { try { Map<String, String> map = new HashMap<>(); @@ -47,16 +50,17 @@ class Json2SingleLevelMap { throw new RuntimeException("Problem reading POSTed data", e); } } + void parse(Map<String, String> map, String parent) throws IOException { for (parser.nextToken(); parser.getCurrentToken() != JsonToken.END_OBJECT; parser.nextToken()) { String fieldName = parent + parser.getCurrentName(); JsonToken token = parser.nextToken(); if ((token == JsonToken.VALUE_STRING) || - (token == JsonToken.VALUE_NUMBER_FLOAT) || - (token == JsonToken.VALUE_NUMBER_INT) || - (token == JsonToken.VALUE_TRUE) || - (token == JsonToken.VALUE_FALSE) || - (token == JsonToken.VALUE_NULL)) { + (token == JsonToken.VALUE_NUMBER_FLOAT) || + (token == JsonToken.VALUE_NUMBER_INT) || + (token == JsonToken.VALUE_TRUE) || + (token == JsonToken.VALUE_FALSE) || + (token == JsonToken.VALUE_NULL)) { map.put(fieldName, parser.getText()); } else if (token == JsonToken.START_ARRAY) { map.put(fieldName, skipChildren(parser, buf)); @@ -71,6 +75,7 @@ class Json2SingleLevelMap { } } } + private String skipChildren(JsonParser parser, byte [] input) throws IOException { JsonLocation start = parser.getCurrentLocation(); parser.skipChildren(); @@ -78,4 +83,5 @@ class Json2SingleLevelMap { int offset = (int)start.getByteOffset() - 1; return new String(input, offset, (int)(end.getByteOffset() - offset), StandardCharsets.UTF_8); } + } diff --git a/container-search/src/main/java/com/yahoo/search/query/QueryTree.java b/container-search/src/main/java/com/yahoo/search/query/QueryTree.java index 6326097d9bd..1cc2b98c65b 100644 --- a/container-search/src/main/java/com/yahoo/search/query/QueryTree.java +++ b/container-search/src/main/java/com/yahoo/search/query/QueryTree.java @@ -140,22 +140,18 @@ public class QueryTree extends CompositeItem { else if (b == null || b instanceof NullItem) { return a; } - else if (a instanceof NotItem && b instanceof NotItem) { - NotItem notItemA = (NotItem)a; - NotItem notItemB = (NotItem)b; + else if (a instanceof NotItem notItemA && b instanceof NotItem notItemB) { NotItem combined = new NotItem(); combined.addPositiveItem(and(notItemA.getPositiveItem(), notItemB.getPositiveItem())); notItemA.negativeItems().forEach(item -> combined.addNegativeItem(item)); notItemB.negativeItems().forEach(item -> combined.addNegativeItem(item)); return combined; } - else if (a instanceof NotItem){ - NotItem notItem = (NotItem)a; + else if (a instanceof NotItem notItem){ notItem.addPositiveItem(b); return a; } - else if (b instanceof NotItem){ - NotItem notItem = (NotItem)b; + else if (b instanceof NotItem notItem){ notItem.addPositiveItem(a); return notItem; } @@ -179,17 +175,16 @@ public class QueryTree extends CompositeItem { } private static void getPositiveTerms(Item item, List<IndexedItem> terms) { - if (item instanceof NotItem) { - getPositiveTerms(((NotItem) item).getPositiveItem(), terms); - } else if (item instanceof PhraseItem) { - PhraseItem pItem = (PhraseItem)item; - terms.add(pItem); - } else if (item instanceof CompositeItem) { - for (Iterator<Item> i = ((CompositeItem) item).getItemIterator(); i.hasNext();) { + if (item instanceof NotItem notItem) { + getPositiveTerms(notItem.getPositiveItem(), terms); + } else if (item instanceof PhraseItem phraseItem) { + terms.add(phraseItem); + } else if (item instanceof CompositeItem compositeItem) { + for (Iterator<Item> i = compositeItem.getItemIterator(); i.hasNext();) { getPositiveTerms(i.next(), terms); } - } else if (item instanceof TermItem) { - terms.add((TermItem)item); + } else if (item instanceof TermItem termItem) { + terms.add(termItem); } } @@ -203,8 +198,7 @@ public class QueryTree extends CompositeItem { private int countItemsRecursively(Item item) { int children = 0; - if (item instanceof CompositeItem) { - CompositeItem composite = (CompositeItem)item; + if (item instanceof CompositeItem composite) { for (ListIterator<Item> i = composite.getItemIterator(); i.hasNext(); ) { children += countItemsRecursively(i.next()); } diff --git a/container-search/src/main/java/com/yahoo/search/query/Sorting.java b/container-search/src/main/java/com/yahoo/search/query/Sorting.java index 6b07b5ef1d5..63bc48ff804 100644 --- a/container-search/src/main/java/com/yahoo/search/query/Sorting.java +++ b/container-search/src/main/java/com/yahoo/search/query/Sorting.java @@ -6,7 +6,6 @@ import com.ibm.icu.util.ULocale; import com.yahoo.prelude.IndexFacts; import com.yahoo.processing.IllegalInputException; import com.yahoo.search.Query; -import com.yahoo.search.searchchain.Execution; import com.yahoo.text.Utf8; import java.nio.ByteBuffer; @@ -161,6 +160,7 @@ public class Sorting implements Cloneable { */ public List<FieldOrder> fieldOrders() { return fieldOrders; } + @Override public Sorting clone() { return new Sorting(this.fieldOrders); } @@ -173,16 +173,13 @@ public class Sorting implements Cloneable { @Override public boolean equals(Object o) { if (o == this) return true; - if( ! (o instanceof Sorting)) return false; - - Sorting ss = (Sorting) o; + if( ! (o instanceof Sorting ss)) return false; return fieldOrders.equals(ss.fieldOrders); } public int encode(ByteBuffer buffer) { int usedBytes = 0; byte[] nameBuffer; - buffer.position(); byte space = '.'; for (FieldOrder fieldOrder : fieldOrders) { if (space == ' ') { @@ -231,10 +228,10 @@ public class Sorting implements Cloneable { @Override public boolean equals(Object other) { - if (!(other instanceof AttributeSorter)) { + if (!(other instanceof AttributeSorter sorter)) { return false; } - return ((AttributeSorter) other).fieldName.equals(fieldName); + return sorter.fieldName.equals(fieldName); } @Override @@ -305,15 +302,14 @@ public class Sorting implements Cloneable { public UcaSorter(String fieldName) { super(fieldName); } static private int strength2Collator(Strength strength) { - switch (strength) { - case PRIMARY: return Collator.PRIMARY; - case SECONDARY: return Collator.SECONDARY; - case TERTIARY: return Collator.TERTIARY; - case QUATERNARY: return Collator.QUATERNARY; - case IDENTICAL: return Collator.IDENTICAL; - case UNDEFINED: return Collator.PRIMARY; - } - return Collator.PRIMARY; + return switch (strength) { + case PRIMARY -> Collator.PRIMARY; + case SECONDARY -> Collator.SECONDARY; + case TERTIARY -> Collator.TERTIARY; + case QUATERNARY -> Collator.QUATERNARY; + case IDENTICAL -> Collator.IDENTICAL; + case UNDEFINED -> Collator.PRIMARY; + }; } public void setLocale(String locale, Strength strength) { @@ -323,15 +319,15 @@ public class Sorting implements Cloneable { try { uloc = new ULocale(locale); } catch (Throwable e) { - throw new RuntimeException("ULocale("+locale+") failed with exception " + e.toString()); + throw new IllegalArgumentException("ULocale '" + locale + "' failed", e); } try { collator = Collator.getInstance(uloc); if (collator == null) { - throw new RuntimeException("No collator available for: " + locale); + throw new IllegalArgumentException("No collator available for locale '" + locale + "'"); } } catch (Throwable e) { - throw new RuntimeException("Collator.getInstance(ULocale("+locale+")) failed with exception " + e.toString()); + throw new RuntimeException("Collator.getInstance(ULocale(" + locale + ")) failed", e); } collator.setStrength(strength2Collator(strength)); // collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION); @@ -343,19 +339,22 @@ public class Sorting implements Cloneable { public String getDecomposition() { return (collator.getDecomposition() == Collator.CANONICAL_DECOMPOSITION) ? "CANONICAL_DECOMPOSITION" : "NO_DECOMPOSITION"; } @Override - public String toSerialForm() { return "uca(" + getName() + ',' + locale + ',' + ((strength != Strength.UNDEFINED) ? strength.toString() : "PRIMARY") + ')'; } + public String toSerialForm() { + return "uca(" + getName() + ',' + locale + ',' + + ((strength != Strength.UNDEFINED) ? strength.toString() : "PRIMARY") + ')'; + } @Override public int hashCode() { return 1 + 3*locale.hashCode() + 5*strength.hashCode() + 7*super.hashCode(); } @Override public boolean equals(Object other) { - if (!(other instanceof UcaSorter)) { - return false; - } + if (this == other) return true; + if (!(other instanceof UcaSorter)) return false; return super.equals(other) && locale.equals(((UcaSorter)other).locale) && (strength == ((UcaSorter)other).strength); } + @Override public UcaSorter clone() { UcaSorter clone = (UcaSorter)super.clone(); if (locale != null) { @@ -365,6 +364,7 @@ public class Sorting implements Cloneable { } @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override public int compare(Comparable a, Comparable b) { if ((a instanceof String) && (b instanceof String)) { return collator.compare((String)a, (String) b); diff --git a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java index 224f75e7034..f5d797b4b9f 100644 --- a/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java +++ b/container-search/src/main/java/com/yahoo/search/query/profile/QueryProfileProperties.java @@ -357,7 +357,7 @@ public class QueryProfileProperties extends Properties { if (profile.getTypes().isEmpty()) return name; CompoundName unaliasedName = name; - for (int i = 0; i<name.size(); i++) { + for (int i = 0; i < name.size(); i++) { QueryProfileType type = profile.getType(name.first(i), context); if (type == null) continue; if (type.aliases() == null) continue; // TODO: Make never null diff --git a/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java b/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java index afd25132510..bcdc84c1808 100644 --- a/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java +++ b/container-search/src/main/java/com/yahoo/search/querytransform/NGramSearcher.java @@ -69,8 +69,7 @@ public class NGramSearcher extends Searcher { private boolean rewriteToNGramMatching(Item item, int indexInParent, IndexFacts.Session indexFacts, Query query) { boolean rewritten = false; - if (item instanceof SegmentItem) { // handle CJK segmented terms which should be grams instead - SegmentItem segments = (SegmentItem)item; + if (item instanceof SegmentItem segments) { // handle CJK segmented terms which should be grams instead Index index = indexFacts.getIndex(segments.getIndexName()); if (index.isNGram()) { Item grams = splitToGrams(segments, toLowerCase(segments.getRawWord()), index.getGramSize(), query); @@ -78,13 +77,11 @@ public class NGramSearcher extends Searcher { rewritten = true; } } - else if (item instanceof CompositeItem) { - CompositeItem composite = (CompositeItem)item; + else if (item instanceof CompositeItem composite) { for (int i=0; i<composite.getItemCount(); i++) rewritten = rewriteToNGramMatching(composite.getItem(i), i, indexFacts, query) || rewritten; } - else if (item instanceof TermItem) { - TermItem term = (TermItem)item; + else if (item instanceof TermItem term) { Index index = indexFacts.getIndex(term.getIndexName()); if (index.isNGram()) { Item grams = splitToGrams(term,term.stringValue(), index.getGramSize(), query); @@ -149,11 +146,10 @@ public class NGramSearcher extends Searcher { } private void replaceItemByGrams(Item item, Item grams, int indexInParent) { - if (!(grams instanceof CompositeItem) || !(item.getParent() instanceof PhraseItem)) { // usually, simply replace + if (!(grams instanceof CompositeItem) || !(item.getParent() instanceof PhraseItem phraseParent)) { // usually, simply replace item.getParent().setItem(indexInParent, grams); } else { // but if the parent is a phrase, we cannot add the AND to it, so add each gram to the phrase - PhraseItem phraseParent = (PhraseItem)item.getParent(); phraseParent.removeItem(indexInParent); int addedTerms = 0; for (Iterator<Item> i = ((CompositeItem)grams).getItemIterator(); i.hasNext(); ) { diff --git a/container-search/src/main/java/com/yahoo/search/result/FieldComparator.java b/container-search/src/main/java/com/yahoo/search/result/FieldComparator.java index 0259dd66dbe..7f392d43753 100644 --- a/container-search/src/main/java/com/yahoo/search/result/FieldComparator.java +++ b/container-search/src/main/java/com/yahoo/search/result/FieldComparator.java @@ -17,7 +17,7 @@ import java.util.Comparator; public class FieldComparator extends ChainableComparator { /** The definition of sorting order */ - private Sorting sorting; + private final Sorting sorting; /** Creates a field comparator using a sort order and having no chained comparator */ public FieldComparator(Sorting sorting) { @@ -32,7 +32,7 @@ public class FieldComparator extends ChainableComparator { /** Creates a comparator given a sorting, or returns null if the given sorting is null */ public static FieldComparator create(Sorting sorting) { - if (sorting==null) return null; + if (sorting == null) return null; return new FieldComparator(sorting); } @@ -41,7 +41,7 @@ public class FieldComparator extends ChainableComparator { * stored in hit fields.0 * <p> * When one of the hits has the requested property and the other - * has not, the the hit containing the property precedes the one + * has not, the hit containing the property precedes the one * that does not. * <p> * There is no locale based sorting here, as the backend does @@ -78,19 +78,14 @@ public class FieldComparator extends ChainableComparator { } Inspector sub = top.field(key); if (sub.valid()) { - switch (sub.type()) { - case EMPTY: - return null; - case BOOL: - return (sub.asBool() ? Boolean.TRUE : Boolean.FALSE); - case LONG: - return sub.asLong(); - case DOUBLE: - return sub.asDouble(); - case STRING: - return sub.asString(); - } - return sub.toString(); + return switch (sub.type()) { + case EMPTY -> null; + case BOOL -> (sub.asBool() ? Boolean.TRUE : Boolean.FALSE); + case LONG -> sub.asLong(); + case DOUBLE -> sub.asDouble(); + case STRING -> sub.asString(); + default -> sub.toString(); + }; } } // fallback value @@ -115,15 +110,14 @@ public class FieldComparator extends ChainableComparator { @SuppressWarnings("rawtypes") private int compareValues(Object first, Object second, Sorting.AttributeSorter s) { - if (first == null) { - if (second == null) return 0; - return -1; - } else if (second == null) { + if (first == null) + return second == null ? 0 : -1; + else if (second == null) return 1; - } + if (first.getClass().isInstance(second) && first instanceof Comparable) { // We now know: - // second is of a type which is a subclass of first's type + // Second is of a type which is a subclass of first's type // They both implement Comparable return s.compare((Comparable)first, (Comparable)second); } else { @@ -133,14 +127,7 @@ public class FieldComparator extends ChainableComparator { @Override public String toString() { - StringBuilder b = new StringBuilder(); - b.append("FieldComparator:"); - if (sorting == null) { - b.append(" null"); - } else { - b.append(sorting.toString()); - } - return b.toString(); + return "FieldComparator:" + (sorting == null ? " null" : sorting.toString()); } } diff --git a/container-search/src/main/java/com/yahoo/search/result/Hit.java b/container-search/src/main/java/com/yahoo/search/result/Hit.java index d7acccc75a7..0011d69fc2c 100644 --- a/container-search/src/main/java/com/yahoo/search/result/Hit.java +++ b/container-search/src/main/java/com/yahoo/search/result/Hit.java @@ -16,6 +16,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.function.BiConsumer; @@ -50,7 +51,7 @@ public class Hit extends ListenableFreezableClass implements Data, Comparable<Hi private Map<String, Object> fields = null; private Map<String, Object> unmodifiableFieldMap = null; - /** Meta data describing how a given searcher should treat this hit. */ + /** Metadata describing how a given searcher should treat this hit. */ // TODO: The case for this is to allow multiple levels of federation searcher routing. // Replace this by a cleaner specific solution to that problem. private Map<Searcher, Object> searcherSpecificMetaData; @@ -289,7 +290,8 @@ public class Hit extends ListenableFreezableClass implements Data, Comparable<Hi * Returns the id to display, or null to not display (render) the id. * This is useful to avoid displaying ids when they are not assigned explicitly * but are just generated values for internal use. - * This default implementation returns {@link #getId()}.toString() + * This default implementation returns the field DOCUMENT_ID if set, + * and {@link #getId()}.toString() otherwise. */ public String getDisplayId() { String id = null; @@ -304,8 +306,7 @@ public class Hit extends ListenableFreezableClass implements Data, Comparable<Hi /** Sets the relevance of this hit */ public void setRelevance(Relevance relevance) { - if (relevance == null) throw new NullPointerException("Cannot assign null as relevance"); - this.relevance = relevance; + this.relevance = Objects.requireNonNull(relevance); } /** Does setRelevance(new Relevance(relevance) */ @@ -330,7 +331,7 @@ public class Hit extends ListenableFreezableClass implements Data, Comparable<Hi * have been used for filling so far. Invoking this method * multiple times is allowed and will have no addition * effect. Note that a fillable hit may not be made unfillable. - **/ + */ public void setFillable() { if (filled == null) { filled = Collections.emptySet(); @@ -344,7 +345,7 @@ public class Hit extends ListenableFreezableClass implements Data, Comparable<Hi * tag this hit as fillable if it is currently not. * * @param summaryClass summary class used for filling - **/ + */ public void setFilled(String summaryClass) { if (filled == null || filled.isEmpty()) { filled = Collections.singleton(summaryClass); diff --git a/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java b/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java index adab4d59ec0..205c65a6256 100644 --- a/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java +++ b/container-search/src/main/java/com/yahoo/search/searchchain/AsyncExecution.java @@ -67,7 +67,7 @@ public class AsyncExecution { * Creates an async execution. * * @param chain the chain to execute - * @param context the the context of this + * @param context the context of this */ public AsyncExecution(Chain<? extends Searcher> chain, Execution.Context context) { this(context, chain); diff --git a/container-search/src/main/java/com/yahoo/search/yql/ProgramParser.java b/container-search/src/main/java/com/yahoo/search/yql/ProgramParser.java index 5258087eb44..32880f9b1a8 100644 --- a/container-search/src/main/java/com/yahoo/search/yql/ProgramParser.java +++ b/container-search/src/main/java/com/yahoo/search/yql/ProgramParser.java @@ -277,29 +277,28 @@ final class ProgramParser { switch (getParseTreeIndex(sourceNode)) { // ALL_SOURCE and MULTI_SOURCE are how FROM SOURCES // *|source_name,... are parsed - case yqlplusParser.RULE_select_source_all: - Location location = toLocation(scope, sourceNode.getChild(2)); + case yqlplusParser.RULE_select_source_all -> { + Location location = toLocation(scope, sourceNode.getChild(2)); source = OperatorNode.create(location, SequenceOperator.ALL); source.putAnnotation("alias", "row"); scope.defineDataSource(location, "row"); - break; - case yqlplusParser.RULE_select_source_multi: - Source_listContext multiSourceContext = ((Select_source_multiContext) sourceNode).source_list(); + } + case yqlplusParser.RULE_select_source_multi -> { + Source_listContext multiSourceContext = ((Select_source_multiContext) sourceNode).source_list(); source = readMultiSource(scope, multiSourceContext); source.putAnnotation("alias", "row"); scope.defineDataSource(toLocation(scope, multiSourceContext), "row"); - break; - case yqlplusParser.RULE_select_source_from: - source = convertSource((ParserRuleContext) sourceNode.getChild(1), scope); - break; } - } else { - source = OperatorNode.create(SequenceOperator.EMPTY); + case yqlplusParser.RULE_select_source_from -> + source = convertSource((ParserRuleContext) sourceNode.getChild(1), scope); } + } else { + source = OperatorNode.create(SequenceOperator.EMPTY); + } - for (int i = 1; i < node.getChildCount(); ++i) { - ParseTree child = node.getChild(i); - switch (getParseTreeIndex(child)) { + for (int i = 1; i < node.getChildCount(); ++i) { + ParseTree child = node.getChild(i); + switch (getParseTreeIndex(child)) { case yqlplusParser.RULE_select_field_spec: if (getParseTreeIndex(child.getChild(0)) == yqlplusParser.RULE_project_spec) { proj = readProjection(((Project_specContext) child.getChild(0)).field_def(), scope); @@ -311,7 +310,7 @@ final class ProgramParser { case yqlplusParser.RULE_orderby: // OrderbyContext orderby() List<Orderby_fieldContext> orderFieds = ((OrderbyContext) child) - .orderby_fields().orderby_field(); + .orderby_fields().orderby_field(); orderby = Lists.newArrayListWithExpectedSize(orderFieds.size()); for (var field: orderFieds) { orderby.add(convertSortKey(field, scope)); @@ -326,8 +325,8 @@ final class ProgramParser { case yqlplusParser.RULE_timeout: timeout = convertExpr(((TimeoutContext) child).fixed_or_parameter(), scope); break; - } } + } // now assemble the logical plan OperatorNode<SequenceOperator> result = source; // filter @@ -415,8 +414,7 @@ final class ProgramParser { private OperatorNode<SequenceOperator> convertQuery(ParseTree node, Scope scope) { if (node instanceof Select_statementContext) { return convertSelect(node, scope.getRoot()); - } else if (node instanceof Source_statementContext) { // for pipe - Source_statementContext sourceStatementContext = (Source_statementContext)node; + } else if (node instanceof Source_statementContext sourceStatementContext) { // for pipe return convertPipe(sourceStatementContext.query_statement(), sourceStatementContext.pipeline_step(), scope); } else { throw new IllegalArgumentException("Unexpected argument type to convertQueryStatement: " + node.toStringTree()); @@ -469,47 +467,44 @@ final class ProgramParser { dataSourceNode = (ParserRuleContext)dataSourceNode.getChild(1); } } - switch (getParseTreeIndex(dataSourceNode)) { - case yqlplusParser.RULE_call_source: { - List<String> names = readName(dataSourceNode.getChild(Namespaced_nameContext.class, 0)); - alias = assignAlias(names.get(names.size() - 1), aliasContext, scope); - List<OperatorNode<ExpressionOperator>> arguments = ImmutableList.of(); - ArgumentsContext argumentsContext = dataSourceNode.getRuleContext(ArgumentsContext.class,0); - if ( argumentsContext != null) { - List<ArgumentContext> argumentContexts = argumentsContext.argument(); - arguments = Lists.newArrayListWithExpectedSize(argumentContexts.size()); - for (ArgumentContext argumentContext:argumentContexts) { - arguments.add(convertExpr(argumentContext, scope)); - } - } - if (names.size() == 1 && scope.isVariable(names.get(0))) { - String ident = names.get(0); - if (arguments.size() > 0) { - throw new ProgramCompileException(toLocation(scope, argumentsContext), "Invalid call-with-arguments on local source '%s'", ident); - } - result = OperatorNode.create(toLocation(scope, dataSourceNode), SequenceOperator.EVALUATE, OperatorNode.create(toLocation(scope, dataSourceNode), ExpressionOperator.VARREF, ident)); + switch (getParseTreeIndex(dataSourceNode)) { + case yqlplusParser.RULE_call_source -> { + List<String> names = readName(dataSourceNode.getChild(Namespaced_nameContext.class, 0)); + alias = assignAlias(names.get(names.size() - 1), aliasContext, scope); + List<OperatorNode<ExpressionOperator>> arguments = ImmutableList.of(); + ArgumentsContext argumentsContext = dataSourceNode.getRuleContext(ArgumentsContext.class, 0); + if (argumentsContext != null) { + List<ArgumentContext> argumentContexts = argumentsContext.argument(); + arguments = Lists.newArrayListWithExpectedSize(argumentContexts.size()); + for (ArgumentContext argumentContext : argumentContexts) { + arguments.add(convertExpr(argumentContext, scope)); + } + } + if (names.size() == 1 && scope.isVariable(names.get(0))) { + String ident = names.get(0); + if (arguments.size() > 0) { + throw new ProgramCompileException(toLocation(scope, argumentsContext), "Invalid call-with-arguments on local source '%s'", ident); + } + result = OperatorNode.create(toLocation(scope, dataSourceNode), SequenceOperator.EVALUATE, OperatorNode.create(toLocation(scope, dataSourceNode), ExpressionOperator.VARREF, ident)); } else { - result = OperatorNode.create(toLocation(scope, dataSourceNode), SequenceOperator.SCAN, scope.resolvePath(names), arguments); + result = OperatorNode.create(toLocation(scope, dataSourceNode), SequenceOperator.SCAN, scope.resolvePath(names), arguments); } - break; } - case yqlplusParser.RULE_sequence_source: { - IdentContext identContext = dataSourceNode.getRuleContext(IdentContext.class,0); + case yqlplusParser.RULE_sequence_source -> { + IdentContext identContext = dataSourceNode.getRuleContext(IdentContext.class, 0); String ident = identContext.getText(); if (!scope.isVariable(ident)) { throw new ProgramCompileException(toLocation(scope, identContext), "Unknown variable reference '%s'", ident); } alias = assignAlias(ident, aliasContext, scope); result = OperatorNode.create(toLocation(scope, dataSourceNode), SequenceOperator.EVALUATE, OperatorNode.create(toLocation(scope, dataSourceNode), ExpressionOperator.VARREF, ident)); - break; } - case yqlplusParser.RULE_source_statement: { + case yqlplusParser.RULE_source_statement -> { alias = assignAlias(null, dataSourceNode, scope); result = convertQuery(dataSourceNode, scope); - break; } - default: - throw new IllegalArgumentException("Unexpected argument type to convertSource: " + dataSourceNode.getText()); + default -> + throw new IllegalArgumentException("Unexpected argument type to convertSource: " + dataSourceNode.getText()); } result.putAnnotation("alias", alias); return result; @@ -522,10 +517,7 @@ final class ProgramParser { List<OperatorNode<StatementOperator>> stmts = Lists.newArrayList(); int output = 0; for (ParseTree node : program.children) { - if (!(node instanceof ParserRuleContext)) { - continue; - } - ParserRuleContext ruleContext = (ParserRuleContext) node; + if (!(node instanceof ParserRuleContext ruleContext)) continue; if (ruleContext.getRuleIndex() != yqlplusParser.RULE_statement) throw new ProgramCompileException("Unknown program element: " + node.getText()); diff --git a/container-search/src/main/java/com/yahoo/search/yql/VespaSerializer.java b/container-search/src/main/java/com/yahoo/search/yql/VespaSerializer.java index cab989d4466..4b035e0ccc5 100644 --- a/container-search/src/main/java/com/yahoo/search/yql/VespaSerializer.java +++ b/container-search/src/main/java/com/yahoo/search/yql/VespaSerializer.java @@ -121,7 +121,6 @@ import com.yahoo.search.query.QueryTree; public class VespaSerializer { // TODO: Refactor, too much copy/paste - private static abstract class Serializer<ITEM extends Item> { abstract void onExit(StringBuilder destination, ITEM item); diff --git a/container-search/src/main/java/com/yahoo/search/yql/YqlParser.java b/container-search/src/main/java/com/yahoo/search/yql/YqlParser.java index 72cb102760a..3a1927983d2 100644 --- a/container-search/src/main/java/com/yahoo/search/yql/YqlParser.java +++ b/container-search/src/main/java/com/yahoo/search/yql/YqlParser.java @@ -90,11 +90,9 @@ import com.yahoo.search.query.parser.ParserFactory; /** * The YQL query language. * - * <p> * This class <em>must</em> be kept in lockstep with {@link VespaSerializer}. * Adding anything here will usually require a corresponding addition in * VespaSerializer. - * </p> * * @author Steinar Knutsen * @author Stian Kristoffersen @@ -132,8 +130,9 @@ public class YqlParser implements Parser { private static final String USER_INPUT_DEFAULT_INDEX = "defaultIndex"; private static final String USER_INPUT_GRAMMAR = "grammar"; public static final String USER_INPUT_LANGUAGE = "language"; - private static final String USER_INPUT_RAW = "raw"; - private static final String USER_INPUT_SEGMENT = "segment"; + private static final String USER_INPUT_GRAMMAR_RAW = "raw"; + private static final String USER_INPUT_GRAMMAR_SEGMENT = "segment"; + private static final String USER_INPUT_GRAMMAR_WEAKAND = "weakAnd"; private static final String USER_INPUT = "userInput"; private static final String USER_QUERY = "userQuery"; private static final String NON_EMPTY = "nonEmpty"; @@ -722,14 +721,19 @@ public class YqlParser implements Parser { String.class, "default", "default index for user input terms"); Language language = decideParsingLanguage(ast, wordData); Item item; - if (USER_INPUT_RAW.equals(grammar)) { + if (USER_INPUT_GRAMMAR_RAW.equals(grammar)) { item = instantiateWordItem(defaultIndex, wordData, ast, null, SegmentWhen.NEVER, true, language); - } else if (USER_INPUT_SEGMENT.equals(grammar)) { + } else if (USER_INPUT_GRAMMAR_SEGMENT.equals(grammar)) { item = instantiateWordItem(defaultIndex, wordData, ast, null, SegmentWhen.ALWAYS, false, language); } else { item = parseUserInput(grammar, defaultIndex, wordData, language, allowEmpty); propagateUserInputAnnotations(ast, item); } + + // Set grammar-specific annotations + if (USER_INPUT_GRAMMAR_WEAKAND.equals(grammar) && item instanceof WeakAndItem weakAndItem) { + weakAndItem.setN(getAnnotation(ast, TARGET_HITS, Integer.class, WeakAndItem.defaultN, "'targetHits' (N) for weak and")); + } return item; } @@ -1428,7 +1432,7 @@ public class YqlParser implements Parser { wordItem = new WordItem(wordData, fromQuery); break; case POSSIBLY: - if (shouldSegment(field, fromQuery) && ! grammar.equals(USER_INPUT_RAW)) { + if (shouldSegment(field, fromQuery) && ! grammar.equals(USER_INPUT_GRAMMAR_RAW)) { wordItem = segment(field, ast, wordData, fromQuery, parent, language); } else { wordItem = new WordItem(wordData, fromQuery); diff --git a/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java b/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java index ae2a8800bbc..5b972e40774 100644 --- a/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/test/QueryTestCase.java @@ -65,7 +65,7 @@ import static org.junit.jupiter.api.Assertions.*; public class QueryTestCase { @Test - void testIt() throws Exception { + void testIt() { JSONObject newroot = new JSONObject("{\"key\": 3}"); var hit = new FastHit(); hit.setField("data", (JsonProducer) s -> s.append(newroot)); diff --git a/container-search/src/test/java/com/yahoo/search/yql/UserInputTestCase.java b/container-search/src/test/java/com/yahoo/search/yql/UserInputTestCase.java index 1e3b52c23af..858d5a16352 100644 --- a/container-search/src/test/java/com/yahoo/search/yql/UserInputTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/yql/UserInputTestCase.java @@ -7,6 +7,7 @@ import com.yahoo.prelude.Index; import com.yahoo.prelude.IndexFacts; import com.yahoo.prelude.IndexModel; import com.yahoo.prelude.SearchDefinition; +import com.yahoo.prelude.query.WeakAndItem; import org.apache.http.client.utils.URIBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -89,6 +90,17 @@ public class UserInputTestCase { } @Test + void testUserInputSettingTargetHits() { + URIBuilder builder = searchUri(); + builder.setParameter("yql", + "select * from sources * where {grammar: \"weakAnd\", targetHits: 17, defaultIndex: \"f\"}userInput(\"a test\")"); + Query query = searchAndAssertNoErrors(builder); + assertEquals("select * from sources * where ({targetNumHits: 17}weakAnd(f contains \"a\", f contains \"test\"))", query.yqlRepresentation()); + WeakAndItem weakAnd = (WeakAndItem)query.getModel().getQueryTree().getRoot(); + assertEquals(17, weakAnd.getN()); + } + + @Test void testSegmentedNoiseUserInput() { URIBuilder builder = searchUri(); builder.setParameter("yql", diff --git a/container-search/src/test/java/com/yahoo/search/yql/YqlParserTestCase.java b/container-search/src/test/java/com/yahoo/search/yql/YqlParserTestCase.java index 5beea5352aa..807681e1d7b 100644 --- a/container-search/src/test/java/com/yahoo/search/yql/YqlParserTestCase.java +++ b/container-search/src/test/java/com/yahoo/search/yql/YqlParserTestCase.java @@ -1002,10 +1002,21 @@ public class YqlParserTestCase { @Test void testRegexp() { - QueryTree x = parse("select * from sources * where foo matches \"a b\""); - Item root = x.getRoot(); - assertSame(RegExpItem.class, root.getClass()); - assertEquals("a b", ((RegExpItem) root).stringValue()); + { + QueryTree x = parse("select * from sources * where foo matches \"a b\""); + Item root = x.getRoot(); + assertSame(RegExpItem.class, root.getClass()); + assertEquals("a b", ((RegExpItem) root).stringValue()); + } + + { + String expression = "a\\\\.b\\\\.c"; + QueryTree query = parse("select * from sources * where foo matches \"" + expression + "\""); + var regExpItem = (RegExpItem) query.getRoot(); + assertEquals("a\\.b\\.c", regExpItem.stringValue()); + assertTrue(regExpItem.getRegexp().matcher("a.b.c").matches(), "a.b.c is matched"); + assertFalse(regExpItem.getRegexp().matcher("a,b,c").matches(), "a,b,c is matched?"); + } } @Test diff --git a/container-test/pom.xml b/container-test/pom.xml index 65418f75ae2..100fe749394 100644 --- a/container-test/pom.xml +++ b/container-test/pom.xml @@ -32,7 +32,7 @@ </exclusion> <exclusion> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>*</artifactId> </exclusion> </exclusions> </dependency> @@ -40,7 +40,7 @@ in provided scope when container-test is declared before, and together with, the container artifact --> <dependency> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>bcpkix-jdk18on</artifactId> </dependency> <!-- All dependencies that should be visible in test classpath, but not compile classpath, diff --git a/container/pom.xml b/container/pom.xml index 966d8d49f5a..10322758d1a 100644 --- a/container/pom.xml +++ b/container/pom.xml @@ -24,7 +24,7 @@ <exclusions> <exclusion> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>*</artifactId> </exclusion> <exclusion> <groupId>org.ow2.asm</groupId> diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java index a35d01f6891..d3331c3cfd4 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota; @@ -22,12 +23,14 @@ import static java.util.Objects.requireNonNull; /** * Data pertaining to a deployment to be done on a config server. + * Accessor names must match the names in com.yahoo.vespa.config.server.session.PrepareParams. * * @author jonmv */ public class DeploymentData { private final ApplicationId instance; + private final Tags tags; private final ZoneId zone; private final byte[] applicationPackage; private final Version platform; @@ -41,7 +44,7 @@ public class DeploymentData { private final Optional<CloudAccount> cloudAccount; private final boolean dryRun; - public DeploymentData(ApplicationId instance, ZoneId zone, byte[] applicationPackage, Version platform, + public DeploymentData(ApplicationId instance, Tags tags, ZoneId zone, byte[] applicationPackage, Version platform, Set<ContainerEndpoint> containerEndpoints, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, Optional<DockerImage> dockerImageRepo, @@ -51,6 +54,7 @@ public class DeploymentData { List<X509Certificate> operatorCertificates, Optional<CloudAccount> cloudAccount, boolean dryRun) { this.instance = requireNonNull(instance); + this.tags = requireNonNull(tags); this.zone = requireNonNull(zone); this.applicationPackage = requireNonNull(applicationPackage); this.platform = requireNonNull(platform); @@ -69,6 +73,8 @@ public class DeploymentData { return instance; } + public Tags tags() { return tags; } + public ZoneId zone() { return zone; } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java index 4e5f2ab64cb..28bb9182c6a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java @@ -133,6 +133,11 @@ public class ZmsClientMock implements ZmsClient { return new ArrayList<>(athenz.domains.keySet()); } + public List<AthenzDomain> getDomainListByAccount(String id) { + log("getDomainListById()"); + return new ArrayList<>(); + } + @Override public boolean hasAccess(AthenzResourceName resource, String action, AthenzIdentity identity) { log("hasAccess(resource=%s, action=%s, identity=%s)", resource, action, identity); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java index 1659a87acb3..7ccbcf2a954 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java @@ -59,8 +59,8 @@ public sealed abstract class AliasTarget permits LatencyAliasTarget, WeightedAli public static AliasTarget unpack(RecordData data) { String[] parts = data.asString().split("/"); switch (parts[0]) { - case "latency": return LatencyAliasTarget.unpack(data); - case "weighted": return WeightedAliasTarget.unpack(data); + case LatencyAliasTarget.TARGET_TYPE: return LatencyAliasTarget.unpack(data); + case WeightedAliasTarget.TARGET_TYPE: return WeightedAliasTarget.unpack(data); } throw new IllegalArgumentException("Unknown alias type '" + parts[0] + "'"); } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java new file mode 100644 index 00000000000..c3cedf93841 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java @@ -0,0 +1,57 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import java.util.Objects; + +/** + * Same as {@link AliasTarget}, except for targets outside AWS (cannot be targeted with ALIAS record). + * + * @author freva + */ +public sealed abstract class DirectTarget permits LatencyDirectTarget, WeightedDirectTarget { + + private final RecordData recordData; + private final String id; + + protected DirectTarget(RecordData recordData, String id) { + this.recordData = Objects.requireNonNull(recordData, "recordData must be non-null"); + this.id = Objects.requireNonNull(id, "id must be non-null"); + } + + /** A unique identifier of this record within the record group */ + public String id() { + return id; + } + + /** Data in this, e.g. IP address for records of type A */ + public RecordData recordData() { + return recordData; + } + + /** Returns the fields in this encoded as record data */ + public abstract RecordData pack(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DirectTarget that = (DirectTarget) o; + return recordData.equals(that.recordData) && id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(recordData, id); + } + + /** Unpack target from given record data */ + public static DirectTarget unpack(RecordData data) { + String[] parts = data.asString().split("/"); + return switch (parts[0]) { + case LatencyDirectTarget.TARGET_TYPE -> LatencyDirectTarget.unpack(data); + case WeightedDirectTarget.TARGET_TYPE -> WeightedDirectTarget.unpack(data); + default -> throw new IllegalArgumentException("Unknown alias type '" + parts[0] + "'"); + }; + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java index 70c89b05f09..00e5218dead 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java @@ -13,6 +13,8 @@ import java.util.Objects; */ public final class LatencyAliasTarget extends AliasTarget { + static final String TARGET_TYPE = "latency"; + private final ZoneId zone; public LatencyAliasTarget(DomainName name, String dnsZone, ZoneId zone) { @@ -27,7 +29,7 @@ public final class LatencyAliasTarget extends AliasTarget { @Override public RecordData pack() { - return RecordData.from("latency/" + name().value() + "/" + dnsZone() + "/" + id()); + return RecordData.from(String.join("/", TARGET_TYPE, name().value(), dnsZone(), id())); } @Override @@ -56,7 +58,7 @@ public final class LatencyAliasTarget extends AliasTarget { throw new IllegalArgumentException("Expected data to be on format type/name/DNS-zone/zone-id, but got " + data.asString()); } - if (!"latency".equals(parts[0])) { + if (!TARGET_TYPE.equals(parts[0])) { throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); } return new LatencyAliasTarget(DomainName.of(parts[1]), parts[2], ZoneId.from(parts[3])); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java new file mode 100644 index 00000000000..09795ae08a7 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java @@ -0,0 +1,66 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import com.yahoo.config.provision.zone.ZoneId; + +import java.util.Objects; + +/** + * An implementation of {@link DirectTarget} that uses latency-based routing. + * + * @author freva + */ +public final class LatencyDirectTarget extends DirectTarget { + + static final String TARGET_TYPE = "latency"; + + private final ZoneId zone; + + public LatencyDirectTarget(RecordData recordData, ZoneId zone) { + super(recordData, zone.value()); + this.zone = Objects.requireNonNull(zone); + } + + /** The zone this record points to */ + public ZoneId zone() { + return zone; + } + + @Override + public RecordData pack() { + return RecordData.from(String.join("/", TARGET_TYPE, recordData().asString(), id())); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + LatencyDirectTarget that = (LatencyDirectTarget) o; + return zone.equals(that.zone); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), zone); + } + + @Override + public String toString() { + return "latency target for " + recordData() + " [id=" + id() + "]"; + } + + /** Unpack latency alias from given record data */ + public static LatencyDirectTarget unpack(RecordData data) { + var parts = data.asString().split("/"); + if (parts.length != 3) { + throw new IllegalArgumentException("Expected data to be on format target-type/record-data/zone-id, but got " + + data.asString()); + } + if (!TARGET_TYPE.equals(parts[0])) { + throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); + } + return new LatencyDirectTarget(RecordData.from(parts[1]), ZoneId.from(parts[2])); + } + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java index 18d7bc53035..9a9270bdf7f 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java @@ -54,6 +54,20 @@ public class MemoryNameService implements NameService { } @Override + public List<Record> createDirect(RecordName name, Set<DirectTarget> targets) { + var records = targets.stream() + .sorted((a, b) -> Comparator.comparing((DirectTarget target) -> target.recordData().asString()).compare(a, b)) + .map(d -> new Record(Record.Type.DIRECT, name, d.pack())) + .collect(Collectors.toList()); + // Satisfy idempotency contract of interface + for (var r1 : records) { + this.records.removeIf(r2 -> conflicts(r1, r2)); + } + this.records.addAll(records); + return records; + } + + @Override public List<Record> createTxtRecords(RecordName name, List<RecordData> txtData) { var records = txtData.stream() .map(data -> new Record(Record.Type.TXT, name, data)) @@ -122,6 +136,11 @@ public class MemoryNameService implements NameService { AliasTarget t2 = AliasTarget.unpack(r2.data()); return t1.name().equals(t2.name()); // ALIAS records require distinct targets } + if (r1.type() == Record.Type.DIRECT && r1.type() == r2.type()) { + DirectTarget t1 = DirectTarget.unpack(r1.data()); + DirectTarget t2 = DirectTarget.unpack(r2.data()); + return t1.id().equals(t2.id()); // DIRECT records require distinct IDs + } return true; // Anything else is considered a conflict } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java index 505ff3850ab..72e983680d9 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java @@ -31,6 +31,15 @@ public interface NameService { List<Record> createAlias(RecordName name, Set<AliasTarget> targets); /** + * Create a non-standard record pointing to given targets. Implementations of this are expected to be + * idempotent + * + * @param targets Targets that should be resolved by this name. + * @return The created records. One per target. + */ + List<Record> createDirect(RecordName name, Set<DirectTarget> targets); + + /** * Create a new TXT record containing the provided data. * @param name Name of the created record * @param txtRecords TXT data values for the record, each consisting of one or more space-separated double-quoted diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java index 2f9312b2f89..e76445faa60 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java @@ -55,6 +55,7 @@ public record Record(Type type, AAAA, ALIAS, CNAME, + DIRECT, MX, NS, PTR, diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java index 6a61b62f3a4..ca01c713e93 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java @@ -16,6 +16,8 @@ import java.util.Objects; */ public final class WeightedAliasTarget extends AliasTarget { + static final String TARGET_TYPE = "weighted"; + private final long weight; public WeightedAliasTarget(DomainName name, String dnsZone, ZoneId zone, long weight) { @@ -31,7 +33,7 @@ public final class WeightedAliasTarget extends AliasTarget { @Override public RecordData pack() { - return RecordData.from("weighted/" + name().value() + "/" + dnsZone() + "/" + id() + "/" + weight); + return RecordData.from(String.join("/", TARGET_TYPE, name().value(), dnsZone(), id(), Long.toString(weight))); } @Override @@ -60,7 +62,7 @@ public final class WeightedAliasTarget extends AliasTarget { throw new IllegalArgumentException("Expected data to be on format type/name/DNS-zone/zone-id/weight, " + "but got " + data.asString()); } - if (!"weighted".equals(parts[0])) { + if (!TARGET_TYPE.equals(parts[0])) { throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); } return new WeightedAliasTarget(DomainName.of(parts[1]), parts[2], ZoneId.from(parts[3]), diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java new file mode 100644 index 00000000000..b899cb57b60 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java @@ -0,0 +1,70 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import com.yahoo.config.provision.zone.ZoneId; + +import java.util.Objects; + +/** + * An implementation of {@link DirectTarget} where is requests are answered based on the weight assigned to the + * record, as a proportion of the total weight for all records having the same DNS name. + * + * The portion of received traffic is calculated as follows: (record weight / sum of the weights of all records). + * + * @author freva + */ +public final class WeightedDirectTarget extends DirectTarget { + + static final String TARGET_TYPE = "weighted"; + + private final long weight; + + public WeightedDirectTarget(RecordData recordData, ZoneId zone, long weight) { + super(recordData, zone.value()); + this.weight = weight; + if (weight < 0) throw new IllegalArgumentException("Weight cannot be negative"); + } + + /** The weight of this target */ + public long weight() { + return weight; + } + + @Override + public RecordData pack() { + return RecordData.from(String.join("/", TARGET_TYPE, recordData().asString(), id(), Long.toString(weight))); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + WeightedDirectTarget that = (WeightedDirectTarget) o; + return weight == that.weight; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), weight); + } + + @Override + public String toString() { + return "weighted target for " + recordData() + "[id=" + id() + ",weight=" + weight + "]"; + } + + /** Unpack weighted alias from given record data */ + public static WeightedDirectTarget unpack(RecordData data) { + var parts = data.asString().split("/"); + if (parts.length != 4) { + throw new IllegalArgumentException("Expected data to be on format target-type/record-data/zone-id/weight, " + + "but got " + data.asString()); + } + if (!TARGET_TYPE.equals(parts[0])) { + throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'"); + } + return new WeightedDirectTarget(RecordData.from(parts[1]), ZoneId.from(parts[2]), Long.parseLong(parts[3])); + } + +} diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java new file mode 100644 index 00000000000..f262821a638 --- /dev/null +++ b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java @@ -0,0 +1,36 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.api.integration.dns; + +import com.yahoo.config.provision.zone.ZoneId; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author freva + */ +class DirectTargetTest { + + @Test + void packing() { + List<DirectTarget> tests = List.of( + new LatencyDirectTarget(RecordData.from("foo.example.com"), ZoneId.from("prod.us-north-1")), + new WeightedDirectTarget(RecordData.from("bar.example.com"), ZoneId.from("prod.us-north-2"), 50)); + for (var target : tests) { + DirectTarget unpacked = DirectTarget.unpack(target.pack()); + assertEquals(target, unpacked); + } + + List<RecordData> invalidData = List.of(RecordData.from(""), RecordData.from("foobar")); + for (var data : invalidData) { + try { + DirectTarget.unpack(data); + fail("Expected exception"); + } catch (IllegalArgumentException ignored) { } + } + } + +}
\ No newline at end of file diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java index e3a0ad7cb84..66e62ff7b95 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java @@ -65,7 +65,7 @@ public class Application { Set.of(), OptionalLong.empty(), RevisionHistory.empty(), List.of()); } - // DO NOT USE! For serialization purposes, only. + // Do not use directly - edit through LockedApplication. public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides, Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner, OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId, @@ -230,11 +230,8 @@ public class Application { @Override public boolean equals(Object o) { if (this == o) return true; - if (! (o instanceof Application)) return false; - - Application that = (Application) o; - - return id.equals(that.id); + if (! (o instanceof Application other)) return false; + return id.equals(other.id); } @Override diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index 19775ef420d..d8234e4f269 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -12,6 +12,7 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.DockerImage; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.log.LogLevel; @@ -166,10 +167,11 @@ public class ApplicationController { int count = 0; for (TenantAndApplicationId id : curator.readApplicationIds()) { lockApplicationIfPresent(id, application -> { - for (InstanceName instance : application.get().deploymentSpec().instanceNames()) - if ( ! application.get().instances().containsKey(instance)) - application = withNewInstance(application, id.instance(instance)); - + for (var declaredInstance : application.get().deploymentSpec().instances()) + if ( ! application.get().instances().containsKey(declaredInstance.name())) + application = withNewInstance(application, + id.instance(declaredInstance.name()), + declaredInstance.tags()); store(application); }); count++; @@ -451,14 +453,14 @@ public class ApplicationController { * * @throws IllegalArgumentException if the instance already exists, or has an invalid instance name. */ - public void createInstance(ApplicationId id) { + public void createInstance(ApplicationId id, Tags tags) { lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { - store(withNewInstance(application, id)); + store(withNewInstance(application, id, tags)); }); } /** Returns given application with a new instance */ - public LockedApplication withNewInstance(LockedApplication application, ApplicationId instance) { + public LockedApplication withNewInstance(LockedApplication application, ApplicationId instance, Tags tags) { if (instance.instance().isTester()) throw new IllegalArgumentException("'" + instance + "' is a tester application!"); InstanceId.validate(instance.instance().value()); @@ -469,7 +471,7 @@ public class ApplicationController { throw new IllegalArgumentException("Could not create '" + instance + "': Instance " + dashToUnderscore(instance) + " already exists"); log.info("Created " + instance); - return application.withNewInstance(instance.instance()); + return application.withNewInstance(instance.instance(), tags); } /** Deploys an application package for an existing application instance. */ @@ -496,10 +498,11 @@ public class ApplicationController { ApplicationPackage applicationPackage = new ApplicationPackage(applicationStore.get(deployment, revision)); AtomicReference<RevisionId> lastRevision = new AtomicReference<>(); + Instance instance; try (Mutex lock = lock(applicationId)) { LockedApplication application = new LockedApplication(requireApplication(applicationId), lock); application.get().revisions().last().map(ApplicationVersion::id).ifPresent(lastRevision::set); - Instance instance = application.get().require(job.application().instance()); + instance = application.get().require(job.application().instance()); if ( ! applicationPackage.trustedCertificates().isEmpty() && run.testerCertificate().isPresent()) @@ -512,7 +515,7 @@ public class ApplicationController { } // Release application lock while doing the deployment, which is a lengthy task. // Carry out deployment without holding the application lock. - ActivateResult result = deploy(job.application(), applicationPackage, zone, platform, containerEndpoints, + ActivateResult result = deploy(job.application(), instance.tags(), applicationPackage, zone, platform, containerEndpoints, endpointCertificateMetadata, run.isDryRun()); endpointCertificateMetadata.ifPresent(e -> deployLogger.accept("Using CA signed certificate version %s".formatted(e.version()))); @@ -543,9 +546,9 @@ public class ApplicationController { lockApplicationOrThrow(applicationId, application -> store(application.with(job.application().instance(), - instance -> instance.withNewDeployment(zone, revision, platform, - clock.instant(), warningsFrom(result), - quotaUsage)))); + i -> i.withNewDeployment(zone, revision, platform, + clock.instant(), warningsFrom(result), + quotaUsage)))); return result; } } @@ -557,16 +560,19 @@ public class ApplicationController { application = application.with(applicationPackage.deploymentSpec()); application = application.with(applicationPackage.validationOverrides()); - var existingInstances = application.get().instances().keySet(); - var declaredInstances = applicationPackage.deploymentSpec().instanceNames(); - for (var name : declaredInstances) - if ( ! existingInstances.contains(name)) - application = withNewInstance(application, application.get().id().instance(name)); + var existingInstances = application.get().instances(); + var declaredInstances = applicationPackage.deploymentSpec().instances(); + for (var declaredInstance : declaredInstances) { + if ( ! existingInstances.containsKey(declaredInstance.name())) + application = withNewInstance(application, application.get().id().instance(declaredInstance.name()), declaredInstance.tags()); + else if ( ! existingInstances.get(declaredInstance.name()).tags().equals(declaredInstance.tags())) + application = application.with(declaredInstance.name(), instance -> instance.with(declaredInstance.tags())); + } // Delete zones not listed in DeploymentSpec, if allowed // We do this at deployment time for externally built applications, and at submission time // for internally built ones, to be able to return a validation failure message when necessary - for (InstanceName name : existingInstances) { + for (InstanceName name : existingInstances.keySet()) { application = withoutDeletedDeployments(application, name); } @@ -599,7 +605,7 @@ public class ApplicationController { ApplicationPackage applicationPackage = new ApplicationPackage( artifactRepository.getSystemApplicationPackage(application.id(), zone, version) ); - return deploy(application.id(), applicationPackage, zone, version, Set.of(), /* No application cert */ Optional.empty(), false); + return deploy(application.id(), Tags.empty(), applicationPackage, zone, version, Set.of(), /* No application cert */ Optional.empty(), false); } else { throw new RuntimeException("This system application does not have an application package: " + application.id().toShortString()); } @@ -607,10 +613,10 @@ public class ApplicationController { /** Deploys the given tester application to the given zone. */ public ActivateResult deployTester(TesterId tester, ApplicationPackage applicationPackage, ZoneId zone, Version platform) { - return deploy(tester.id(), applicationPackage, zone, platform, Set.of(), /* No application cert for tester*/ Optional.empty(), false); + return deploy(tester.id(), Tags.empty(), applicationPackage, zone, platform, Set.of(), /* No application cert for tester*/ Optional.empty(), false); } - private ActivateResult deploy(ApplicationId application, ApplicationPackage applicationPackage, + private ActivateResult deploy(ApplicationId application, Tags tags, ApplicationPackage applicationPackage, ZoneId zone, Version platform, Set<ContainerEndpoint> endpoints, Optional<EndpointCertificateMetadata> endpointCertificateMetadata, boolean dryRun) { @@ -646,7 +652,7 @@ public class ApplicationController { .collect(toList()); Optional<CloudAccount> cloudAccount = decideCloudAccountOf(deployment, applicationPackage.deploymentSpec()); ConfigServer.PreparedApplication preparedApplication = - configServer.deploy(new DeploymentData(application, zone, applicationPackage.zippedContent(), platform, + configServer.deploy(new DeploymentData(application, tags, zone, applicationPackage.zippedContent(), platform, endpoints, endpointCertificateMetadata, dockerImageRepo, domain, deploymentQuota, tenantSecretStores, operatorCertificates, cloudAccount, dryRun)); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java index d66d1491f73..430bafe5c44 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java @@ -5,6 +5,7 @@ import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId; @@ -40,6 +41,7 @@ import java.util.stream.Collectors; public class Instance { private final ApplicationId id; + private final Tags tags; private final Map<ZoneId, Deployment> deployments; private final List<AssignedRotation> rotations; private final RotationStatus rotationStatus; @@ -47,14 +49,15 @@ public class Instance { private final Change change; /** Creates an empty instance */ - public Instance(ApplicationId id) { - this(id, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty()); + public Instance(ApplicationId id, Tags tags) { + this(id, tags, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty()); } /** Creates an empty instance*/ - public Instance(ApplicationId id, Collection<Deployment> deployments, Map<JobType, Instant> jobPauses, + public Instance(ApplicationId id, Tags tags, Collection<Deployment> deployments, Map<JobType, Instant> jobPauses, List<AssignedRotation> rotations, RotationStatus rotationStatus, Change change) { this.id = Objects.requireNonNull(id, "id cannot be null"); + this.tags = Objects.requireNonNull(tags, "tags cannot be null"); this.deployments = Objects.requireNonNull(deployments, "deployments cannot be null").stream() .collect(Collectors.toUnmodifiableMap(Deployment::zone, Function.identity())); this.jobPauses = Map.copyOf(Objects.requireNonNull(jobPauses, "deploymentJobs cannot be null")); @@ -63,6 +66,10 @@ public class Instance { this.change = Objects.requireNonNull(change, "change cannot be null"); } + public Instance with(Tags tags) { + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); + } + public Instance withNewDeployment(ZoneId zone, RevisionId revision, Version version, Instant instant, Map<DeploymentMetrics.Warning, Integer> warnings, QuotaUsage quotaUsage) { // Use info from previous deployment if available, otherwise create a new one. @@ -87,7 +94,7 @@ public class Instance { else jobPauses.remove(jobType); - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } public Instance recordActivityAt(Instant instant, ZoneId zone) { @@ -118,15 +125,15 @@ public class Instance { } public Instance with(List<AssignedRotation> assignedRotations) { - return new Instance(id, deployments.values(), jobPauses, assignedRotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, assignedRotations, rotationStatus, change); } public Instance with(RotationStatus rotationStatus) { - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } public Instance withChange(Change change) { - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } private Instance with(Deployment deployment) { @@ -136,13 +143,15 @@ public class Instance { } private Instance with(Map<ZoneId, Deployment> deployments) { - return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change); + return new Instance(id, tags, deployments.values(), jobPauses, rotations, rotationStatus, change); } public ApplicationId id() { return id; } public InstanceName name() { return id.instance(); } + public Tags tags() { return tags; } + /** Returns an immutable map of the current deployments of this */ public Map<ZoneId, Deployment> deployments() { return deployments; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java index 3e822415e96..fa702a166d2 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId; @@ -88,9 +89,9 @@ public class LockedApplication { projectId, revisions, instances.values()); } - LockedApplication withNewInstance(InstanceName instance) { + LockedApplication withNewInstance(InstanceName instance, Tags tags) { var instances = new HashMap<>(this.instances); - instances.put(instance, new Instance(id.instance(instance))); + instances.put(instance, new Instance(id.instance(instance), tags)); return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys, projectId, instances, revisions); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java index 4bc9aeb00e4..c2c95a0c4bf 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java @@ -16,6 +16,7 @@ import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.security.X509CertificateUtils; import com.yahoo.slime.Inspector; import com.yahoo.slime.Slime; @@ -202,7 +203,8 @@ public class ApplicationPackage { new InputStreamReader(new ByteArrayInputStream(servicesXml.content()), UTF_8), InstanceName.defaultName(), Environment.prod, - RegionName.defaultName()) + RegionName.defaultName(), + Tags.empty()) .run(); // Populates the zip archive cache with files that would be included. } catch (IllegalArgumentException e) { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java index 8e8a4e24970..f72b6f2e9f0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java @@ -10,7 +10,6 @@ import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; -import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Application; @@ -26,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** @@ -97,15 +97,10 @@ public class ApplicationPackageValidator { var clouds = new HashSet<CloudName>(); for (var region : endpoint.regions()) { for (ZoneApi zone : controller.zoneRegistry().zones().all().in(Environment.prod).in(region).zones()) { - if (zone.getCloudName().equals(CloudName.GCP)) { - throw new IllegalArgumentException("Endpoint '" + endpoint.endpointId() + "' in " + instance + - " contains a Google Cloud region (" + region + - "), which is not yet supported"); - } clouds.add(zone.getCloudName()); } } - if (clouds.size() != 1) { + if (clouds.size() != 1 && !clouds.equals(Set.of(CloudName.GCP, CloudName.AWS))) { throw new IllegalArgumentException("Endpoint '" + endpoint.endpointId() + "' in " + instance + " cannot contain regions in different clouds: " + endpoint.regions().stream().sorted().toList()); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java index 9bc2c5a5595..0fe9a84f5fa 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java @@ -54,6 +54,7 @@ import static com.yahoo.config.application.api.DeploymentSpec.RevisionTarget.nex import static com.yahoo.config.provision.Environment.prod; import static com.yahoo.config.provision.Environment.staging; import static com.yahoo.config.provision.Environment.test; +import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication; import static java.util.Comparator.comparing; import static java.util.Comparator.naturalOrder; import static java.util.Comparator.reverseOrder; @@ -1027,10 +1028,11 @@ public class DeploymentStatus { Versions lastVersions = job.lastCompleted().get().versions(); Versions toRun = Versions.from(change, status.application, dependent.flatMap(status::deploymentFor), status.fallbackPlatform(change, job.id())); if ( ! toRun.targetsMatch(lastVersions)) return Optional.empty(); - if ( job.id().type().environment().isTest() + if ( job.id().type().environment().isTest() && ! dependent.map(JobId::type).map(status::findCloud).map(List.of(CloudName.AWS, CloudName.GCP)::contains).orElse(true) - && job.isNodeAllocationFailure()) return Optional.empty(); + && job.isNodeAllocationFailure()) return Optional.empty(); + if (job.lastStatus().get() == invalidApplication) return Optional.of(status.now.plus(Duration.ofDays(36524))); // 100 years Instant firstFailing = job.firstFailing().get().end().get(); Instant lastCompleted = job.lastCompleted().get().end().get(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java index cf09afa7181..d5a31a07408 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java @@ -91,10 +91,31 @@ public class DeploymentTrigger { status, false)); } + + // If app has been broken since it was first submitted, and not fixed for a long time, we stop managing it until a new submission comes in. + if (applicationWasAlwaysBroken(status)) + application = application.withProjectId(OptionalLong.empty()); + applications().store(application); }); } + private boolean applicationWasAlwaysBroken(DeploymentStatus status) { + // If application has a production deployment, we cannot forget it. + if (status.application().instances().values().stream().anyMatch(instance -> ! instance.productionDeployments().isEmpty())) + return false; + + // Then, we need a job that always failed, and failed on the last revision for at least 30 days. + RevisionId last = status.application().revisions().last().get().id(); + Instant threshold = clock.instant().minus(Duration.ofDays(30)); + for (JobStatus job : status.jobs().asList()) + for (Run run : job.runs().descendingMap().values()) + if (run.hasEnded() && ! run.hasFailed() || ! run.versions().targetRevision().equals(last)) break; + else if (run.start().isBefore(threshold)) return true; + + return false; + } + /** * Records information when a job completes (successfully or not). This information is used when deciding what to * trigger next. @@ -339,8 +360,8 @@ public class DeploymentTrigger { /** Returns the set of all jobs which have changes to propagate from the upstream steps. */ private List<Job> computeReadyJobs() { return jobs.deploymentStatuses(ApplicationList.from(applications().readable()) - .withProjectId() // Need to keep this, as we have applications with deployment spec that shouldn't be orchestrated. // Maybe not any longer? - .withDeploymentSpec()) + .withProjectId() // Need to keep this, as we have applications with deployment spec that shouldn't be orchestrated. + .withJobs()) .withChanges() .asList().stream() .filter(status -> ! hasExceededQuota(status.application().id().tenant())) diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java index 4d7b84be65e..dcdfea6e594 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java @@ -73,10 +73,12 @@ import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Nod import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.error; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed; +import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.nodeAllocationFailure; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running; +import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.testFailure; import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded; import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs; @@ -242,6 +244,10 @@ public class InternalStepRunner implements StepRunner { logger.log("Deployment failed with possibly transient error " + e.code() + ", will retry: " + e.getMessage()); return result; + case INTERNAL_SERVER_ERROR: + // Log only error code, to avoid exposing internal data in error message + logger.log("Deployment failed with possibly transient error " + e.code() + ", will retry"); + return result; case LOAD_BALANCER_NOT_READY: case PARENT_HOST_NOT_READY: logger.log(e.message()); // Consider splitting these messages in summary and details, on config server. @@ -252,6 +258,8 @@ public class InternalStepRunner implements StepRunner { ? result : Optional.of(nodeAllocationFailure); case INVALID_APPLICATION_PACKAGE: + logger.log(WARNING, e.getMessage()); + return Optional.of(invalidApplication); case BAD_REQUEST: logger.log(WARNING, e.getMessage()); return Optional.of(deploymentFailed); @@ -759,14 +767,18 @@ public class InternalStepRunner implements StepRunner { private Optional<RunStatus> report(RunId id, DualLogger logger) { try { + boolean isRemoved = ! id.type().environment().isManuallyDeployed() + && ! controller.jobController().deploymentStatus(controller.applications().requireApplication(TenantAndApplicationId.from(id.application()))) + .jobSteps().containsKey(id.job()); + controller.jobController().active(id).ifPresent(run -> { if (run.status() == reset) return; - if (run.hasFailed()) + if (run.hasFailed() && ! isRemoved) sendEmailNotification(run, logger); - updateConsoleNotification(run); + updateConsoleNotification(run, isRemoved); }); } catch (IllegalStateException e) { @@ -816,10 +828,10 @@ public class InternalStepRunner implements StepRunner { .orElse(true); } - private void updateConsoleNotification(Run run) { + private void updateConsoleNotification(Run run, boolean isRemoved) { NotificationSource source = NotificationSource.from(run.id()); Consumer<String> updater = msg -> controller.notificationsDb().setNotification(source, Notification.Type.deployment, Notification.Level.error, msg); - switch (run.status()) { + switch (isRemoved ? success : run.status()) { case aborted: return; // wait and see how the next run goes. case noTests: case running: @@ -829,9 +841,12 @@ public class InternalStepRunner implements StepRunner { case nodeAllocationFailure: if ( ! run.id().type().environment().isTest()) updater.accept("could not allocate the requested capacity to your tenant. Please contact Vespa Cloud support."); return; - case deploymentFailed: + case invalidApplication: updater.accept("invalid application configuration. Please review warnings and errors in the deployment job log."); return; + case deploymentFailed: + updater.accept("failure processing application configuration. Please review warnings and errors in the deployment job log."); + return; case installationFailed: updater.accept("nodes were not able to deploy to the new configuration. Please check the Vespa log for errors, and contact Vespa Cloud support if unable to resolve these."); return; @@ -858,6 +873,7 @@ public class InternalStepRunner implements StepRunner { case nodeAllocationFailure: return run.id().type().isProduction() ? Optional.of(mails.nodeAllocationFailure(run.id(), recipients)) : Optional.empty(); case deploymentFailed: + case invalidApplication: return Optional.of(mails.deploymentFailure(run.id(), recipients)); case installationFailed: return Optional.of(mails.installationFailure(run.id(), recipients)); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index 0d4b2c658cf..edddaa5dee7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java @@ -5,9 +5,9 @@ import com.google.common.collect.ImmutableSortedMap; import com.yahoo.component.Version; import com.yahoo.component.VersionCompatibility; import com.yahoo.concurrent.UncheckedTimeoutException; -import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.controller.Application; @@ -52,6 +52,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; @@ -126,7 +127,7 @@ public class JobController { this.curator = controller.curator(); this.logs = new BufferedLogStore(curator, controller.serviceRegistry().runDataStore()); this.cloud = controller.serviceRegistry().testerCloud(); - this.metric = new JobMetrics(controller.metric(), controller::system); + this.metric = new JobMetrics(controller.metric()); } public TesterCloud cloud() { return cloud; } @@ -320,15 +321,13 @@ public class JobController { public List<ApplicationId> instances() { return controller.applications().readable().stream() .flatMap(application -> application.instances().values().stream()) - .map(Instance::id) - .collect(toUnmodifiableList()); + .map(Instance::id).toList(); } /** Returns all job types which have been run for the given application. */ private List<JobType> jobs(ApplicationId id) { return JobType.allIn(controller.zoneRegistry()).stream() - .filter(type -> last(id, type).isPresent()) - .collect(toUnmodifiableList()); + .filter(type -> last(id, type).isPresent()).toList(); } /** Returns an immutable map of all known runs for the given application and job type. */ @@ -339,9 +338,8 @@ public class JobController { /** Lists the start time of non-redeployment runs of the given job, in order of increasing age. */ public List<Instant> jobStarts(JobId id) { return runs(id).descendingMap().values().stream() - .filter(run -> ! run.isRedeployment()) - .map(Run::start) - .collect(toUnmodifiableList()); + .filter(run -> !run.isRedeployment()) + .map(Run::start).toList(); } /** Returns when given deployment last started deploying, falling back to time of deployment if it cannot be determined from job runs */ @@ -697,7 +695,7 @@ public class JobController { controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> { if ( ! application.get().instances().containsKey(id.instance())) - application = controller.applications().withNewInstance(application, id); + application = controller.applications().withNewInstance(application, id, Tags.empty()); // TODO(mpolden): Enable for public CD once all tests have been updated if (controller.system() != SystemName.PublicCd) { controller.applications().validatePackage(applicationPackage, application.get()); @@ -767,8 +765,16 @@ public class JobController { .filter(versions::contains) // Don't deploy versions that are no longer known. .ifPresent(versions::add); - if (versions.isEmpty()) - throw new IllegalStateException("no deployable platform version found in the system"); + // Remove all versions that are older than the compile version. + versions.removeIf(version -> applicationPackage.compileVersion().map(version::isBefore).orElse(false)); + if (versions.isEmpty()) { + // Fall back to the newest deployable version, if all the ones with normal confidence were too old. + Iterator<VespaVersion> descending = reversed(versionStatus.deployableVersions()).iterator(); + if ( ! descending.hasNext()) + throw new IllegalStateException("no deployable platform version found in the system"); + else + versions.add(descending.next().versionNumber()); + } VersionCompatibility compatibility = controller.applications().versionCompatibility(id.applicationId()); List<Version> compatibleVersions = new ArrayList<>(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java index 14fce806152..d1fa00d1c41 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java @@ -19,6 +19,7 @@ public class JobMetrics { public static final String nodeAllocationFailure = "deployment.nodeAllocationFailure"; public static final String endpointCertificateTimeout = "deployment.endpointCertificateTimeout"; public static final String deploymentFailure = "deployment.deploymentFailure"; + public static final String invalidApplication = "deployment.invalidApplication"; public static final String convergenceFailure = "deployment.convergenceFailure"; public static final String testFailure = "deployment.testFailure"; public static final String noTests = "deployment.noTests"; @@ -27,11 +28,9 @@ public class JobMetrics { public static final String success = "deployment.success"; private final Metric metric; - private final Supplier<SystemName> system; - public JobMetrics(Metric metric, Supplier<SystemName> system) { + public JobMetrics(Metric metric) { this.metric = metric; - this.system = system; } public void jobStarted(JobId id) { @@ -51,18 +50,19 @@ public class JobMetrics { } static String valueOf(RunStatus status) { - switch (status) { - case nodeAllocationFailure: return nodeAllocationFailure; - case endpointCertificateTimeout: return endpointCertificateTimeout; - case deploymentFailed: return deploymentFailure; - case installationFailed: return convergenceFailure; - case testFailure: return testFailure; - case noTests: return noTests; - case error: return error; - case aborted: return abort; - case success: return success; - default: throw new IllegalArgumentException("Unexpected run status '" + status + "'"); - } + return switch (status) { + case nodeAllocationFailure -> nodeAllocationFailure; + case endpointCertificateTimeout -> endpointCertificateTimeout; + case invalidApplication -> invalidApplication; + case deploymentFailed -> deploymentFailure; + case installationFailed -> convergenceFailure; + case testFailure -> testFailure; + case noTests -> noTests; + case error -> error; + case aborted -> abort; + case success -> success; + default -> throw new IllegalArgumentException("Unexpected run status '" + status + "'"); + }; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java index 9ca634b19fd..aa727b602e1 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java @@ -14,7 +14,10 @@ public enum RunStatus { /** Deployment was rejected due node allocation failure. */ nodeAllocationFailure, - /** Deployment of the real application was rejected. */ + /** Deployment of the real application was rejected because the package is faulty. */ + invalidApplication, + + /** Deployment of the real application was rejected, for other reasons. */ deploymentFailed, /** Deployment timed out waiting for endpoint certificate */ diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java index d0c901ccb36..b97fdde560e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.dns; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; @@ -28,8 +29,8 @@ public class CreateRecords implements NameServiceRequest { this.name = requireOneOf(Record::name, records); this.type = requireOneOf(Record::type, records); this.records = List.copyOf(Objects.requireNonNull(records, "records must be non-null")); - if (type != Record.Type.ALIAS && type != Record.Type.TXT) { - throw new IllegalArgumentException("Records of type " + type + "are not supported: " + records); + if (type != Record.Type.ALIAS && type != Record.Type.TXT && type != Record.Type.DIRECT) { + throw new IllegalArgumentException("Records of type " + type + " are not supported: " + records); } } @@ -40,14 +41,18 @@ public class CreateRecords implements NameServiceRequest { @Override public void dispatchTo(NameService nameService) { switch (type) { - case ALIAS: + case ALIAS -> { var targets = records.stream().map(Record::data).map(AliasTarget::unpack).collect(Collectors.toSet()); nameService.createAlias(name, targets); - break; - case TXT: + } + case DIRECT -> { + var targets = records.stream().map(Record::data).map(DirectTarget::unpack).collect(Collectors.toSet()); + nameService.createDirect(name, targets); + } + case TXT -> { var dataFields = records.stream().map(Record::data).collect(Collectors.toList()); nameService.createTxtRecords(name, dataFields); - break; + } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java index 9d2c7918252..57c83280b8b 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.dns; import com.yahoo.transaction.Mutex; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; @@ -55,6 +56,14 @@ public class NameServiceForwarder { forward(new CreateRecords(records), priority); } + /** Create or update a DIRECT record with given name and targets */ + public void createDirect(RecordName name, Set<DirectTarget> targets, NameServiceQueue.Priority priority) { + var records = targets.stream() + .map(target -> new Record(Record.Type.DIRECT, name, target.pack())) + .collect(Collectors.toList()); + forward(new CreateRecords(records), priority); + } + /** Create or update a TXT record with given name and data */ public void createTxt(RecordName name, List<RecordData> txtData, NameServiceQueue.Priority priority) { var records = txtData.stream() diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java index f940d53fab3..6fa7473ad1e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java @@ -2,6 +2,7 @@ package com.yahoo.vespa.hosted.controller.dns; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; @@ -69,11 +70,11 @@ public class RemoveRecords implements NameServiceRequest { .stream() .filter(record -> { // Records to remove must match both name and data fields - String dataValue = record.data().asString(); - // If we're comparing an ALIAS record we have to unpack it to access the target name - if (record.type() == Record.Type.ALIAS) { - dataValue = AliasTarget.unpack(record.data()).name().value(); - } + String dataValue = switch (record.type()) { + case ALIAS -> AliasTarget.unpack(record.data()).name().value(); + case DIRECT -> DirectTarget.unpack(record.data()).recordData().asString(); + default -> record.data().asString(); + }; return fqdn(dataValue).equals(fqdn(data.get().asString())); }) .forEach(records::add); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java index 42821ea8fe2..0c8a50fa821 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java @@ -8,6 +8,7 @@ import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository; import com.yahoo.vespa.hosted.controller.application.SystemApplication; import com.yahoo.vespa.hosted.controller.archive.CuratorArchiveBucketDb; +import com.yahoo.yolean.Exceptions; import java.net.URI; import java.time.Duration; @@ -15,6 +16,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.logging.Level; /** * Updates archive URIs for tenants in all zones. @@ -51,20 +53,28 @@ public class ArchiveUriUpdater extends ControllerMaintainer { } } - tenantsByZone.forEach((zone, tenants) -> { - Map<TenantName, URI> zoneArchiveUris = nodeRepository.getArchiveUris(zone); - for (TenantName tenant : tenants) { - archiveBucketDb.archiveUriFor(zone, tenant, true) - .filter(uri -> !uri.equals(zoneArchiveUris.get(tenant))) - .ifPresent(uri -> nodeRepository.setArchiveUri(zone, tenant, uri)); - } + int failures = 0; + for (ZoneId zone : tenantsByZone.keySet()) { + try { + Map<TenantName, URI> zoneArchiveUris = nodeRepository.getArchiveUris(zone); + + for (TenantName tenant : tenantsByZone.get(zone)) { + archiveBucketDb.archiveUriFor(zone, tenant, true) + .filter(uri -> !uri.equals(zoneArchiveUris.get(tenant))) + .ifPresent(uri -> nodeRepository.setArchiveUri(zone, tenant, uri)); + } - zoneArchiveUris.keySet().stream() - .filter(tenant -> !tenants.contains(tenant)) - .forEach(tenant -> nodeRepository.removeArchiveUri(zone, tenant)); - }); + zoneArchiveUris.keySet().stream() + .filter(tenant -> !tenantsByZone.get(zone).contains(tenant)) + .forEach(tenant -> nodeRepository.removeArchiveUri(zone, tenant)); + } catch (Exception e) { + log.log(Level.WARNING, "Failed to update archive URI in " + zone + ". Retrying in " + interval() + ". Error: " + + Exceptions.toMessageString(e)); + failures++; + } + } - return 1.0; + return asSuccessFactor(tenantsByZone.size(), failures); } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java index 16f62c37ffa..502b3b0188f 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java @@ -55,6 +55,7 @@ public class OsUpgradeScheduler extends ControllerMaintainer { private boolean upgradingToNewMajor(CloudName cloud) { return controller().osVersionStatus().versionsIn(cloud).stream() + .filter(version -> !version.isEmpty()) // Ignore empty/unknown versions .map(Version::getMajor) .distinct() .count() > 1; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java index b33a43a2031..37b06fea066 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java @@ -26,9 +26,8 @@ public class OutstandingChangeDeployer extends ControllerMaintainer { protected double maintain() { double ok = 0, total = 0; for (Application application : ApplicationList.from(controller().applications().readable()) - .withProductionDeployment() .withProjectId() - .withDeploymentSpec() + .withJobs() .asList()) try { ++total; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java index 037dacfcac9..d49cb244e47 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java @@ -70,7 +70,8 @@ public class Upgrader extends ControllerMaintainer { private DeploymentStatusList deploymentStatuses(VersionStatus versionStatus) { return controller().jobController().deploymentStatuses(ApplicationList.from(controller().applications().readable()) - .withProjectId(), + .withProjectId() + .withJobs(), versionStatus); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java index 5aa847f648a..37c45f38e36 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java @@ -9,6 +9,7 @@ import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyUtils; import com.yahoo.slime.ArrayTraverser; @@ -97,6 +98,7 @@ public class ApplicationSerializer { // Instance fields private static final String instanceNameField = "instanceName"; + private static final String tagsField = "tags"; private static final String deploymentsField = "deployments"; private static final String deploymentJobsField = "deploymentJobs"; // TODO jonmv: clean up serialisation format private static final String assignedRotationsField = "assignedRotations"; @@ -184,6 +186,7 @@ public class ApplicationSerializer { for (Instance instance : application.instances().values()) { Cursor instanceObject = array.addObject(); instanceObject.setString(instanceNameField, instance.name().value()); + instanceObject.setString(tagsField, instance.tags().asString()); deploymentsToSlime(instance.deployments().values(), instanceObject.setArray(deploymentsField)); toSlime(instance.jobPauses(), instanceObject.setObject(deploymentJobsField)); assignedRotationsToSlime(instance.rotations(), instanceObject); @@ -380,12 +383,14 @@ public class ApplicationSerializer { List<Instance> instances = new ArrayList<>(); field.traverse((ArrayTraverser) (name, object) -> { InstanceName instanceName = InstanceName.from(object.field(instanceNameField).asString()); - List<Deployment> deployments = deploymentsFromSlime(object.field(deploymentsField), id.instance(instanceName)); + Tags tags = Tags.fromString(object.field(tagsField).asString()); + List < Deployment > deployments = deploymentsFromSlime(object.field(deploymentsField), id.instance(instanceName)); Map<JobType, Instant> jobPauses = jobPausesFromSlime(object.field(deploymentJobsField)); List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(object); RotationStatus rotationStatus = rotationStatusFromSlime(object); Change change = changeFromSlime(object.field(deployingField)); instances.add(new Instance(id.instance(instanceName), + tags, deployments, jobPauses, assignedRotations, diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java index fcc6d99aec2..49d108d08df 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java @@ -34,6 +34,7 @@ import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentF import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.endpointCertificateTimeout; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.error; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed; +import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.nodeAllocationFailure; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset; @@ -329,39 +330,38 @@ class RunSerializer { } static String valueOf(RunStatus status) { - switch (status) { - case running : return "running"; - case nodeAllocationFailure : return "nodeAllocationFailure"; - case endpointCertificateTimeout : return "endpointCertificateTimeout"; - case deploymentFailed : return "deploymentFailed"; - case installationFailed : return "installationFailed"; - case testFailure : return "testFailure"; - case noTests : return "noTests"; - case error : return "error"; - case success : return "success"; - case aborted : return "aborted"; - case reset : return "reset"; - - default: throw new AssertionError("No value defined for '" + status + "'!"); - } + return switch (status) { + case running -> "running"; + case nodeAllocationFailure -> "nodeAllocationFailure"; + case endpointCertificateTimeout -> "endpointCertificateTimeout"; + case deploymentFailed -> "deploymentFailed"; + case invalidApplication -> "invalidApplication"; + case installationFailed -> "installationFailed"; + case testFailure -> "testFailure"; + case noTests -> "noTests"; + case error -> "error"; + case success -> "success"; + case aborted -> "aborted"; + case reset -> "reset"; + }; } static RunStatus runStatusOf(String status) { - switch (status) { - case "running" : return running; - case "nodeAllocationFailure" : return nodeAllocationFailure; - case "endpointCertificateTimeout" : return endpointCertificateTimeout; - case "deploymentFailed" : return deploymentFailed; - case "installationFailed" : return installationFailed; - case "noTests" : return noTests; - case "testFailure" : return testFailure; - case "error" : return error; - case "success" : return success; - case "aborted" : return aborted; - case "reset" : return reset; - - default: throw new IllegalArgumentException("No run status defined by '" + status + "'!"); - } + return switch (status) { + case "running" -> running; + case "nodeAllocationFailure" -> nodeAllocationFailure; + case "endpointCertificateTimeout" -> endpointCertificateTimeout; + case "deploymentFailed" -> deploymentFailed; + case "invalidApplication" -> invalidApplication; + case "installationFailed" -> installationFailed; + case "noTests" -> noTests; + case "testFailure" -> testFailure; + case "error" -> error; + case "success" -> success; + case "aborted" -> aborted; + case "reset" -> reset; + default -> throw new IllegalArgumentException("No run status defined by '" + status + "'!"); + }; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index e8fe7b7d5f0..81fb72e19fd 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -22,6 +22,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NodeResources; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneId; @@ -2035,7 +2036,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler { if (controller.applications().getApplication(applicationId).isEmpty()) createApplication(tenantName, applicationName, request); - controller.applications().createInstance(applicationId.instance(instanceName)); + controller.applications().createInstance(applicationId.instance(instanceName), Tags.empty()); Slime slime = new Slime(); toSlime(applicationId.instance(instanceName), slime.setObject(), request); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java index 8c601f8c678..592fbd0e856 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java @@ -227,20 +227,18 @@ class JobControllerApiHandlerHelper { } private static String nameOf(RunStatus status) { - switch (status) { - case reset: // This means the run will reset and keep running. - case running: return "running"; - case aborted: return "aborted"; - case error: return "error"; - case testFailure: return "testFailure"; - case noTests: return "noTests"; - case endpointCertificateTimeout: return "endpointCertificateTimeout"; - case nodeAllocationFailure: return "nodeAllocationFailure"; - case installationFailed: return "installationFailed"; - case deploymentFailed: return "deploymentFailed"; - case success: return "success"; - default: throw new IllegalArgumentException("Unexpected status '" + status + "'"); - } + return switch (status) { + case reset, running -> "running"; + case aborted -> "aborted"; + case error -> "error"; + case testFailure -> "testFailure"; + case noTests -> "noTests"; + case endpointCertificateTimeout -> "endpointCertificateTimeout"; + case nodeAllocationFailure -> "nodeAllocationFailure"; + case installationFailed -> "installationFailed"; + case invalidApplication, deploymentFailed -> "deploymentFailed"; + case success -> "success"; + }; } /** @@ -440,7 +438,7 @@ class JobControllerApiHandlerHelper { runObject.setString("url", baseUriForJob.resolve(baseUriForJob.getPath() + "/run/" + run.id().number()).toString()); runObject.setLong("start", run.start().toEpochMilli()); run.end().ifPresent(end -> runObject.setLong("end", end.toEpochMilli())); - runObject.setString("status", run.status().name()); + runObject.setString("status", nameOf(run.status())); run.reason().ifPresent(reason -> runObject.setString("reason", reason)); toSlime(runObject.setObject("versions"), run.versions(), application); Cursor runStepsArray = runObject.setArray("steps"); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java index 62b48307f37..999cf63abf6 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java @@ -22,6 +22,7 @@ import com.yahoo.vespa.hosted.controller.application.Change; import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus; import com.yahoo.vespa.hosted.controller.deployment.Run; +import com.yahoo.vespa.hosted.controller.deployment.RunStatus; import com.yahoo.vespa.hosted.controller.deployment.Versions; import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses; import com.yahoo.vespa.hosted.controller.restapi.application.EmptyResponse; @@ -121,7 +122,7 @@ public class DeploymentApiHandler extends ThreadedHttpRequestHandler { Cursor applicationObject = failingArray.addObject(); toSlime(applicationObject, run.id().application(), request); applicationObject.setString("failing", run.id().type().jobName()); - applicationObject.setString("status", run.status().name()); + applicationObject.setString("status", nameOf(run.status())); } var statusByInstance = deploymentStatuses.asList().stream() @@ -224,7 +225,7 @@ public class DeploymentApiHandler extends ThreadedHttpRequestHandler { runObject.setLong("number", run.id().number()); runObject.setLong("start", run.start().toEpochMilli()); run.end().ifPresent(end -> runObject.setLong("end", end.toEpochMilli())); - runObject.setString("status", run.status().name()); + runObject.setString("status", nameOf(run.status())); } private void toSlime(Cursor object, ApplicationId id, HttpRequest request) { @@ -247,6 +248,21 @@ public class DeploymentApiHandler extends ThreadedHttpRequestHandler { return upgradePolicy.name(); } + public static String nameOf(RunStatus status) { + return switch (status) { + case reset, running -> "running"; + case aborted -> "aborted"; + case error -> "error"; + case testFailure -> "testFailure"; + case noTests -> "noTests"; + case endpointCertificateTimeout -> "endpointCertificateTimeout"; + case nodeAllocationFailure -> "nodeAllocationFailure"; + case installationFailed -> "installationFailed"; + case invalidApplication, deploymentFailed -> "deploymentFailed"; + case success -> "success"; + }; + } + private static class RunInfo { final Run run; final boolean upgrade; diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java index f76d04c9e1d..fc0badae9ea 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java @@ -12,11 +12,13 @@ import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId; import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer; import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedAliasTarget; +import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedDirectTarget; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.EndpointList; @@ -34,6 +36,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -169,9 +172,16 @@ public class RoutingPolicies { // Create a weighted ALIAS per region, pointing to all zones within the same region Collection<RegionEndpoint> regionEndpoints = computeRegionEndpoints(policies, inactiveZones); regionEndpoints.forEach(regionEndpoint -> { - controller.nameServiceForwarder().createAlias(RecordName.from(regionEndpoint.target().name().value()), - Collections.unmodifiableSet(regionEndpoint.zoneTargets()), - Priority.normal); + if ( ! regionEndpoint.zoneAliasTargets().isEmpty()) { + controller.nameServiceForwarder().createAlias(RecordName.from(regionEndpoint.target().name().value()), + regionEndpoint.zoneAliasTargets(), + Priority.normal); + } + if ( ! regionEndpoint.zoneDirectTargets().isEmpty()) { + controller.nameServiceForwarder().createDirect(RecordName.from(regionEndpoint.target().name().value()), + regionEndpoint.zoneDirectTargets(), + Priority.normal); + } }); // Create global latency-based ALIAS pointing to each per-region weighted ALIAS @@ -203,7 +213,7 @@ public class RoutingPolicies { private Collection<RegionEndpoint> computeRegionEndpoints(List<RoutingPolicy> policies, Set<ZoneId> inactiveZones) { Map<Endpoint, RegionEndpoint> endpoints = new LinkedHashMap<>(); for (var policy : policies) { - if (policy.dnsZone().isEmpty()) continue; + if (policy.dnsZone().isEmpty() && policy.canonicalName().isPresent()) continue; if (controller.zoneRegistry().routingMethod(policy.id().zone()) != RoutingMethod.exclusive) continue; Endpoint endpoint = policy.regionEndpointIn(controller.system(), RoutingMethod.exclusive); var zonePolicy = db.readZoneRoutingPolicy(policy.id().zone()); @@ -219,9 +229,11 @@ public class RoutingPolicies { if (policy.canonicalName().isPresent()) { var weightedTarget = new WeightedAliasTarget( policy.canonicalName().get(), policy.dnsZone().get(), policy.id().zone(), weight); - regionEndpoint.zoneTargets().add(weightedTarget); + regionEndpoint.add(weightedTarget); } else { - // TODO (freva): Add direct weighted record + var weightedTarget = new WeightedDirectTarget( + RecordData.from(policy.ipAddress().get()), policy.id().zone(), weight); + regionEndpoint.add(weightedTarget); } } return endpoints.values(); @@ -237,8 +249,8 @@ public class RoutingPolicies { if (routingTable.isEmpty()) return; Application application = controller.applications().requireApplication(routingTable.keySet().iterator().next().application()); - Map<Endpoint, Set<AliasTarget>> targetsByEndpoint = new LinkedHashMap<>(); - Map<Endpoint, Set<AliasTarget>> inactiveTargetsByEndpoint = new LinkedHashMap<>(); + Map<Endpoint, Set<Target>> targetsByEndpoint = new LinkedHashMap<>(); + Map<Endpoint, Set<Target>> inactiveTargetsByEndpoint = new LinkedHashMap<>(); for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) { RoutingId routingId = routeEntry.getKey(); EndpointList endpoints = controller.routing().declaredEndpointsOf(application) @@ -253,17 +265,15 @@ public class RoutingPolicies { for (var policy : routeEntry.getValue()) { for (var target : endpoint.targets()) { if (!policy.appliesTo(target.deployment())) continue; - if (policy.dnsZone().isEmpty()) continue; // Does not support ALIAS records - if (policy.canonicalName().isEmpty()) continue; // TODO (freva): Handle DIRECT records + if (policy.dnsZone().isEmpty() && policy.canonicalName().isPresent()) continue; // Does not support ALIAS records ZoneRoutingPolicy zonePolicy = db.readZoneRoutingPolicy(policy.id().zone()); - WeightedAliasTarget weightedAliasTarget = new WeightedAliasTarget(policy.canonicalName().get(), policy.dnsZone().get(), - target.deployment().zoneId(), target.weight()); - Set<AliasTarget> activeTargets = targetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); - Set<AliasTarget> inactiveTargets = inactiveTargetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); + + Set<Target> activeTargets = targetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); + Set<Target> inactiveTargets = inactiveTargetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>()); if (isConfiguredOut(zonePolicy, policy, inactiveZones)) { - inactiveTargets.add(weightedAliasTarget); + inactiveTargets.add(Target.weighted(policy, target)); } else { - activeTargets.add(weightedAliasTarget); + activeTargets.add(Target.weighted(policy, target)); } } } @@ -273,11 +283,11 @@ public class RoutingPolicies { // the ALIAS records would cause the application endpoint to stop resolving entirely (NXDOMAIN). for (var kv : targetsByEndpoint.entrySet()) { Endpoint endpoint = kv.getKey(); - Set<AliasTarget> activeTargets = kv.getValue(); + Set<Target> activeTargets = kv.getValue(); if (!activeTargets.isEmpty()) { continue; } - Set<AliasTarget> inactiveTargets = inactiveTargetsByEndpoint.get(endpoint); + Set<Target> inactiveTargets = inactiveTargetsByEndpoint.get(endpoint); activeTargets.addAll(inactiveTargets); inactiveTargets.clear(); } @@ -287,9 +297,21 @@ public class RoutingPolicies { .map(DeploymentId::zoneId) .findFirst() .get(); - nameServiceForwarderIn(targetZone).createAlias(RecordName.from(applicationEndpoint.dnsName()), - targets, - Priority.normal); + Set<AliasTarget> aliasTargets = new LinkedHashSet<>(); + Set<DirectTarget> directTargets = new LinkedHashSet<>(); + for (Target target : targets) { + if (target.aliasOrDirectTarget() instanceof AliasTarget at) aliasTargets.add(at); + else directTargets.add((DirectTarget) target.aliasOrDirectTarget()); + } + + if ( ! aliasTargets.isEmpty()) { + nameServiceForwarderIn(targetZone).createAlias( + RecordName.from(applicationEndpoint.dnsName()), aliasTargets, Priority.normal); + } + if ( ! directTargets.isEmpty()) { + nameServiceForwarderIn(targetZone).createDirect( + RecordName.from(applicationEndpoint.dnsName()), directTargets, Priority.normal); + } }); inactiveTargetsByEndpoint.forEach((applicationEndpoint, targets) -> { ZoneId targetZone = applicationEndpoint.targets().stream() @@ -298,9 +320,9 @@ public class RoutingPolicies { .findFirst() .get(); targets.forEach(target -> { - nameServiceForwarderIn(targetZone).removeRecords(Record.Type.ALIAS, + nameServiceForwarderIn(targetZone).removeRecords(target.type(), RecordName.from(applicationEndpoint.dnsName()), - RecordData.fqdn(target.name().value()), + target.data(), Priority.normal); }); }); @@ -317,7 +339,8 @@ public class RoutingPolicies { if (loadBalancer.hostname().isEmpty() && loadBalancer.ipAddress().isEmpty()) continue; var policyId = new RoutingPolicyId(loadBalancer.application(), loadBalancer.cluster(), allocation.deployment.zoneId()); var existingPolicy = policies.get(policyId); - var newPolicy = new RoutingPolicy(policyId, loadBalancer.hostname(), loadBalancer.ipAddress(), loadBalancer.dnsZone(), + var dnsZone = loadBalancer.ipAddress().isPresent() ? Optional.of("ignored") : loadBalancer.dnsZone(); + var newPolicy = new RoutingPolicy(policyId, loadBalancer.hostname(), loadBalancer.ipAddress(), dnsZone, allocation.instanceEndpointsOf(loadBalancer), allocation.applicationEndpointsOf(loadBalancer), new RoutingPolicy.Status(isActive(loadBalancer), RoutingStatus.DEFAULT)); @@ -407,7 +430,10 @@ public class RoutingPolicies { RecordData.fqdn(policy.canonicalName().get().value()), Priority.normal); } else { - // TODO (freva): Remove DIRECT records + forwarder.removeRecords(Record.Type.DIRECT, + RecordName.from(endpoint.dnsName()), + RecordData.from(policy.ipAddress().get()), + Priority.normal); } } } @@ -460,22 +486,23 @@ public class RoutingPolicies { private static class RegionEndpoint { private final LatencyAliasTarget target; - private final Set<WeightedAliasTarget> zoneTargets = new LinkedHashSet<>(); + private final Set<WeightedAliasTarget> zoneAliasTargets = new LinkedHashSet<>(); + private final Set<WeightedDirectTarget> zoneDirectTargets = new LinkedHashSet<>(); public RegionEndpoint(LatencyAliasTarget target) { this.target = Objects.requireNonNull(target); } - public LatencyAliasTarget target() { - return target; - } + public LatencyAliasTarget target() { return target; } + public Set<AliasTarget> zoneAliasTargets() { return Collections.unmodifiableSet(zoneAliasTargets); } + public Set<DirectTarget> zoneDirectTargets() { return Collections.unmodifiableSet(zoneDirectTargets); } - public Set<WeightedAliasTarget> zoneTargets() { - return zoneTargets; - } + public void add(WeightedAliasTarget target) { zoneAliasTargets.add(target); } + public void add(WeightedDirectTarget target) { zoneDirectTargets.add(target); } public boolean active() { - return zoneTargets.stream().anyMatch(target -> target.weight() > 0); + return zoneAliasTargets.stream().anyMatch(target -> target.weight() > 0) || + zoneDirectTargets.stream().anyMatch(target -> target.weight() > 0); } @Override @@ -573,6 +600,20 @@ public class RoutingPolicies { }; } + /** Denotes record data (record rhs) of either an ALIAS or a DIRECT target */ + private record Target(Record.Type type, RecordData data, Object aliasOrDirectTarget) { + static Target weighted(RoutingPolicy policy, Endpoint.Target endpointTarget) { + if (policy.ipAddress().isPresent()) { + var wt = new WeightedDirectTarget(RecordData.from(policy.ipAddress().get()), + endpointTarget.deployment().zoneId(), endpointTarget.weight()); + return new Target(Record.Type.DIRECT, wt.recordData(), wt); + } + var wt = new WeightedAliasTarget(policy.canonicalName().get(), policy.dnsZone().get(), + endpointTarget.deployment().zoneId(), endpointTarget.weight()); + return new Target(Record.Type.ALIAS, RecordData.fqdn(wt.name().value()), wt); + } + } + /** A {@link NameServiceForwarder} that does nothing. Used in zones where no explicit DNS updates are needed */ private static class NameServiceDiscarder extends NameServiceForwarder { diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java index 46bcd3b85c0..127e2828732 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java @@ -124,12 +124,12 @@ public record VespaVersion(Version version, /** We don't have sufficient evidence that this version is working */ low, - /** This version works, but we want users to stop using it */ - legacy, - /** We have sufficient evidence that this version is working */ normal, - + + /** This version works, but we want users to stop using it */ + legacy, + /** We have overwhelming evidence that this version is working */ high; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java index c4e138c4d18..cd3d6ca7531 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -912,38 +912,6 @@ public class ControllerTest { } @Test - void testDeployWithGlobalEndpointsInGcp() { - tester.controllerTester().zoneRegistry().setZones( - ZoneApiMock.fromId("test.us-west-1"), - ZoneApiMock.fromId("staging.us-west-1"), - ZoneApiMock.newBuilder().with(CloudName.GCP).withId("prod.gcp-us-east1-b").build() - ); - var context = tester.newDeploymentContext(); - var applicationPackage = new ApplicationPackageBuilder() - .region("gcp-us-east1-b") - .endpoint("default", "default") // Contains all regions by default - .build(); - - try { - context.submit(applicationPackage); - fail("Expected exception"); - } catch (IllegalArgumentException e) { - assertEquals("Endpoint 'default' in instance 'default' contains a Google Cloud region (gcp-us-east1-b), which is not yet supported", e.getMessage()); - } - - var applicationPackage2 = new ApplicationPackageBuilder() - .region("gcp-us-east1-b") - .endpoint("gcp", "default", "gcp-us-east1-b") - .build(); - try { - context.submit(applicationPackage2); - fail("Expected exception"); - } catch (IllegalArgumentException e) { - assertEquals("Endpoint 'gcp' in instance 'default' contains a Google Cloud region (gcp-us-east1-b), which is not yet supported", e.getMessage()); - } - } - - @Test void testDeployWithoutSourceRevision() { var context = tester.newDeploymentContext(); var applicationPackage = new ApplicationPackageBuilder() diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java index 5380cf4ee27..0c908b1f035 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java @@ -7,6 +7,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.HostName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneApi; @@ -370,7 +371,7 @@ public final class ControllerTester { public Application createApplication(String tenant, String applicationName, String instanceName) { Application application = createApplication(tenant, applicationName); - controller().applications().createInstance(application.id().instance(instanceName)); + controller().applications().createInstance(application.id().instance(instanceName), Tags.empty()); return application; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java index 3163c8b6439..db45933f498 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java @@ -8,6 +8,7 @@ import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Instance; @@ -63,8 +64,8 @@ public class DeploymentQuotaCalculatorTest { var existing_dev_deployment = new Application(TenantAndApplicationId.from(ApplicationId.defaultId()), Instant.EPOCH, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(1, 1), Set.of(), OptionalLong.empty(), RevisionHistory.empty(), - List.of(new Instance(ApplicationId.defaultId()).withNewDeployment(ZoneId.from(Environment.dev, RegionName.defaultName()), - RevisionId.forProduction(1), Version.emptyVersion, Instant.EPOCH, Map.of(), QuotaUsage.create(0.53d)))); + List.of(new Instance(ApplicationId.defaultId(), Tags.empty()).withNewDeployment(ZoneId.from(Environment.dev, RegionName.defaultName()), + RevisionId.forProduction(1), Version.emptyVersion, Instant.EPOCH, Map.of(), QuotaUsage.create(0.53d)))); Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited().withBudget(2), List.of(existing_dev_deployment), ApplicationId.defaultId(), ZoneId.defaultId(), DeploymentSpec.fromXml( diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java index 9a2d9eddba7..274cf3c5867 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java @@ -8,6 +8,7 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyAlgorithm; @@ -100,7 +101,7 @@ public class EndpointCertificatesTest { return x509CertificateBuilder.build(); } - private final Instance testInstance = new Instance(ApplicationId.defaultId()); + private final Instance testInstance = new Instance(ApplicationId.defaultId(), Tags.empty()); private final String testKeyName = "testKeyName"; private final String testCertName = "testCertName"; private ZoneId testZone; @@ -234,7 +235,7 @@ public class EndpointCertificatesTest { @Test void includes_application_endpoint_when_declared() { - Instance instance = new Instance(ApplicationId.from("t1", "a1", "default")); + Instance instance = new Instance(ApplicationId.from("t1", "a1", "default"), Tags.empty()); ZoneId zone1 = ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1c")); ZoneId zone2 = ZoneId.from(Environment.prod, RegionName.from("aws-us-west-2a")); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java index d8cef45f124..73630488969 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java @@ -8,12 +8,15 @@ import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.RoutingMethod; import com.yahoo.config.provision.zone.ZoneApi; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.vespa.flags.PermanentFlags; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.Instance; +import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException; +import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException.ErrorCode; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId; @@ -118,9 +121,17 @@ public class DeploymentTriggerTest { tester.triggerJobs(); app.assertRunning(productionUsWest1); + tester.configServer().throwOnNextPrepare(new ConfigServerException(ErrorCode.INVALID_APPLICATION_PACKAGE, "nope", "bah")); + tester.runner().run(); + assertEquals(RunStatus.invalidApplication, tester.jobs().last(app.instanceId(), productionUsWest1).get().status()); + tester.triggerJobs(); + app.assertNotRunning(productionUsWest1); + // production-us-west-1 fails, but the app loses its projectId, and the job isn't retried. + app.submit(applicationPackage).runJob(systemTest).runJob(stagingTest).triggerJobs(); tester.applications().lockApplicationOrThrow(app.application().id(), locked -> tester.applications().store(locked.withProjectId(OptionalLong.empty()))); + app.timeOutConvergence(productionUsWest1); tester.triggerJobs(); assertEquals(0, tester.jobs().active().size(), "Job is not triggered when no projectId is present"); @@ -986,7 +997,7 @@ public class DeploymentTriggerTest { @Test void testUserInstancesNotInDeploymentSpec() { var app = tester.newDeploymentContext(); - tester.controller().applications().createInstance(app.application().id().instance("user")); + tester.controller().applications().createInstance(app.application().id().instance("user"), Tags.empty()); app.submit().deploy(); } @@ -2700,6 +2711,54 @@ public class DeploymentTriggerTest { } @Test + void testBrokenApplication() { + DeploymentContext app = tester.newDeploymentContext(); + app.submit().runJob(systemTest).failDeployment(stagingTest).failDeployment(stagingTest); + tester.clock().advance(Duration.ofDays(31)); + tester.outstandingChangeDeployer().run(); + assertEquals(OptionalLong.empty(), app.application().projectId()); + + app.assertNotRunning(stagingTest); + tester.triggerJobs(); + app.assertNotRunning(stagingTest); + assertEquals(4, app.deploymentStatus().jobsToRun().size()); + + app.submit().runJob(systemTest).failDeployment(stagingTest); + tester.clock().advance(Duration.ofDays(20)); + app.submit().runJob(systemTest).failDeployment(stagingTest); + tester.clock().advance(Duration.ofDays(20)); + tester.outstandingChangeDeployer().run(); + assertEquals(OptionalLong.of(1000), app.application().projectId()); + tester.clock().advance(Duration.ofDays(20)); + tester.outstandingChangeDeployer().run(); + assertEquals(OptionalLong.empty(), app.application().projectId()); + + app.assertNotRunning(stagingTest); + tester.triggerJobs(); + app.assertNotRunning(stagingTest); + assertEquals(4, app.deploymentStatus().jobsToRun().size()); + + app.submit().runJob(systemTest).runJob(stagingTest).failDeployment(productionUsCentral1); + tester.clock().advance(Duration.ofDays(31)); + tester.outstandingChangeDeployer().run(); + assertEquals(OptionalLong.empty(), app.application().projectId()); + + app.assertNotRunning(productionUsCentral1); + tester.triggerJobs(); + app.assertNotRunning(productionUsCentral1); + assertEquals(3, app.deploymentStatus().jobsToRun().size()); + + app.submit().runJob(systemTest).runJob(stagingTest).timeOutConvergence(productionUsCentral1); + tester.clock().advance(Duration.ofDays(31)); + tester.outstandingChangeDeployer().run(); + assertEquals(OptionalLong.of(1000), app.application().projectId()); + + app.assertNotRunning(productionUsCentral1); + tester.triggerJobs(); + app.assertRunning(productionUsCentral1); + } + + @Test void testJobNames() { ZoneRegistryMock zones = new ZoneRegistryMock(SystemName.main); List<ZoneApi> existing = new ArrayList<>(zones.zones().all().zones()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java index 849261d5ae4..40217890351 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java @@ -6,6 +6,7 @@ import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.ValidationOverrides; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.SystemName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import com.yahoo.security.KeyUtils; import com.yahoo.slime.SlimeUtils; @@ -127,18 +128,21 @@ public class ApplicationSerializerTest { RevisionHistory revisions = RevisionHistory.ofRevisions(List.of(applicationVersion1), Map.of(new JobId(id1, DeploymentContext.productionUsEast3), List.of(applicationVersion2))); - List<Instance> instances = List.of(new Instance(id1, - deployments, - Map.of(DeploymentContext.systemTest, Instant.ofEpochMilli(333)), - List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of("us-west-1"))), - rotationStatus, - Change.of(new Version("6.1"))), - new Instance(id3, - List.of(), - Map.of(), - List.of(), - RotationStatus.EMPTY, - Change.of(Version.fromString("6.7")).withPin())); + List<Instance> instances = + List.of(new Instance(id1, + Tags.fromString("tag1 tag2"), + deployments, + Map.of(DeploymentContext.systemTest, Instant.ofEpochMilli(333)), + List.of(AssignedRotation.fromStrings("foo", "default", "my-rotation", Set.of("us-west-1"))), + rotationStatus, + Change.of(new Version("6.1"))), + new Instance(id3, + Tags.empty(), + List.of(), + Map.of(), + List.of(), + RotationStatus.EMPTY, + Change.of(Version.fromString("6.7")).withPin())); Application original = new Application(TenantAndApplicationId.from(id1), Instant.now().truncatedTo(ChronoUnit.MILLIS), @@ -177,6 +181,9 @@ public class ApplicationSerializerTest { assertEquals(original.revisions().production(), serialized.revisions().production()); assertEquals(original.revisions().development(), serialized.revisions().development()); + assertEquals(original.require(id1.instance()).tags(), serialized.require(id1.instance()).tags()); + assertEquals(original.require(id3.instance()).tags(), serialized.require(id3.instance()).tags()); + assertEquals(original.deploymentSpec().xmlForm(), serialized.deploymentSpec().xmlForm()); assertEquals(original.validationOverrides().xmlForm(), serialized.validationOverrides().xmlForm()); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java index 6f0a36690ed..c42d5621a46 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java @@ -7,6 +7,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget; import com.yahoo.vespa.hosted.controller.api.integration.dns.Record; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; +import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedDirectTarget; import com.yahoo.vespa.hosted.controller.dns.CreateRecord; import com.yahoo.vespa.hosted.controller.dns.CreateRecords; import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue; @@ -38,8 +39,16 @@ public class NameServiceQueueSerializerTest { new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"), new LatencyAliasTarget(HostName.of("alias2"), "dns-zone-02", - ZoneId.from("prod", "us-north-2")).pack())) + ZoneId.from("prod", "us-north-2")).pack()), + new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"), + new LatencyAliasTarget(HostName.of("alias2"), + "ignored", + ZoneId.from("prod", "us-south-1")).pack())) ), + new CreateRecords(List.of(new Record(Record.Type.DIRECT, RecordName.from("direct.example.com"), + new WeightedDirectTarget(RecordData.from("10.1.2.3"), + ZoneId.from("prod", "us-north-1"), + 100).pack()))), new RemoveRecords(record1.type(), record1.name()), new RemoveRecords(record2.type(), record2.data()) ); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java index 71e3607983c..6555277b06b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java @@ -39,6 +39,7 @@ import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.tes import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.applicationPackage; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed; +import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication; import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -87,7 +88,7 @@ public class JobControllerApiHandlerHelperTest { // us-east-3 eats the deployment failure and fails before deployment, while us-west-1 fails after. tester.configServer().throwOnNextPrepare(new ConfigServerException(INVALID_APPLICATION_PACKAGE, "ERROR!", "Failed to deploy application")); tester.runner().run(); - assertEquals(deploymentFailed, tester.jobs().last(app.instanceId(), productionUsEast3).get().status()); + assertEquals(invalidApplication, tester.jobs().last(app.instanceId(), productionUsEast3).get().status()); tester.runner().run(); tester.clock().advance(Duration.ofHours(4).plusSeconds(1)); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java index 3a539987443..2f4b4154c08 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.restapi.athenz; import com.yahoo.application.container.handler.Request; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Tags; import com.yahoo.vespa.athenz.api.AthenzDomain; import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; @@ -25,8 +26,8 @@ public class AthenzApiTest extends ControllerContainerTest { controllerTester.createTenant("sandbox", AthenzApiHandler.sandboxDomainIn(tester.controller().system()), 123L); controllerTester.createApplication("sandbox", "app", "default"); - tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", hostedOperator.getName())); - tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", defaultUser.getName())); + tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", hostedOperator.getName()), Tags.empty()); + tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", defaultUser.getName()), Tags.empty()); controllerTester.createApplication("sandbox", "opp", "default"); controllerTester.createTenant("tenant1", "domain1", 123L); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json index 7a8eef983f1..68ac684b0fb 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json @@ -163,6 +163,8 @@ "targetVersion": true, "upgradeBudget": "PT24H", "scheduledAt": 1234, + "nextVersion": "8.2.1.20211228", + "nextScheduledAt": 1640743200000, "cloud": "cloud2", "nodes": [ ] } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java index 2761c736e11..29834863976 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java @@ -370,11 +370,16 @@ public class RoutingPoliciesTest { List<String> expectedRecords = List.of("c0.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud", "c0.app1.tenant1.gcp-us-south1-b.z.vespa-app.cloud", "c0.app1.tenant1.aws-us-east-1.w.vespa-app.cloud", + "c0.app1.tenant1.gcp-us-south1.w.vespa-app.cloud", "r0.app1.tenant1.g.vespa-app.cloud"); assertEquals(Set.copyOf(expectedRecords), tester.recordNames()); assertEquals(List.of("lb-0--tenant1.app1.default--prod.aws-us-east-1c."), tester.recordDataOf(Record.Type.CNAME, expectedRecords.get(0))); assertEquals(List.of("10.0.0.0"), tester.recordDataOf(Record.Type.A, expectedRecords.get(1))); + assertEquals(List.of("weighted/10.0.0.0/prod.gcp-us-south1-b/1"), tester.recordDataOf(Record.Type.DIRECT, expectedRecords.get(3))); + assertEquals(List.of("latency/c0.app1.tenant1.aws-us-east-1.w.vespa-app.cloud/dns-zone-1/prod.aws-us-east-1c", + "latency/c0.app1.tenant1.gcp-us-south1.w.vespa-app.cloud/ignored/prod.gcp-us-south1-b"), + tester.recordDataOf(Record.Type.ALIAS, expectedRecords.get(4))); } @Test diff --git a/default_build_settings.cmake b/default_build_settings.cmake index 08f5e7d14da..5a21d12d0c6 100644 --- a/default_build_settings.cmake +++ b/default_build_settings.cmake @@ -2,102 +2,16 @@ include(VespaExtendedDefaultBuildSettings OPTIONAL) -function(setup_vespa_default_build_settings_rhel_8) - message("-- Setting up default build settings for rhel 8") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "12" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_centos_stream_8) - message("-- Setting up default build settings for centos stream 8") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "14" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_centos_stream_9) - message("-- Setting up default build settings for centos stream 9") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" "/usr/include/openblas" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "14" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_rocky_8_6) - message("-- Setting up default build settings for rocky 8.6") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "13" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_almalinux_8_6) - message("-- Setting up default build settings for almalinux 8.6") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "13" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_almalinux_9_0) - message("-- Setting up default build settings for almalinux 9.0") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" "/usr/include/openblas" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "13" PARENT_SCOPE) -endfunction() - function(setup_vespa_default_build_settings_darwin) message("-- Setting up default build settings for darwin") - set(DEFAULT_VESPA_LLVM_VERSION "15" PARENT_SCOPE) - set(DEFAULT_CMAKE_PREFIX_PATH "${VESPA_DEPS}" "/usr/local/opt/bison" "/usr/local/opt/flex" "/usr/local/opt/openssl@1.1" "/usr/local/opt/openblas" "/usr/local/opt/icu4c" PARENT_SCOPE) - set(DEFAULT_EXTRA_LINK_DIRECTORY "${VESPA_DEPS}/lib" "/usr/local/opt/bison/lib" "/usr/local/opt/flex/lib" "/usr/local/opt/icu4c/lib" "/usr/local/opt/openssl@1.1/lib" "/usr/local/opt/openblas/lib") + set(DEFAULT_EXTRA_LINK_DIRECTORY "${VESPA_DEPS_PREFIX}/lib" "/usr/local/opt/bison/lib" "/usr/local/opt/flex/lib" "/usr/local/opt/icu4c/lib" "/usr/local/opt/openssl@1.1/lib" "/usr/local/opt/openblas/lib") list(APPEND DEFAULT_EXTRA_LINK_DIRECTORY "/usr/local/lib") set(DEFAULT_EXTRA_LINK_DIRECTORY "${DEFAULT_EXTRA_LINK_DIRECTORY}" PARENT_SCOPE) - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" "/usr/local/opt/flex/include" "/usr/local/opt/icu4c/include" "/usr/local/opt/openssl@1.1/include" "/usr/local/opt/openblas/include") + set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS_PREFIX}/include" "/usr/local/opt/flex/include" "/usr/local/opt/icu4c/include" "/usr/local/opt/openssl@1.1/include" "/usr/local/opt/openblas/include") list(APPEND DEFAULT_EXTRA_INCLUDE_DIRECTORY "/usr/local/include") set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${DEFAULT_EXTRA_INCLUDE_DIRECTORY}" PARENT_SCOPE) endfunction() -function(setup_vespa_default_build_settings_fedora_36) - message("-- Setting up default build settings for fedora 36") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" "/usr/include/openblas" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "14" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_fedora_37) - message("-- Setting up default build settings for fedora 37") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" "/usr/include/openblas" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "15" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_fedora_38) - message("-- Setting up default build settings for fedora 38") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" "/usr/include/openblas" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "15" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_amzn_2022) - message("-- Setting up default build settings for amzn 2022") - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" "/usr/include/openblas" PARENT_SCOPE) - set(DEFAULT_VESPA_LLVM_VERSION "14" PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_ubuntu) - message("-- Setting up default build settings for ubuntu") - SET(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL) - SET(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC) - find_package(LLVM REQUIRED CONFIG) - message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") - message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") - set(DEFAULT_VESPA_LLVM_VERSION ${LLVM_VERSION_MAJOR} PARENT_SCOPE) - set(DEFAULT_EXTRA_LINK_DIRECTORY "${VESPA_DEPS}/lib" ${LLVM_LIBRARY_DIRS} PARENT_SCOPE) - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" ${LLVM_INCLUDE_DIRS} PARENT_SCOPE) -endfunction() - -function(setup_vespa_default_build_settings_debian) - message("-- Setting up default build settings for debian") - set(DEFAULT_EXTRA_LINK_DIRECTORY "${VESPA_DEPS}/lib" PARENT_SCOPE) - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include" PARENT_SCOPE) - SET(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL) - SET(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC) - find_package(LLVM REQUIRED CONFIG) - message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") - message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}") - set(DEFAULT_VESPA_LLVM_VERSION ${LLVM_VERSION_MAJOR} PARENT_SCOPE) -endfunction() - function(vespa_use_default_vespa_unprivileged) if(NOT DEFINED VESPA_UNPRIVILEGED) message("-- Setting VESPA_UNPRIVILEGED to yes") @@ -151,72 +65,62 @@ function(vespa_use_default_vespa_group) endif() endfunction() -function(vespa_use_default_build_settings) - set(VESPA_DEPS "/opt/vespa-deps") - unset(DEFAULT_VESPA_LLVM_VERSION) - unset(DEFAULT_CMAKE_PREFIX_PATH) - unset(DEFAULT_EXTRA_LINK_DIRECTORY) - unset(DEFAULT_EXTRA_INCLUDE_DIRECTORY) - unset(DEFAULT_VESPA_CPU_ARCH_FLAGS) - unset(DEFAULT_CMAKE_SHARED_LINKER_FLAGS) +function(vespa_use_default_vespa_deps_prefix) + set(VESPA_DEPS_PREFIX "/opt/vespa-deps") if(APPLE) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") - set(VESPA_DEPS "/opt/vespa-deps-clang") + set(VESPA_DEPS_PREFIX "/opt/vespa-deps-clang") elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") - set(VESPA_DEPS "/opt/vespa-deps-appleclang") + set(VESPA_DEPS_PREFIX "/opt/vespa-deps-appleclang") endif() endif() if(COMMAND vespa_use_specific_vespa_deps) vespa_use_specific_vespa_deps() endif() + message("-- Setting VESPA_DEPS_PREFIX to ${VESPA_DEPS_PREFIX}") + set(VESPA_DEPS_PREFIX ${VESPA_DEPS_PREFIX} PARENT_SCOPE) +endfunction() + +function(vespa_use_default_cmake_prefix_path) + set(DEFAULT_CMAKE_PREFIX_PATH ${VESPA_DEPS_PREFIX}) + if (APPLE) + list(APPEND DEFAULT_CMAKE_PREFIX_PATH "/usr/local/opt/bison" "/usr/local/opt/flex" "/usr/local/opt/openssl@1.1" "/usr/local/opt/openblas" "/usr/local/opt/icu4c") + endif() + message("-- DEFAULT_CMAKE_PREFIX_PATH is ${DEFAULT_CMAKE_PREFIX_PATH}") + if(NOT DEFINED CMAKE_PREFIX_PATH) + message("-- Setting CMAKE_PREFIX_PATH to ${DEFAULT_CMAKE_PREFIX_PATH}") + set(CMAKE_PREFIX_PATH ${DEFAULT_CMAKE_PREFIX_PATH} PARENT_SCOPE) + endif() +endfunction() + +function(vespa_use_default_build_settings) + unset(DEFAULT_EXTRA_LINK_DIRECTORY) + unset(DEFAULT_EXTRA_INCLUDE_DIRECTORY) + unset(DEFAULT_VESPA_CPU_ARCH_FLAGS) + unset(DEFAULT_CMAKE_SHARED_LINKER_FLAGS) if(COMMAND vespa_use_specific_compiler_rpath) vespa_use_specific_compiler_rpath() endif() - if(COMMAND vespa_use_specific_llvm_version) - vespa_use_specific_llvm_version() - endif() - if(VESPA_OS_DISTRO STREQUAL "rhel" AND - VESPA_OS_DISTRO_VERSION VERSION_GREATER_EQUAL "8" AND - VESPA_OS_DISTRO_VERSION VERSION_LESS "9") - setup_vespa_default_build_settings_rhel_8() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "centos 8") - setup_vespa_default_build_settings_centos_stream_8() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "centos 9") - setup_vespa_default_build_settings_centos_stream_9() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "rocky 8.6") - setup_vespa_default_build_settings_rocky_8_6() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "almalinux 8.6") - setup_vespa_default_build_settings_almalinux_8_6() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "almalinux 9.0") - setup_vespa_default_build_settings_almalinux_9_0() - elseif(VESPA_OS_DISTRO STREQUAL "darwin") + if(APPLE) setup_vespa_default_build_settings_darwin() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "fedora 36") - setup_vespa_default_build_settings_fedora_36() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "fedora 37") - setup_vespa_default_build_settings_fedora_37() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "fedora 38") - setup_vespa_default_build_settings_fedora_38() - elseif(VESPA_OS_DISTRO_COMBINED STREQUAL "amzn 2022") - setup_vespa_default_build_settings_amzn_2022() - elseif(VESPA_OS_DISTRO STREQUAL "ubuntu") - setup_vespa_default_build_settings_ubuntu() - elseif(VESPA_OS_DISTRO STREQUAL "debian") - setup_vespa_default_build_settings_debian() else() - message(FATAL_ERROR "-- Unknown vespa build platform ${VESPA_OS_DISTRO_COMBINED}") - endif() - if(NOT DEFINED VESPA_LLVM_VERSION AND NOT DEFINED DEFAULT_VESPA_LLVM_VERSION) - message(FATAL_ERROR "-- Unknown default llvm version") - endif() - if(NOT DEFINED DEFAULT_CMAKE_PREFIX_PATH) - set(DEFAULT_CMAKE_PREFIX_PATH "${VESPA_DEPS}") + message("-- Setting up default build settings for for ${VESPA_OS_DISTRO_COMBINED}") endif() + set(DEFAULT_VESPA_LLVM_VERSION ${LLVM_VERSION_MAJOR}) if(NOT DEFINED DEFAULT_EXTRA_LINK_DIRECTORY) - set(DEFAULT_EXTRA_LINK_DIRECTORY "${VESPA_DEPS}/lib64") + set(DEFAULT_EXTRA_LINK_DIRECTORY "${VESPA_DEPS_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + list(APPEND DEFAULT_EXTRA_LINK_DIRECTORY ${LLVM_LIBRARY_DIRS}) + list(REMOVE_ITEM DEFAULT_EXTRA_LINK_DIRECTORY "/usr/${CMAKE_INSTALL_LIBDIR}") + list(REMOVE_DUPLICATES DEFAULT_EXTRA_LINK_DIRECTORY) endif() if(NOT DEFINED DEFAULT_EXTRA_INCLUDE_DIRECTORY) - set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS}/include") + set(DEFAULT_EXTRA_INCLUDE_DIRECTORY "${VESPA_DEPS_PREFIX}/include") + list(APPEND DEFAULT_EXTRA_INCLUDE_DIRECTORY ${LLVM_INCLUDE_DIRS}) + if(EXISTS "/usr/include/openblas") + list(APPEND DEFAULT_EXTRA_INCLUDE_DIRECTORY "/usr/include/openblas") + endif() + list(REMOVE_ITEM DEFAULT_EXTRA_INCLUDE_DIRECTORY "/usr/include") + list(REMOVE_DUPLICATES DEFAULT_EXTRA_INCLUDE_DIRECTORY) endif() if(DEFINED DEFAULT_CMAKE_SHARED_LINKER_FLAGS) message("-- DEFAULT_CMAKE_SHARED_LINKER_FLAGS is ${DEFAULT_CMAKE_SHARED_LINKER_FLAGS}") @@ -235,23 +139,14 @@ function(vespa_use_default_build_settings) set(DEFAULT_VESPA_CPU_ARCH_FLAGS "-march=armv8.2-a+fp16+rcpc+dotprod+crypto -mtune=neoverse-n1") endif() endif() - if(DEFINED DEFAULT_CMAKE_PREFIX_PATH) - message("-- DEFAULT_CMAKE_PREFIX_PATH is ${DEFAULT_CMAKE_PREFIX_PATH}") - endif() if(DEFINED DEFAULT_EXTRA_LINK_DIRECTORY) message("-- DEFAULT_EXTRA_LINK_DIRECTORY is ${DEFAULT_EXTRA_LINK_DIRECTORY}") endif() if(DEFINED DEFAULT_EXTRA_INCLUDE_DIRECTORY) message("-- DEFAULT_EXTRA_INCLUDE_DIRECTORY is ${DEFAULT_EXTRA_INCLUDE_DIRECTORY}") endif() - if(DEFINED DEFAULT_VESPA_LLVM_VERSION) - message("-- DEFAULT_VESPA_LLVM_VERSION is ${DEFAULT_VESPA_LLVM_VERSION}") - endif() + message("-- DEFAULT_VESPA_LLVM_VERSION is ${DEFAULT_VESPA_LLVM_VERSION}") message("-- DEFAULT_VESPA_CPU_ARCH_FLAGS is ${DEFAULT_VESPA_CPU_ARCH_FLAGS}") - if(NOT DEFINED CMAKE_PREFIX_PATH AND DEFINED DEFAULT_CMAKE_PREFIX_PATH) - message("-- Setting CMAKE_PREFIX_PATH to ${DEFAULT_CMAKE_PREFIX_PATH}") - set(CMAKE_PREFIX_PATH "${DEFAULT_CMAKE_PREFIX_PATH}" PARENT_SCOPE) - endif() if(NOT DEFINED EXTRA_INCLUDE_DIRECTORY AND DEFINED DEFAULT_EXTRA_INCLUDE_DIRECTORY) message("-- Setting EXTRA_INCLUDE_DIRECTORY to ${DEFAULT_EXTRA_INCLUDE_DIRECTORY}") set(EXTRA_INCLUDE_DIRECTORY "${DEFAULT_EXTRA_INCLUDE_DIRECTORY}" PARENT_SCOPE) @@ -284,7 +179,7 @@ function(vespa_use_default_build_settings) set(CMAKE_BUILD_RPATH "${CMAKE_BUILD_RPATH}" PARENT_SCOPE) endif() endif() - if(NOT DEFINED VESPA_LLVM_VERSION AND DEFINED DEFAULT_VESPA_LLVM_VERSION) + if(NOT DEFINED VESPA_LLVM_VERSION) message("-- Setting VESPA_LLVM_VERSION to ${DEFAULT_VESPA_LLVM_VERSION}") set(VESPA_LLVM_VERSION "${DEFAULT_VESPA_LLVM_VERSION}" PARENT_SCOPE) endif() diff --git a/dist/STLExtras.h.diff b/dist/STLExtras.h.diff deleted file mode 100644 index 40f6a2a12ba..00000000000 --- a/dist/STLExtras.h.diff +++ /dev/null @@ -1,20 +0,0 @@ ---- STLExtras.h.orig 2021-01-28 01:34:01.000000000 +0100 -+++ STLExtras.h 2021-03-03 22:18:46.028992086 +0100 -@@ -1820,7 +1820,7 @@ - result_pair(std::size_t Index, IterOfRange<R> Iter) - : Index(Index), Iter(Iter) {} - -- result_pair<R>(const result_pair<R> &Other) -+ result_pair(const result_pair<R> &Other) - : Index(Other.Index), Iter(Other.Iter) {} - result_pair<R> &operator=(const result_pair<R> &Other) { - Index = Other.Index; -@@ -1870,7 +1870,7 @@ - return Result.Iter == RHS.Result.Iter; - } - -- enumerator_iter<R>(const enumerator_iter<R> &Other) : Result(Other.Result) {} -+ enumerator_iter(const enumerator_iter<R> &Other) : Result(Other.Result) {} - enumerator_iter<R> &operator=(const enumerator_iter<R> &Other) { - Result = Other.Result; - return *this; diff --git a/dist/vespa.spec b/dist/vespa.spec index 8953efb82f8..b8498bed210 100644 --- a/dist/vespa.spec +++ b/dist/vespa.spec @@ -6,6 +6,17 @@ # Only strip debug info %global _find_debuginfo_opts -g +# Don't enable LTO +%global _lto_cflags %{nil} + +# Disable hardened package build. +%global _preprocessor_defines %{nil} +%undefine _hardened_build + +# Libraries and binaries use shared libraries in /opt/vespa/lib64 and +# /opt/vespa-deps/lib64 +%global __brp_check_rpaths %{nil} + # Go binaries' build-ids are not recognized by RPMs yet, see # https://github.com/rpm-software-management/rpm/issues/367 and # https://github.com/tpokorra/lbs-mono-fedora/issues/3#issuecomment-219857688. @@ -13,7 +24,6 @@ # Force special prefix for Vespa %define _prefix /opt/vespa -%define _vespa_deps_prefix /opt/vespa-deps %define _vespa_user vespa %define _vespa_group vespa %undefine _vespa_user_uid @@ -32,7 +42,7 @@ License: Commercial URL: http://vespa.ai Source0: vespa-%{version}.tar.gz -%if 0%{?centos} || 0%{?rocky} +%if 0%{?centos} || 0%{?rocky} || 0%{?oraclelinux} BuildRequires: epel-release %endif %if 0%{?el8} @@ -78,7 +88,7 @@ BuildRequires: glibc-langpack-en %endif %if 0%{?el8} BuildRequires: cmake >= 3.11.4-3 -%if 0%{?centos} || 0%{?rocky} +%if 0%{?centos} || 0%{?rocky} || 0%{?oraclelinux} %if 0%{?centos} # Current cmake on CentOS 8 is broken and manually requires libarchive install BuildRequires: libarchive @@ -90,26 +100,26 @@ BuildRequires: (llvm-devel >= 14.0.0 and llvm-devel < 15) BuildRequires: (llvm-devel >= 13.0.1 and llvm-devel < 14) %endif %else -BuildRequires: (llvm-devel >= 12.0.1 and llvm-devel < 13) +BuildRequires: (llvm-devel >= 13.0.1 and llvm-devel < 14) %endif BuildRequires: vespa-boost-devel >= 1.76.0-1 BuildRequires: vespa-openssl-devel >= 1.1.1o-1 %define _use_vespa_openssl 1 BuildRequires: vespa-gtest = 1.11.0 %define _use_vespa_gtest 1 -BuildRequires: vespa-lz4-devel >= 1.9.2-2 +BuildRequires: vespa-lz4-devel >= 1.9.4-1 BuildRequires: vespa-onnxruntime-devel = 1.12.1 -BuildRequires: vespa-protobuf-devel = 3.19.1 -BuildRequires: vespa-libzstd-devel >= 1.4.5-2 +BuildRequires: vespa-protobuf-devel = 3.21.7 +BuildRequires: vespa-libzstd-devel >= 1.5.2-1 %endif %if 0%{?el9} BuildRequires: cmake >= 3.20.2 BuildRequires: maven BuildRequires: maven-openjdk17 BuildRequires: openssl-devel -BuildRequires: vespa-lz4-devel >= 1.9.2-2 +BuildRequires: vespa-lz4-devel >= 1.9.4-1 BuildRequires: vespa-onnxruntime-devel = 1.12.1 -BuildRequires: vespa-libzstd-devel >= 1.4.5-2 +BuildRequires: vespa-libzstd-devel >= 1.5.2-1 BuildRequires: protobuf-devel %if 0%{?_centos_stream} BuildRequires: (llvm-devel >= 14.0.0 and llvm-devel < 15) @@ -132,9 +142,9 @@ BuildRequires: maven-openjdk17 %endif %endif BuildRequires: openssl-devel -BuildRequires: vespa-lz4-devel >= 1.9.2-2 +BuildRequires: vespa-lz4-devel >= 1.9.4-1 BuildRequires: vespa-onnxruntime-devel = 1.12.1 -BuildRequires: vespa-libzstd-devel >= 1.4.5-2 +BuildRequires: vespa-libzstd-devel >= 1.5.2-1 %if 0%{?amzn2022} BuildRequires: protobuf-devel BuildRequires: llvm-devel >= 14.0.5 @@ -164,9 +174,14 @@ BuildRequires: gtest-devel BuildRequires: gmock-devel %endif %endif -BuildRequires: xxhash-devel >= 0.8.0 +%if 0%{?amzn2022} +BuildRequires: vespa-xxhash-devel >= 0.8.1 +%define _use_vespa_xxhash 1 +%else +BuildRequires: xxhash-devel >= 0.8.1 +%endif %if 0%{?el8} -BuildRequires: vespa-openblas-devel = 0.3.18 +BuildRequires: vespa-openblas-devel = 0.3.21 %define _use_vespa_openblas 1 %else BuildRequires: openblas-devel @@ -220,8 +235,12 @@ BuildRequires: perl-Pod-Usage BuildRequires: perl-URI BuildRequires: valgrind BuildRequires: perf +%if 0%{?amzn2022} +Requires: vespa-xxhash >= 0.8.1 +%else Requires: xxhash -Requires: xxhash-libs >= 0.8.0 +Requires: xxhash-libs >= 0.8.1 +%endif Requires: gdb Requires: hostname Requires: nc @@ -231,45 +250,13 @@ Requires: unzip Requires: zlib Requires: zstd %if 0%{?el8} -%if 0%{?centos} || 0%{?rocky} -%if 0%{?_centos_stream} -%define _vespa_llvm_version 14 -%else -%define _vespa_llvm_version 13 -%endif -%else -%define _vespa_llvm_version 12 -%endif Requires: vespa-gtest = 1.11.0 -%define _extra_link_directory %{_vespa_deps_prefix}/lib64 -%define _extra_include_directory %{_vespa_deps_prefix}/include %endif %if 0%{?el9} -%if 0%{?_centos_stream} -%define _vespa_llvm_version 14 -%else -%define _vespa_llvm_version 13 -%endif Requires: gtest -%define _extra_link_directory %{_vespa_deps_prefix}/lib64 -%define _extra_include_directory %{_vespa_deps_prefix}/include;/usr/include/openblas %endif %if 0%{?fedora} Requires: gtest -%if 0%{?amzn2022} -%define _vespa_llvm_version 14 -%endif -%if 0%{?fc36} -%define _vespa_llvm_version 14 -%endif -%if 0%{?fc37} -%define _vespa_llvm_version 15 -%endif -%if 0%{?fc38} -%define _vespa_llvm_version 15 -%endif -%define _extra_link_directory %{_vespa_deps_prefix}/lib64 -%define _extra_include_directory %{_vespa_deps_prefix}/include;/usr/include/openblas %endif Requires: %{name}-base = %{version}-%{release} Requires: %{name}-base-libs = %{version}-%{release} @@ -312,19 +299,23 @@ Vespa - The open big data serving engine - base Summary: Vespa - The open big data serving engine - base C++ libraries -%if 0%{?centos} || 0%{?rocky} +%if 0%{?centos} || 0%{?rocky} || 0%{?oraclelinux} Requires: epel-release %endif -Requires: xxhash-libs >= 0.8.0 +%if 0%{?amzn2022} +Requires: vespa-xxhash >= 0.8.1 +%else +Requires: xxhash-libs >= 0.8.1 +%endif %if 0%{?el8} Requires: vespa-openssl >= 1.1.1o-1 %else Requires: openssl-libs %endif -Requires: vespa-lz4 >= 1.9.2-2 -Requires: vespa-libzstd >= 1.4.5-2 +Requires: vespa-lz4 >= 1.9.4-1 +Requires: vespa-libzstd >= 1.5.2-1 %if 0%{?el8} -Requires: vespa-openblas = 0.3.18 +Requires: vespa-openblas = 0.3.21 %else Requires: openblas-serial %endif @@ -353,16 +344,16 @@ Requires: vespa-openssl >= 1.1.1o-1 Requires: openssl-libs %endif %if 0%{?el8} -%if 0%{?centos} || 0%{?rocky} +%if 0%{?centos} || 0%{?rocky} || 0%{?oraclelinux} %if 0%{?_centos_stream} Requires: (llvm-libs >= 14.0.0 and llvm-libs < 15) %else Requires: (llvm-libs >= 13.0.1 and llvm-libs < 14) %endif %else -Requires: (llvm-libs >= 12.0.1 and llvm-libs < 13) +Requires: (llvm-libs >= 13.0.1 and llvm-libs < 14) %endif -Requires: vespa-protobuf = 3.19.1 +Requires: vespa-protobuf = 3.21.7 %endif %if 0%{?el9} %if 0%{?_centos_stream} @@ -492,12 +483,6 @@ nearest neighbor search used for low-level benchmarking. %endif %else %setup -q -%if 0%{?el8} && %{?_vespa_llvm_version}%{!?_vespa_llvm_version:13} < 13 -if grep -qs 'result_pair<R>(' /usr/include/llvm/ADT/STLExtras.h -then - patch /usr/include/llvm/ADT/STLExtras.h < dist/STLExtras.h.diff -fi -%endif echo '%{version}' > VERSION case '%{version}' in *.0) @@ -536,11 +521,6 @@ mvn --batch-mode -e -N io.takari:maven:wrapper -Dmaven=3.6.3 %{?_use_mvn_wrapper:./mvnw}%{!?_use_mvn_wrapper:mvn} --batch-mode -nsu -T 1C install -Dmaven.test.skip=true -Dmaven.javadoc.skip=true %{_command_cmake} -DCMAKE_INSTALL_PREFIX=%{_prefix} \ -DJAVA_HOME=$JAVA_HOME \ - -DCMAKE_PREFIX_PATH=%{_vespa_deps_prefix} \ - -DEXTRA_LINK_DIRECTORY="%{_extra_link_directory}" \ - -DEXTRA_INCLUDE_DIRECTORY="%{_extra_include_directory}" \ - -DCMAKE_INSTALL_RPATH="%{_prefix}/lib64%{?_extra_link_directory:;%{_extra_link_directory}}" \ - %{?_vespa_llvm_version:-DVESPA_LLVM_VERSION="%{_vespa_llvm_version}"} \ -DVESPA_USER=%{_vespa_user} \ -DVESPA_UNPRIVILEGED=no \ . @@ -719,6 +699,7 @@ fi %dir %attr(-,%{_vespa_user},%{_vespa_group}) %{_prefix}/var/db/vespa/config_server %dir %attr(-,%{_vespa_user},%{_vespa_group}) %{_prefix}/var/db/vespa/config_server/serverdb %dir %attr(-,%{_vespa_user},%{_vespa_group}) %{_prefix}/var/db/vespa/config_server/serverdb/tenants +%dir %attr(-,%{_vespa_user},%{_vespa_group}) %{_prefix}/var/db/vespa/download %dir %attr(-,%{_vespa_user},%{_vespa_group}) %{_prefix}/var/db/vespa/filedistribution %dir %attr(-,%{_vespa_user},%{_vespa_group}) %{_prefix}/var/db/vespa/index %dir %attr(-,%{_vespa_user},%{_vespa_group}) %{_prefix}/var/db/vespa/logcontrol @@ -839,8 +820,7 @@ fi %dir %{_prefix}/lib %dir %{_prefix}/lib/jars %{_prefix}/lib/jars/application-model-jar-with-dependencies.jar -%{_prefix}/lib/jars/bcpkix-jdk15on-*.jar -%{_prefix}/lib/jars/bcprov-jdk15on-*.jar +%{_prefix}/lib/jars/bc*-jdk18on-*.jar %{_prefix}/lib/jars/config-bundle-jar-with-dependencies.jar %{_prefix}/lib/jars/configdefinitions-jar-with-dependencies.jar %{_prefix}/lib/jars/config-model-api-jar-with-dependencies.jar diff --git a/docproc/src/main/java/com/yahoo/docproc/DocumentProcessor.java b/docproc/src/main/java/com/yahoo/docproc/DocumentProcessor.java index 7b8f07373a5..1f4ab3c8f00 100644 --- a/docproc/src/main/java/com/yahoo/docproc/DocumentProcessor.java +++ b/docproc/src/main/java/com/yahoo/docproc/DocumentProcessor.java @@ -90,7 +90,7 @@ public abstract class DocumentProcessor extends ChainedComponent { Map<String, String> ret = new HashMap<>(); for (Entry<Pair<String, String>, String> e : fieldMap.entrySet()) { // Remember to include tuple if doctype is unset in mapping - if (docType.equals(e.getKey().getFirst()) || e.getKey().getFirst()==null || "".equals(e.getKey().getFirst())) { + if (docType.equals(e.getKey().getFirst()) || e.getKey().getFirst() == null || "".equals(e.getKey().getFirst())) { ret.put(e.getKey().getSecond(), e.getValue()); } } diff --git a/document/src/main/java/com/yahoo/document/Document.java b/document/src/main/java/com/yahoo/document/Document.java index 5937ba00292..760b9de0199 100644 --- a/document/src/main/java/com/yahoo/document/Document.java +++ b/document/src/main/java/com/yahoo/document/Document.java @@ -242,8 +242,7 @@ public class Document extends StructuredFieldValue { @Override public boolean equals(Object o) { if (o == this) return true; - if (!(o instanceof Document)) return false; - Document other = (Document) o; + if (!(o instanceof Document other)) return false; return (super.equals(o) && docId.equals(other.docId) && header.equals(other.header)); } diff --git a/document/src/main/java/com/yahoo/document/DocumentUpdate.java b/document/src/main/java/com/yahoo/document/DocumentUpdate.java index befabfb6c07..0731344cea9 100644 --- a/document/src/main/java/com/yahoo/document/DocumentUpdate.java +++ b/document/src/main/java/com/yahoo/document/DocumentUpdate.java @@ -443,4 +443,5 @@ public class DocumentUpdate extends DocumentOperation implements Iterable<FieldP public Optional<Boolean> getOptionalCreateIfNonExistent() { return Optional.ofNullable(createIfNonExistent); } + } diff --git a/document/src/main/java/com/yahoo/document/datatypes/StructuredFieldValue.java b/document/src/main/java/com/yahoo/document/datatypes/StructuredFieldValue.java index 396a42b1237..6adbcda4772 100644 --- a/document/src/main/java/com/yahoo/document/datatypes/StructuredFieldValue.java +++ b/document/src/main/java/com/yahoo/document/datatypes/StructuredFieldValue.java @@ -46,14 +46,12 @@ public abstract class StructuredFieldValue extends CompositeFieldValue { * and using the returned value to call {@link #getFieldValue(Field)}. If the named field does not exist, this * method returns null. * - * @param fieldName The name of the field whose value to return. - * @return The value of the field, or null. + * @param fieldName the name of the field whose value to return. + * @return the value of the field, or null if it is not declared in this, or has no value set */ public FieldValue getFieldValue(String fieldName) { Field field = getField(fieldName); - if (field == null) { - return null; - } + if (field == null) return null; return getFieldValue(field); } @@ -61,10 +59,10 @@ public abstract class StructuredFieldValue extends CompositeFieldValue { * Sets the value of the given field. The type of the value must match the type of this field, i.e. * <pre>field.getDataType().getValueClass().isAssignableFrom(value.getClass())</pre> must be true. * - * @param field The field whose value to set. - * @param value The value to set. - * @return The previous value of the field, or null. - * @throws IllegalArgumentException If the value is not compatible with the field. + * @param field the field whose value to set + * @param value the value to set + * @return the previous value of the field, or null + * @throws IllegalArgumentException if the value is not compatible with the field */ public FieldValue setFieldValue(Field field, FieldValue value) { if (value == null) { diff --git a/document/src/main/java/com/yahoo/document/json/JsonFeedReader.java b/document/src/main/java/com/yahoo/document/json/JsonFeedReader.java index d0bd41c692c..4618a516f66 100644 --- a/document/src/main/java/com/yahoo/document/json/JsonFeedReader.java +++ b/document/src/main/java/com/yahoo/document/json/JsonFeedReader.java @@ -16,11 +16,9 @@ import com.yahoo.vespaxmlparser.RemoveFeedOperation; import java.io.InputStream; - /** * Facade between JsonReader and the FeedReader API. * - * <p> * The feed reader will take ownership of the input stream and close it when the * last parseable document has been read. * @@ -29,7 +27,7 @@ import java.io.InputStream; public class JsonFeedReader implements FeedReader { private final JsonReader reader; - private InputStream stream; + private final InputStream stream; private static final JsonFactory jsonFactory = new JsonFactoryBuilder().disable(JsonFactory.Feature.CANONICALIZE_FIELD_NAMES).build(); public JsonFeedReader(InputStream stream, DocumentTypeManager docMan) { diff --git a/document/src/main/java/com/yahoo/document/json/JsonReader.java b/document/src/main/java/com/yahoo/document/json/JsonReader.java index 94ce986fc81..86023d52b63 100644 --- a/document/src/main/java/com/yahoo/document/json/JsonReader.java +++ b/document/src/main/java/com/yahoo/document/json/JsonReader.java @@ -29,11 +29,6 @@ import static com.yahoo.document.json.readers.JsonParserHelpers.expectArrayStart */ public class JsonReader { - public Optional<DocumentParseInfo> parseDocument() throws IOException { - DocumentParser documentParser = new DocumentParser(parser); - return documentParser.parse(Optional.empty()); - } - private final JsonParser parser; private final DocumentTypeManager typeManager; private ReaderState state = ReaderState.AT_START; @@ -53,14 +48,19 @@ public class JsonReader { } } + public Optional<DocumentParseInfo> parseDocument() throws IOException { + DocumentParser documentParser = new DocumentParser(parser); + return documentParser.parse(Optional.empty()); + } + /** * Reads a single operation. The operation is not expected to be part of an array. * * @param operationType the type of operation (update or put) - * @param docIdString document ID. - * @return the document + * @param docIdString document ID + * @return the parsed document operation */ - public DocumentOperation readSingleDocument(DocumentOperationType operationType, String docIdString) { + public ParsedDocumentOperation readOperation(DocumentOperationType operationType, String docIdString) { DocumentId docId = new DocumentId(docIdString); DocumentParseInfo documentParseInfo; try { @@ -72,12 +72,17 @@ public class JsonReader { } documentParseInfo.operationType = operationType; VespaJsonDocumentReader vespaJsonDocumentReader = new VespaJsonDocumentReader(typeManager.getIgnoreUndefinedFields()); - DocumentOperation operation = vespaJsonDocumentReader.createDocumentOperation( + ParsedDocumentOperation operation = vespaJsonDocumentReader.createDocumentOperation( getDocumentTypeFromString(documentParseInfo.documentId.getDocType(), typeManager), documentParseInfo); - operation.setCondition(TestAndSetCondition.fromConditionString(documentParseInfo.condition)); + operation.operation().setCondition(TestAndSetCondition.fromConditionString(documentParseInfo.condition)); return operation; } + @Deprecated // Use readOperation instead + public DocumentOperation readSingleDocument(DocumentOperationType operationType, String docIdString) { + return readOperation(operationType, docIdString).operation(); + } + /** Returns the next document operation, or null if we have reached the end */ public DocumentOperation next() { switch (state) { @@ -106,7 +111,7 @@ public class JsonReader { VespaJsonDocumentReader vespaJsonDocumentReader = new VespaJsonDocumentReader(typeManager.getIgnoreUndefinedFields()); DocumentOperation operation = vespaJsonDocumentReader.createDocumentOperation( getDocumentTypeFromString(documentParseInfo.get().documentId.getDocType(), typeManager), - documentParseInfo.get()); + documentParseInfo.get()).operation(); operation.setCondition(TestAndSetCondition.fromConditionString(documentParseInfo.get().condition)); return operation; } @@ -132,4 +137,5 @@ public class JsonReader { throw new IllegalArgumentException(e); } } + } diff --git a/document/src/main/java/com/yahoo/document/json/ParsedDocumentOperation.java b/document/src/main/java/com/yahoo/document/json/ParsedDocumentOperation.java new file mode 100644 index 00000000000..a395973a55a --- /dev/null +++ b/document/src/main/java/com/yahoo/document/json/ParsedDocumentOperation.java @@ -0,0 +1,51 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.document.json; + +import com.yahoo.document.DocumentOperation; + +import java.util.Objects; + +/** + * The result of JSON parsing a single document operation + */ +public final class ParsedDocumentOperation { + + private final DocumentOperation operation; + private final boolean fullyApplied; + + /** + * @param operation the parsed operation + * @param fullyApplied true if all the JSON content could be applied, + * false if some (or all) of the fields were not poresent in this document and was ignored + */ + public ParsedDocumentOperation(DocumentOperation operation, boolean fullyApplied) { + this.operation = operation; + this.fullyApplied = fullyApplied; + } + + public DocumentOperation operation() { return operation; } + + public boolean fullyApplied() { return fullyApplied; } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (ParsedDocumentOperation) obj; + return Objects.equals(this.operation, that.operation) && + this.fullyApplied == that.fullyApplied; + } + + @Override + public int hashCode() { + return Objects.hash(operation, fullyApplied); + } + + @Override + public String toString() { + return "ParsedDocumentOperation[" + + "operation=" + operation + ", " + + "fullyApplied=" + fullyApplied + ']'; + } + +} diff --git a/document/src/main/java/com/yahoo/document/json/TokenBuffer.java b/document/src/main/java/com/yahoo/document/json/TokenBuffer.java index 4fff9c45ea5..e6d2021f90b 100644 --- a/document/src/main/java/com/yahoo/document/json/TokenBuffer.java +++ b/document/src/main/java/com/yahoo/document/json/TokenBuffer.java @@ -213,4 +213,12 @@ public class TokenBuffer { } return toReturn; } + + public void skipToRelativeNesting(int relativeNesting) { + int initialNesting = nesting(); + do { + next(); + } while ( nesting() > initialNesting + relativeNesting); + } + } diff --git a/document/src/main/java/com/yahoo/document/json/readers/CompositeReader.java b/document/src/main/java/com/yahoo/document/json/readers/CompositeReader.java index a2dd91b90a0..6cbc1c1e0b1 100644 --- a/document/src/main/java/com/yahoo/document/json/readers/CompositeReader.java +++ b/document/src/main/java/com/yahoo/document/json/readers/CompositeReader.java @@ -19,14 +19,22 @@ import static com.yahoo.document.json.readers.WeightedSetReader.fillWeightedSet; public class CompositeReader { - // TODO: reateComposite is extremely similar to add/remove, refactor + public static boolean populateComposite(TokenBuffer buffer, FieldValue fieldValue, boolean ignoreUndefinedFields) { + boolean fullyApplied = populateComposite(buffer.currentToken(), buffer, fieldValue, ignoreUndefinedFields); + expectCompositeEnd(buffer.currentToken()); + return fullyApplied; + } + + // TODO: createComposite is extremely similar to add/remove, refactor // yes, this suppresswarnings ugliness is by intention, the code relies on the contracts in the builders @SuppressWarnings({ "cast", "rawtypes" }) - public static void populateComposite(TokenBuffer buffer, FieldValue fieldValue, boolean ignoreUndefinedFields) { - JsonToken token = buffer.currentToken(); + private static boolean populateComposite(JsonToken token, TokenBuffer buffer, FieldValue fieldValue, + boolean ignoreUndefinedFields) { if ((token != JsonToken.START_OBJECT) && (token != JsonToken.START_ARRAY)) { throw new IllegalArgumentException("Expected '[' or '{'. Got '" + token + "'."); } + + boolean fullyApplied = true; if (fieldValue instanceof CollectionFieldValue) { DataType valueType = ((CollectionFieldValue) fieldValue).getDataType().getNestedType(); if (fieldValue instanceof WeightedSet) { @@ -39,13 +47,14 @@ public class CompositeReader { } else if (PositionDataType.INSTANCE.equals(fieldValue.getDataType())) { GeoPositionReader.fillGeoPosition(buffer, fieldValue); } else if (fieldValue instanceof StructuredFieldValue) { - StructReader.fillStruct(buffer, (StructuredFieldValue) fieldValue, ignoreUndefinedFields); + fullyApplied = StructReader.fillStruct(buffer, (StructuredFieldValue) fieldValue, ignoreUndefinedFields); } else if (fieldValue instanceof TensorFieldValue) { TensorReader.fillTensor(buffer, (TensorFieldValue) fieldValue); } else { throw new IllegalArgumentException("Expected a " + fieldValue.getClass().getName() + " but got an " + (token == JsonToken.START_OBJECT ? "object" : "array" )); } - expectCompositeEnd(buffer.currentToken()); + return fullyApplied; } + } diff --git a/document/src/main/java/com/yahoo/document/json/readers/SingleValueReader.java b/document/src/main/java/com/yahoo/document/json/readers/SingleValueReader.java index 1747e739bbf..3ae82676fa8 100644 --- a/document/src/main/java/com/yahoo/document/json/readers/SingleValueReader.java +++ b/document/src/main/java/com/yahoo/document/json/readers/SingleValueReader.java @@ -53,31 +53,17 @@ public class SingleValueReader { @SuppressWarnings("rawtypes") public static ValueUpdate readSingleUpdate(TokenBuffer buffer, DataType expectedType, String action, boolean ignoreUndefinedFields) { - ValueUpdate update; - - switch (action) { - case UPDATE_ASSIGN: - update = (buffer.currentToken() == JsonToken.VALUE_NULL) - ? ValueUpdate.createClear() - : ValueUpdate.createAssign(readSingleValue(buffer, expectedType, ignoreUndefinedFields)); - break; + return switch (action) { + case UPDATE_ASSIGN -> (buffer.currentToken() == JsonToken.VALUE_NULL) + ? ValueUpdate.createClear() + : ValueUpdate.createAssign(readSingleValue(buffer, expectedType, ignoreUndefinedFields)); // double is silly, but it's what is used internally anyway - case UPDATE_INCREMENT: - update = ValueUpdate.createIncrement(Double.valueOf(buffer.currentText())); - break; - case UPDATE_DECREMENT: - update = ValueUpdate.createDecrement(Double.valueOf(buffer.currentText())); - break; - case UPDATE_MULTIPLY: - update = ValueUpdate.createMultiply(Double.valueOf(buffer.currentText())); - break; - case UPDATE_DIVIDE: - update = ValueUpdate.createDivide(Double.valueOf(buffer.currentText())); - break; - default: - throw new IllegalArgumentException("Operation '" + buffer.currentName() + "' not implemented."); - } - return update; + case UPDATE_INCREMENT -> ValueUpdate.createIncrement(Double.valueOf(buffer.currentText())); + case UPDATE_DECREMENT -> ValueUpdate.createDecrement(Double.valueOf(buffer.currentText())); + case UPDATE_MULTIPLY -> ValueUpdate.createMultiply(Double.valueOf(buffer.currentText())); + case UPDATE_DIVIDE -> ValueUpdate.createDivide(Double.valueOf(buffer.currentText())); + default -> throw new IllegalArgumentException("Operation '" + buffer.currentName() + "' not implemented."); + }; } public static Matcher matchArithmeticOperation(String expression) { @@ -94,7 +80,7 @@ public class SingleValueReader { } } - private static FieldValue readReferenceFieldValue(final String refText, DataType expectedType) { + private static FieldValue readReferenceFieldValue(String refText, DataType expectedType) { final FieldValue value = expectedType.createFieldValue(); if (!refText.isEmpty()) { value.assign(new DocumentId(refText)); diff --git a/document/src/main/java/com/yahoo/document/json/readers/StructReader.java b/document/src/main/java/com/yahoo/document/json/readers/StructReader.java index b9eaf0d8ec6..b944d273a72 100644 --- a/document/src/main/java/com/yahoo/document/json/readers/StructReader.java +++ b/document/src/main/java/com/yahoo/document/json/readers/StructReader.java @@ -12,15 +12,32 @@ import static com.yahoo.document.json.readers.SingleValueReader.readSingleValue; public class StructReader { - public static void fillStruct(TokenBuffer buffer, StructuredFieldValue parent, boolean ignoreUndefinedFields) { + /** + * Fills this struct. + * + * @return true if all this was applied and false if it was ignored because the field does not exist + */ + public static boolean fillStruct(TokenBuffer buffer, StructuredFieldValue parent, boolean ignoreUndefinedFields) { // do note the order of initializing initNesting and token is relevant for empty docs - int initNesting = buffer.nesting(); + int initialNesting = buffer.nesting(); buffer.next(); - while (buffer.nesting() >= initNesting) { - Field field = getField(buffer, parent, ignoreUndefinedFields); + boolean fullyApplied = true; + while (buffer.nesting() >= initialNesting) { + Field field = parent.getField(buffer.currentName()); + if (field == null) { + if (! ignoreUndefinedFields) + throw new IllegalArgumentException("No field '" + buffer.currentName() + "' in the structure of type '" + + parent.getDataType().getDataTypeName() + + "', which has the fields: " + parent.getDataType().getFields()); + + buffer.skipToRelativeNesting(0); + fullyApplied = false; + continue; + } + try { - if (field != null && buffer.currentToken() != JsonToken.VALUE_NULL) { + if (buffer.currentToken() != JsonToken.VALUE_NULL) { FieldValue v = readSingleValue(buffer, field.getDataType(), ignoreUndefinedFields); parent.setFieldValue(field, v); } @@ -29,16 +46,7 @@ public class StructReader { throw new JsonReaderException(field, e); } } - } - - private static Field getField(TokenBuffer buffer, StructuredFieldValue parent, boolean ignoreUndefinedFields) { - Field field = parent.getField(buffer.currentName()); - if (field == null && ! ignoreUndefinedFields) { - throw new IllegalArgumentException("No field '" + buffer.currentName() + "' in the structure of type '" + - parent.getDataType().getDataTypeName() + - "', which has the fields: " + parent.getDataType().getFields()); - } - return field; + return fullyApplied; } } diff --git a/document/src/main/java/com/yahoo/document/json/readers/VespaJsonDocumentReader.java b/document/src/main/java/com/yahoo/document/json/readers/VespaJsonDocumentReader.java index 7bc462ec73a..22a9e7a1119 100644 --- a/document/src/main/java/com/yahoo/document/json/readers/VespaJsonDocumentReader.java +++ b/document/src/main/java/com/yahoo/document/json/readers/VespaJsonDocumentReader.java @@ -17,6 +17,7 @@ import com.yahoo.document.fieldpathupdate.AssignFieldPathUpdate; import com.yahoo.document.fieldpathupdate.FieldPathUpdate; import com.yahoo.document.fieldpathupdate.RemoveFieldPathUpdate; import com.yahoo.document.json.JsonReaderException; +import com.yahoo.document.json.ParsedDocumentOperation; import com.yahoo.document.json.TokenBuffer; import com.yahoo.document.update.FieldUpdate; @@ -50,67 +51,65 @@ public class VespaJsonDocumentReader { this.ignoreUndefinedFields = ignoreUndefinedFields; } - public DocumentOperation createDocumentOperation(DocumentType documentType, DocumentParseInfo documentParseInfo) { + public ParsedDocumentOperation createDocumentOperation(DocumentType documentType, DocumentParseInfo documentParseInfo) { final DocumentOperation documentOperation; + boolean fullyApplied = true; try { switch (documentParseInfo.operationType) { - case PUT: + case PUT -> { documentOperation = new DocumentPut(new Document(documentType, documentParseInfo.documentId)); - readPut(documentParseInfo.fieldsBuffer, (DocumentPut) documentOperation); + fullyApplied = readPut(documentParseInfo.fieldsBuffer, (DocumentPut) documentOperation); verifyEndState(documentParseInfo.fieldsBuffer, JsonToken.END_OBJECT); - break; - case REMOVE: - documentOperation = new DocumentRemove(documentParseInfo.documentId); - break; - case UPDATE: + } + case REMOVE -> documentOperation = new DocumentRemove(documentParseInfo.documentId); + case UPDATE -> { documentOperation = new DocumentUpdate(documentType, documentParseInfo.documentId); - readUpdate(documentParseInfo.fieldsBuffer, (DocumentUpdate) documentOperation); + fullyApplied = readUpdate(documentParseInfo.fieldsBuffer, (DocumentUpdate) documentOperation); verifyEndState(documentParseInfo.fieldsBuffer, JsonToken.END_OBJECT); - break; - default: - throw new IllegalStateException("Implementation out of sync with itself. This is a bug."); + } + default -> throw new IllegalStateException("Implementation out of sync with itself. This is a bug."); } } catch (JsonReaderException e) { throw JsonReaderException.addDocId(e, documentParseInfo.documentId); } if (documentParseInfo.create.isPresent()) { - if (! ( documentOperation instanceof DocumentUpdate)) { + if (! (documentOperation instanceof DocumentUpdate update)) { throw new IllegalArgumentException("Could not set create flag on non update operation."); } - DocumentUpdate update = (DocumentUpdate) documentOperation; update.setCreateIfNonExistent(documentParseInfo.create.get()); } - return documentOperation; + return new ParsedDocumentOperation(documentOperation, fullyApplied); } // Exposed for unit testing... - public void readPut(TokenBuffer buffer, DocumentPut put) { + public boolean readPut(TokenBuffer buffer, DocumentPut put) { try { if (buffer.isEmpty()) // no "fields" map throw new IllegalArgumentException(put + " is missing a 'fields' map"); - populateComposite(buffer, put.getDocument(), ignoreUndefinedFields); + return populateComposite(buffer, put.getDocument(), ignoreUndefinedFields); } catch (JsonReaderException e) { throw JsonReaderException.addDocId(e, put.getId()); } } // Exposed for unit testing... - public void readUpdate(TokenBuffer buffer, DocumentUpdate update) { + public boolean readUpdate(TokenBuffer buffer, DocumentUpdate update) { if (buffer.isEmpty()) - throw new IllegalArgumentException("update of document " + update.getId() + " is missing a 'fields' map"); + throw new IllegalArgumentException("Update of document " + update.getId() + " is missing a 'fields' map"); expectObjectStart(buffer.currentToken()); int localNesting = buffer.nesting(); buffer.next(); + boolean fullyApplied = true; while (localNesting <= buffer.nesting()) { expectObjectStart(buffer.currentToken()); String fieldName = buffer.currentName(); try { if (isFieldPath(fieldName)) { - addFieldPathUpdates(update, buffer, fieldName); + fullyApplied &= addFieldPathUpdates(update, buffer, fieldName); } else { - addFieldUpdates(update, buffer, fieldName); + fullyApplied &= addFieldUpdates(update, buffer, fieldName); } expectObjectEnd(buffer.currentToken()); } @@ -119,12 +118,18 @@ public class VespaJsonDocumentReader { } buffer.next(); } + return fullyApplied; } - private void addFieldUpdates(DocumentUpdate update, TokenBuffer buffer, String fieldName) { + private boolean addFieldUpdates(DocumentUpdate update, TokenBuffer buffer, String fieldName) { Field field = update.getType().getField(fieldName); - if (field == null) - throw new IllegalArgumentException("No field named '" + fieldName + "' in " + update.getType()); + if (field == null) { + if (! ignoreUndefinedFields) + throw new IllegalArgumentException("No field named '" + fieldName + "' in " + update.getType()); + buffer.skipToRelativeNesting(-1); + return false; + } + int localNesting = buffer.nesting(); FieldUpdate fieldUpdate = FieldUpdate.create(field); @@ -158,9 +163,10 @@ public class VespaJsonDocumentReader { buffer.next(); } update.addFieldUpdate(fieldUpdate); + return true; } - private void addFieldPathUpdates(DocumentUpdate update, TokenBuffer buffer, String fieldPath) { + private boolean addFieldPathUpdates(DocumentUpdate update, TokenBuffer buffer, String fieldPath) { int localNesting = buffer.nesting(); buffer.next(); @@ -185,6 +191,7 @@ public class VespaJsonDocumentReader { update.addFieldPathUpdate(fieldPathUpdate); buffer.next(); } + return true; // TODO: Track fullyApplied for fieldPath updates } private AssignFieldPathUpdate readAssignFieldPathUpdate(DocumentType documentType, String fieldPath, TokenBuffer buffer) { @@ -230,4 +237,5 @@ public class VespaJsonDocumentReader { Preconditions.checkState(buffer.next() == null, "Dangling data at end of operation"); Preconditions.checkState(buffer.size() == 0, "Dangling data at end of operation"); } + } diff --git a/document/src/main/java/com/yahoo/document/json/readers/WeightedSetReader.java b/document/src/main/java/com/yahoo/document/json/readers/WeightedSetReader.java index c9af929735e..7a9921498ef 100644 --- a/document/src/main/java/com/yahoo/document/json/readers/WeightedSetReader.java +++ b/document/src/main/java/com/yahoo/document/json/readers/WeightedSetReader.java @@ -10,12 +10,14 @@ import static com.yahoo.document.json.readers.JsonParserHelpers.expectObjectStar public class WeightedSetReader { + public static void fillWeightedSet(TokenBuffer buffer, DataType valueType, @SuppressWarnings("rawtypes") WeightedSet weightedSet) { int initNesting = buffer.nesting(); expectObjectStart(buffer.currentToken()); buffer.next(); iterateThroughWeightedSet(buffer, initNesting, valueType, weightedSet); } + public static void fillWeightedSetUpdate(TokenBuffer buffer, int initNesting, DataType valueType, @SuppressWarnings("rawtypes") WeightedSet weightedSet) { iterateThroughWeightedSet(buffer, initNesting, valueType, weightedSet); } @@ -29,4 +31,5 @@ public class WeightedSetReader { buffer.next(); } } + } diff --git a/document/src/main/java/com/yahoo/document/update/FieldUpdate.java b/document/src/main/java/com/yahoo/document/update/FieldUpdate.java index b2992bf4988..fc4a293f0fb 100644 --- a/document/src/main/java/com/yahoo/document/update/FieldUpdate.java +++ b/document/src/main/java/com/yahoo/document/update/FieldUpdate.java @@ -46,10 +46,10 @@ import java.util.List; * type - any name/value pair which existing in an updatable structure can be addressed by creating the Fields as * needed. For example: * <pre> - * FieldUpdate field=FieldUpdate.createIncrement(new Field("myattribute",DataType.INT),130); + * FieldUpdate field = FieldUpdate.createIncrement(new Field("myattribute",DataType.INT),130); * </pre> * - * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + * @author Einar M R Rosenvinge * @see com.yahoo.document.update.ValueUpdate * @see com.yahoo.document.DocumentUpdate */ @@ -135,7 +135,7 @@ public class FieldUpdate { * @throws IllegalArgumentException if the data type of the value update is not equal to the data type of this field */ public FieldUpdate addValueUpdate(ValueUpdate valueUpdate) { - valueUpdate.checkCompatibility(field.getDataType()); //will throw exception + valueUpdate.checkCompatibility(field.getDataType()); // will throw exception valueUpdates.add(valueUpdate); return this; } @@ -149,7 +149,7 @@ public class FieldUpdate { * @throws IllegalArgumentException if the data type of the value update is not equal to the data type of this field */ public FieldUpdate addValueUpdate(int index, ValueUpdate valueUpdate) { - valueUpdate.checkCompatibility(field.getDataType()); //will throw exception + valueUpdate.checkCompatibility(field.getDataType()); // will throw exception valueUpdates.add(index, valueUpdate); return this; } diff --git a/document/src/main/java/com/yahoo/document/update/ValueUpdate.java b/document/src/main/java/com/yahoo/document/update/ValueUpdate.java index 3aa728ca5b2..74f5ba9d30a 100644 --- a/document/src/main/java/com/yahoo/document/update/ValueUpdate.java +++ b/document/src/main/java/com/yahoo/document/update/ValueUpdate.java @@ -13,7 +13,7 @@ import java.util.List; /** * A value update represents some action to perform to a value. * - * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + * @author Einar M R Rosenvinge * @see com.yahoo.document.update.FieldUpdate * @see com.yahoo.document.DocumentUpdate * @see AddValueUpdate @@ -31,11 +31,7 @@ public abstract class ValueUpdate<T extends FieldValue> { this.valueUpdateClassID = valueUpdateClassID; } - /** - * Returns the valueUpdateClassID of this value update. - * - * @return the valueUpdateClassID of this ValueUpdate - */ + /** Returns the valueUpdateClassID of this value update. */ public ValueUpdateClassID getValueUpdateClassID() { return valueUpdateClassID; } @@ -46,7 +42,7 @@ public abstract class ValueUpdate<T extends FieldValue> { @Override public boolean equals(Object o) { - return o instanceof ValueUpdate && valueUpdateClassID == ((ValueUpdate) o).valueUpdateClassID; + return o instanceof ValueUpdate && valueUpdateClassID == ((ValueUpdate<?>) o).valueUpdateClassID; } @Override diff --git a/document/src/main/java/com/yahoo/vespaxmlparser/FeedOperation.java b/document/src/main/java/com/yahoo/vespaxmlparser/FeedOperation.java index 6b084a55309..69d851b09bc 100644 --- a/document/src/main/java/com/yahoo/vespaxmlparser/FeedOperation.java +++ b/document/src/main/java/com/yahoo/vespaxmlparser/FeedOperation.java @@ -7,7 +7,9 @@ import com.yahoo.document.DocumentUpdate; import com.yahoo.document.TestAndSetCondition; public class FeedOperation { + public enum Type {DOCUMENT, REMOVE, UPDATE, INVALID} + public static final FeedOperation INVALID = new FeedOperation(Type.INVALID); private Type type; @@ -36,4 +38,5 @@ public class FeedOperation { " testandset=" + getCondition() + '}'; } + }
\ No newline at end of file diff --git a/document/src/test/java/com/yahoo/document/json/DocumentUpdateJsonSerializerTest.java b/document/src/test/java/com/yahoo/document/json/DocumentUpdateJsonSerializerTest.java index e396fe8912b..070ede480ac 100644 --- a/document/src/test/java/com/yahoo/document/json/DocumentUpdateJsonSerializerTest.java +++ b/document/src/test/java/com/yahoo/document/json/DocumentUpdateJsonSerializerTest.java @@ -96,7 +96,7 @@ public class DocumentUpdateJsonSerializerTest { private static DocumentUpdate jsonToDocumentUpdate(String jsonDoc, String docId) { final InputStream rawDoc = new ByteArrayInputStream(Utf8.toBytes(jsonDoc)); JsonReader reader = new JsonReader(types, rawDoc, parserFactory); - return (DocumentUpdate) reader.readSingleDocument(DocumentOperationType.UPDATE, docId); + return (DocumentUpdate) reader.readOperation(DocumentOperationType.UPDATE, docId).operation(); } private static String documentUpdateToJson(DocumentUpdate update) { diff --git a/document/src/test/java/com/yahoo/document/json/JsonReaderTestCase.java b/document/src/test/java/com/yahoo/document/json/JsonReaderTestCase.java index 441f1fd28ea..bee2adb10a0 100644 --- a/document/src/test/java/com/yahoo/document/json/JsonReaderTestCase.java +++ b/document/src/test/java/com/yahoo/document/json/JsonReaderTestCase.java @@ -78,6 +78,7 @@ import static com.yahoo.document.json.readers.SingleValueReader.UPDATE_INCREMENT import static com.yahoo.document.json.readers.SingleValueReader.UPDATE_MULTIPLY; import static com.yahoo.test.json.JsonTestHelper.inputJson; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -103,6 +104,8 @@ public class JsonReaderTestCase { DocumentType x = new DocumentType("smoke"); x.addField(new Field("something", DataType.STRING)); x.addField(new Field("nalle", DataType.STRING)); + x.addField(new Field("field1", DataType.STRING)); + x.addField(new Field("field2", DataType.STRING)); x.addField(new Field("int1", DataType.INT)); x.addField(new Field("flag", DataType.BOOL)); types.registerDocumentType(x); @@ -215,8 +218,8 @@ public class JsonReaderTestCase { " 'nalle': 'bamse'", " }", "}")); - DocumentPut put = (DocumentPut) r.readSingleDocument(DocumentOperationType.PUT, - "id:unittest:smoke::doc1"); + DocumentPut put = (DocumentPut) r.readOperation(DocumentOperationType.PUT, + "id:unittest:smoke::doc1").operation(); smokeTestDoc(put.getDocument()); } @@ -226,7 +229,7 @@ public class JsonReaderTestCase { " 'fields': {", " 'something': {", " 'assign': 'orOther' }}}")); - DocumentUpdate doc = (DocumentUpdate) r.readSingleDocument(DocumentOperationType.UPDATE, "id:unittest:smoke::whee"); + DocumentUpdate doc = (DocumentUpdate) r.readOperation(DocumentOperationType.UPDATE, "id:unittest:smoke::whee").operation(); FieldUpdate f = doc.getFieldUpdate("something"); assertEquals(1, f.size()); assertTrue(f.getValueUpdate(0) instanceof AssignValueUpdate); @@ -238,7 +241,7 @@ public class JsonReaderTestCase { " 'fields': {", " 'int1': {", " 'assign': null }}}")); - DocumentUpdate doc = (DocumentUpdate) r.readSingleDocument(DocumentOperationType.UPDATE, "id:unittest:smoke::whee"); + DocumentUpdate doc = (DocumentUpdate) r.readOperation(DocumentOperationType.UPDATE, "id:unittest:smoke::whee").operation(); FieldUpdate f = doc.getFieldUpdate("int1"); assertEquals(1, f.size()); assertTrue(f.getValueUpdate(0) instanceof ClearValueUpdate); @@ -1006,17 +1009,65 @@ public class JsonReaderTestCase { } @Test - public void nonExistingFieldCanBeIgnored() throws IOException{ + public void nonExistingFieldsCanBeIgnoredInPut() throws IOException{ JsonReader r = createReader(inputJson( - "{ 'put': 'id:unittest:smoke::whee',", + "{ ", + " 'put': 'id:unittest:smoke::doc1',", " 'fields': {", - " 'smething': 'smoketest',", - " 'nalle': 'bamse' }}")); + " 'nonexisting1': 'ignored value',", + " 'field1': 'value1',", + " 'nonexisting2': {", + " 'blocks':{", + " 'a':[2.0,3.0],", + " 'b':[4.0,5.0]", + " }", + " },", + " 'field2': 'value2',", + " 'nonexisting3': 'ignored value'", + " }", + "}")); DocumentParseInfo parseInfo = r.parseDocument().get(); DocumentType docType = r.readDocumentType(parseInfo.documentId); DocumentPut put = new DocumentPut(new Document(docType, parseInfo.documentId)); + boolean fullyApplied = new VespaJsonDocumentReader(true).readPut(parseInfo.fieldsBuffer, put); + assertFalse(fullyApplied); + assertNull(put.getDocument().getField("nonexisting1")); + assertEquals("value1", put.getDocument().getFieldValue("field1").toString()); + assertNull(put.getDocument().getField("nonexisting2")); + assertEquals("value2", put.getDocument().getFieldValue("field2").toString()); + assertNull(put.getDocument().getField("nonexisting3")); + } - new VespaJsonDocumentReader(true).readPut(parseInfo.fieldsBuffer, put); + @Test + public void nonExistingFieldsCanBeIgnoredInUpdate() throws IOException{ + JsonReader r = createReader(inputJson( + "{ ", + " 'update': 'id:unittest:smoke::doc1',", + " 'fields': {", + " 'nonexisting1': { 'assign': 'ignored value' },", + " 'field1': { 'assign': 'value1' },", + " 'nonexisting2': { " + + " 'assign': {", + " 'blocks': {", + " 'a':[2.0,3.0],", + " 'b':[4.0,5.0]", + " }", + " }", + " },", + " 'field2': { 'assign': 'value2' },", + " 'nonexisting3': { 'assign': 'ignored value' }", + " }", + "}")); + DocumentParseInfo parseInfo = r.parseDocument().get(); + DocumentType docType = r.readDocumentType(parseInfo.documentId); + DocumentUpdate update = new DocumentUpdate(docType, parseInfo.documentId); + boolean fullyApplied = new VespaJsonDocumentReader(true).readUpdate(parseInfo.fieldsBuffer, update); + assertFalse(fullyApplied); + assertNull(update.getFieldUpdate("nonexisting1")); + assertEquals("value1", update.getFieldUpdate("field1").getValueUpdates().get(0).getValue().getWrappedValue().toString()); + assertNull(update.getFieldUpdate("nonexisting2")); + assertEquals("value2", update.getFieldUpdate("field2").getValueUpdates().get(0).getValue().getWrappedValue().toString()); + assertNull(update.getFieldUpdate("nonexisting3")); } @Test @@ -1044,7 +1095,8 @@ public class JsonReaderTestCase { DocumentParseInfo parseInfo = r.parseDocument().get(); DocumentType docType = r.readDocumentType(parseInfo.documentId); DocumentPut put = new DocumentPut(new Document(docType, parseInfo.documentId)); - new VespaJsonDocumentReader(false).readPut(parseInfo.fieldsBuffer, put); + boolean fullyApplied = new VespaJsonDocumentReader(false).readPut(parseInfo.fieldsBuffer, put); + assertTrue(fullyApplied); smokeTestDoc(put.getDocument()); } @@ -1271,7 +1323,7 @@ public class JsonReaderTestCase { fail("Expected exception"); } catch (IllegalArgumentException e) { - assertEquals("update of document id:unittest:smoke::whee is missing a 'fields' map", e.getMessage()); + assertEquals("Update of document id:unittest:smoke::whee is missing a 'fields' map", e.getMessage()); } } @@ -1287,8 +1339,8 @@ public class JsonReaderTestCase { " 'tensorfield': null", " }", "}")); - DocumentPut put = (DocumentPut) r.readSingleDocument(DocumentOperationType.PUT, - "id:unittest:testnull::doc1"); + DocumentPut put = (DocumentPut) r.readOperation(DocumentOperationType.PUT, + "id:unittest:testnull::doc1").operation(); Document doc = put.getDocument(); assertFieldValueNull(doc, "intfield"); assertFieldValueNull(doc, "stringfield"); @@ -1305,7 +1357,7 @@ public class JsonReaderTestCase { " 'arrayfield': [ null ]", " }", "}")); - r.readSingleDocument(DocumentOperationType.PUT, "id:unittest:testnull::doc1"); + r.readOperation(DocumentOperationType.PUT, "id:unittest:testnull::doc1"); fail(); } @@ -1587,7 +1639,7 @@ public class JsonReaderTestCase { " 'fields': {", " 'something': {", " 'modify': {} }}}")); - reader.readSingleDocument(DocumentOperationType.UPDATE, "id:unittest:smoke::doc1"); + reader.readOperation(DocumentOperationType.UPDATE, "id:unittest:smoke::doc1"); fail("Expected exception"); } catch (IllegalArgumentException e) { diff --git a/documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java b/documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java index 4a77d30ec92..682bcc54a56 100644 --- a/documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java +++ b/documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java @@ -22,6 +22,7 @@ import com.yahoo.vespa.objects.BufferSerializer; public class ProgressToken { private static final Logger log = Logger.getLogger(ProgressToken.class.getName()); + /** * Any bucket kept track of by a <code>ProgressToken</code> instance may * be in one of two states: pending or active. <em>Pending</em> means that @@ -848,4 +849,5 @@ public class ProgressToken { pendingBucketCount = 0; activeBucketCount = 0; } + } diff --git a/documentapi/src/main/java/com/yahoo/documentapi/Response.java b/documentapi/src/main/java/com/yahoo/documentapi/Response.java index 3778189e272..133c3586276 100644 --- a/documentapi/src/main/java/com/yahoo/documentapi/Response.java +++ b/documentapi/src/main/java/com/yahoo/documentapi/Response.java @@ -9,10 +9,8 @@ import static com.yahoo.documentapi.Response.Outcome.ERROR; import static com.yahoo.documentapi.Response.Outcome.SUCCESS; /** - * <p>An asynchronous response from the document api. - * Subclasses of this provide additional response information for particular operations.</p> - * - * <p>This is a <i>value object</i>.</p> + * An asynchronous response from the document api. + * Subclasses of this provide additional response information for particular operations. * * @author bratseth */ diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java index 4a41769ff4c..e2efaf1de10 100644 --- a/documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java @@ -4,10 +4,9 @@ package com.yahoo.documentapi; import com.yahoo.messagebus.Trace; /** - * A session for tracking progress for and potentially receiving data from a - * visitor. + * A session for tracking progress for and potentially receiving data from a visitor. * - * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + * @author Thomas Gundersen */ public interface VisitorSession extends VisitorControlSession { /** @@ -15,21 +14,21 @@ public interface VisitorSession extends VisitorControlSession { * * @return True if visiting is done (either by error or success). */ - public boolean isDone(); + boolean isDone(); /** * Retrieves the last progress token gotten for this visitor. * * @return The progress token. */ - public ProgressToken getProgress(); + ProgressToken getProgress(); /** * Returns the tracing information so far about the visitor. * * @return Returns the trace. */ - public Trace getTrace(); + Trace getTrace(); /** * Waits until visiting is done, or the given timeout (in ms) expires. @@ -39,5 +38,5 @@ public interface VisitorSession extends VisitorControlSession { * @return True if visiting is done (either by error or success). * @throws InterruptedException If an interrupt signal was received while waiting. */ - public boolean waitUntilDone(long timeoutMs) throws InterruptedException; + boolean waitUntilDone(long timeoutMs) throws InterruptedException; } diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java index 15e971329e6..eb9640a38b8 100644 --- a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java @@ -44,4 +44,5 @@ public class MapVisitorMessage extends VisitorMessage { public String toString() { return "MapVisitorMessage(" + data.toString() + ")"; } + } diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories60.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories60.java index c4d4fb216be..099839672a2 100644 --- a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories60.java +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories60.java @@ -202,10 +202,10 @@ public abstract class RoutableFactories60 { buf.putInt(null, msg.getBuckets().size()); for (BucketId id : msg.getBuckets()) { long rawid = id.getRawId(); - long reversed = ((rawid >>> 56) & 0x00000000000000FFl) | ((rawid >>> 40) & 0x000000000000FF00l) | - ((rawid >>> 24) & 0x0000000000FF0000l) | ((rawid >>> 8) & 0x00000000FF000000l) | - ((rawid << 8) & 0x000000FF00000000l) | ((rawid << 24) & 0x0000FF0000000000l) | - ((rawid << 40) & 0x00FF000000000000l) | ((rawid << 56) & 0xFF00000000000000l); + long reversed = ((rawid >>> 56) & 0x00000000000000FFL) | ((rawid >>> 40) & 0x000000000000FF00L) | + ((rawid >>> 24) & 0x0000000000FF0000L) | ((rawid >>> 8) & 0x00000000FF000000L) | + ((rawid << 8) & 0x000000FF00000000L) | ((rawid << 24) & 0x0000FF0000000000L) | + ((rawid << 40) & 0x00FF000000000000L) | ((rawid << 56) & 0xFF00000000000000L); buf.putLong(null, reversed); } diff --git a/documentapi/src/test/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSessionTestCase.java b/documentapi/src/test/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSessionTestCase.java index 6b191b50a57..04f4800231f 100755 --- a/documentapi/src/test/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSessionTestCase.java +++ b/documentapi/src/test/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSessionTestCase.java @@ -56,6 +56,7 @@ import static org.junit.Assert.fail; // TODO replace explicit pre-mockito mock classes with proper mockito mocks wherever possible public class MessageBusVisitorSessionTestCase { + private class MockSender implements MessageBusVisitorSession.Sender { private int maxPending = 1000; private int pendingCount = 0; @@ -2520,4 +2521,5 @@ public class MessageBusVisitorSessionTestCase { // TODO: consider refactoring locking granularity // TODO: figure out if we risk a re-run of the "too many tasks" issue + } diff --git a/documentapi/src/test/java/com/yahoo/documentapi/messagebus/protocol/test/ErrorCodesTest.java b/documentapi/src/test/java/com/yahoo/documentapi/messagebus/protocol/test/ErrorCodesTest.java index 3b4fac60fcd..ba4ae381057 100644 --- a/documentapi/src/test/java/com/yahoo/documentapi/messagebus/protocol/test/ErrorCodesTest.java +++ b/documentapi/src/test/java/com/yahoo/documentapi/messagebus/protocol/test/ErrorCodesTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertEquals; * @author vekterli */ public class ErrorCodesTest { + private class NamedErrorCodes { private final TreeMap<String, Integer> nameAndCode = new TreeMap<>(); diff --git a/eval/CMakeLists.txt b/eval/CMakeLists.txt index 844c81acf52..822a48bffc3 100644 --- a/eval/CMakeLists.txt +++ b/eval/CMakeLists.txt @@ -72,6 +72,7 @@ vespa_define_module( src/tests/instruction/inplace_map_function src/tests/instruction/join_with_number src/tests/instruction/l2_distance + src/tests/instruction/mapped_lookup src/tests/instruction/mixed_112_dot_product src/tests/instruction/mixed_inner_product_function src/tests/instruction/mixed_simple_join_function diff --git a/eval/src/tests/eval/gen_spec/gen_spec_test.cpp b/eval/src/tests/eval/gen_spec/gen_spec_test.cpp index 855b298f295..a52ece90e6a 100644 --- a/eval/src/tests/eval/gen_spec/gen_spec_test.cpp +++ b/eval/src/tests/eval/gen_spec/gen_spec_test.cpp @@ -31,13 +31,13 @@ TEST(DimSpecTest, mapped_dimension) { TEST(DimSpecTest, simple_dictionary_creation) { auto dict = DimSpec::make_dict(5, 1, ""); - std::vector<vespalib::string> expect = {"0", "1", "2", "3", "4"}; + std::vector<vespalib::string> expect = {"1", "2", "3", "4", "5"}; EXPECT_EQ(dict, expect); } TEST(DimSpecTest, advanced_dictionary_creation) { auto dict = DimSpec::make_dict(5, 3, "str_"); - std::vector<vespalib::string> expect = {"str_0", "str_3", "str_6", "str_9", "str_12"}; + std::vector<vespalib::string> expect = {"str_3", "str_6", "str_9", "str_12", "str_15"}; EXPECT_EQ(dict, expect); } @@ -176,50 +176,50 @@ TEST(GenSpecTest, generating_custom_vector) { //----------------------------------------------------------------------------- TensorSpec basic_map = TensorSpec("tensor(a{})") - .add({{"a", "0"}}, 1.0) - .add({{"a", "1"}}, 2.0) - .add({{"a", "2"}}, 3.0); + .add({{"a", "1"}}, 1.0) + .add({{"a", "2"}}, 2.0) + .add({{"a", "3"}}, 3.0); TensorSpec custom_map = TensorSpec("tensor(a{})") - .add({{"a", "s0"}}, 1.0) - .add({{"a", "s5"}}, 2.0) - .add({{"a", "s10"}}, 3.0); + .add({{"a", "s5"}}, 1.0) + .add({{"a", "s10"}}, 2.0) + .add({{"a", "s15"}}, 3.0); TEST(GenSpecTest, generating_basic_map) { EXPECT_EQ(GenSpec().map("a", 3).gen(), basic_map); EXPECT_EQ(GenSpec().map("a", 3, 1).gen(), basic_map); EXPECT_EQ(GenSpec().map("a", 3, 1, "").gen(), basic_map); - EXPECT_EQ(GenSpec().map("a", {"0", "1", "2"}).gen(), basic_map); + EXPECT_EQ(GenSpec().map("a", {"1", "2", "3"}).gen(), basic_map); } TEST(GenSpecTest, generating_custom_map) { EXPECT_EQ(GenSpec().map("a", 3, 5, "s").gen(), custom_map); - EXPECT_EQ(GenSpec().map("a", {"s0", "s5", "s10"}).gen(), custom_map); + EXPECT_EQ(GenSpec().map("a", {"s5", "s10", "s15"}).gen(), custom_map); } //----------------------------------------------------------------------------- TensorSpec basic_mixed = TensorSpec("tensor(a{},b[1],c{},d[3])") - .add({{"a", "0"},{"b", 0},{"c", "0"},{"d", 0}}, 1.0) - .add({{"a", "0"},{"b", 0},{"c", "0"},{"d", 1}}, 2.0) - .add({{"a", "0"},{"b", 0},{"c", "0"},{"d", 2}}, 3.0) - .add({{"a", "1"},{"b", 0},{"c", "0"},{"d", 0}}, 4.0) - .add({{"a", "1"},{"b", 0},{"c", "0"},{"d", 1}}, 5.0) - .add({{"a", "1"},{"b", 0},{"c", "0"},{"d", 2}}, 6.0) - .add({{"a", "2"},{"b", 0},{"c", "0"},{"d", 0}}, 7.0) - .add({{"a", "2"},{"b", 0},{"c", "0"},{"d", 1}}, 8.0) - .add({{"a", "2"},{"b", 0},{"c", "0"},{"d", 2}}, 9.0); + .add({{"a", "1"},{"b", 0},{"c", "1"},{"d", 0}}, 1.0) + .add({{"a", "1"},{"b", 0},{"c", "1"},{"d", 1}}, 2.0) + .add({{"a", "1"},{"b", 0},{"c", "1"},{"d", 2}}, 3.0) + .add({{"a", "2"},{"b", 0},{"c", "1"},{"d", 0}}, 4.0) + .add({{"a", "2"},{"b", 0},{"c", "1"},{"d", 1}}, 5.0) + .add({{"a", "2"},{"b", 0},{"c", "1"},{"d", 2}}, 6.0) + .add({{"a", "3"},{"b", 0},{"c", "1"},{"d", 0}}, 7.0) + .add({{"a", "3"},{"b", 0},{"c", "1"},{"d", 1}}, 8.0) + .add({{"a", "3"},{"b", 0},{"c", "1"},{"d", 2}}, 9.0); TensorSpec inverted_mixed = TensorSpec("tensor(a{},b[1],c{},d[3])") - .add({{"a", "0"},{"b", 0},{"c", "0"},{"d", 0}}, 1.0) - .add({{"a", "1"},{"b", 0},{"c", "0"},{"d", 0}}, 2.0) - .add({{"a", "2"},{"b", 0},{"c", "0"},{"d", 0}}, 3.0) - .add({{"a", "0"},{"b", 0},{"c", "0"},{"d", 1}}, 4.0) - .add({{"a", "1"},{"b", 0},{"c", "0"},{"d", 1}}, 5.0) - .add({{"a", "2"},{"b", 0},{"c", "0"},{"d", 1}}, 6.0) - .add({{"a", "0"},{"b", 0},{"c", "0"},{"d", 2}}, 7.0) - .add({{"a", "1"},{"b", 0},{"c", "0"},{"d", 2}}, 8.0) - .add({{"a", "2"},{"b", 0},{"c", "0"},{"d", 2}}, 9.0); + .add({{"a", "1"},{"b", 0},{"c", "1"},{"d", 0}}, 1.0) + .add({{"a", "2"},{"b", 0},{"c", "1"},{"d", 0}}, 2.0) + .add({{"a", "3"},{"b", 0},{"c", "1"},{"d", 0}}, 3.0) + .add({{"a", "1"},{"b", 0},{"c", "1"},{"d", 1}}, 4.0) + .add({{"a", "2"},{"b", 0},{"c", "1"},{"d", 1}}, 5.0) + .add({{"a", "3"},{"b", 0},{"c", "1"},{"d", 1}}, 6.0) + .add({{"a", "1"},{"b", 0},{"c", "1"},{"d", 2}}, 7.0) + .add({{"a", "2"},{"b", 0},{"c", "1"},{"d", 2}}, 8.0) + .add({{"a", "3"},{"b", 0},{"c", "1"},{"d", 2}}, 9.0); TEST(GenSpecTest, generating_basic_mixed) { EXPECT_EQ(GenSpec().map("a", 3).idx("b", 1).map("c", 1).idx("d", 3).gen(), basic_mixed); diff --git a/eval/src/tests/eval/tensor_lambda/tensor_lambda_test.cpp b/eval/src/tests/eval/tensor_lambda/tensor_lambda_test.cpp index 657a6ca8d0e..a455e5522b3 100644 --- a/eval/src/tests/eval/tensor_lambda/tensor_lambda_test.cpp +++ b/eval/src/tests/eval/tensor_lambda/tensor_lambda_test.cpp @@ -31,7 +31,7 @@ EvalFixture::ParamRepo make_params() { .add("x3_float", GenSpec().idx("x", 3).cells(CellType::FLOAT)) .add("x3_bfloat16", GenSpec().idx("x", 3).cells(CellType::BFLOAT16)) .add("x3_int8", GenSpec().idx("x", 3).cells(CellType::INT8)) - .add("x3m", GenSpec().map("x", 3)) + .add("x3m", GenSpec().map("x", {"0", "1", "2"})) .add("x3y5", GenSpec().idx("x", 3).idx("y", 5)) .add("x3y5_float", GenSpec().idx("x", 3).idx("y", 5).cells(CellType::FLOAT)) .add("x3y5_bfloat16", GenSpec().idx("x", 3).idx("y", 5).cells(CellType::BFLOAT16)) diff --git a/eval/src/tests/eval/value_type/value_type_test.cpp b/eval/src/tests/eval/value_type/value_type_test.cpp index 3d4872c2ab3..245c9c30242 100644 --- a/eval/src/tests/eval/value_type/value_type_test.cpp +++ b/eval/src/tests/eval/value_type/value_type_test.cpp @@ -353,33 +353,34 @@ TEST("require that dimension index can be obtained") { void verify_predicates(const ValueType &type, bool expect_error, bool expect_double, bool expect_tensor, - bool expect_sparse, bool expect_dense) + bool expect_sparse, bool expect_dense, bool expect_mixed) { EXPECT_EQUAL(type.is_error(), expect_error); EXPECT_EQUAL(type.is_double(), expect_double); EXPECT_EQUAL(type.has_dimensions(), expect_tensor); EXPECT_EQUAL(type.is_sparse(), expect_sparse); EXPECT_EQUAL(type.is_dense(), expect_dense); + EXPECT_EQUAL(type.is_mixed(), expect_mixed); } TEST("require that type-related predicate functions work as expected") { - TEST_DO(verify_predicates(type("error"), true, false, false, false, false)); - TEST_DO(verify_predicates(type("double"), false, true, false, false, false)); - TEST_DO(verify_predicates(type("tensor()"), false, true, false, false, false)); - TEST_DO(verify_predicates(type("tensor(x{})"), false, false, true, true, false)); - TEST_DO(verify_predicates(type("tensor(x{},y{})"), false, false, true, true, false)); - TEST_DO(verify_predicates(type("tensor(x[5])"), false, false, true, false, true)); - TEST_DO(verify_predicates(type("tensor(x[5],y[10])"), false, false, true, false, true)); - TEST_DO(verify_predicates(type("tensor(x[5],y{})"), false, false, true, false, false)); - TEST_DO(verify_predicates(type("tensor<float>(x{})"), false, false, true, true, false)); - TEST_DO(verify_predicates(type("tensor<float>(x[5])"), false, false, true, false, true)); - TEST_DO(verify_predicates(type("tensor<float>(x[5],y{})"), false, false, true, false, false)); - TEST_DO(verify_predicates(type("tensor<bfloat16>(x{})"), false, false, true, true, false)); - TEST_DO(verify_predicates(type("tensor<bfloat16>(x[5])"), false, false, true, false, true)); - TEST_DO(verify_predicates(type("tensor<bfloat16>(x[5],y{})"), false, false, true, false, false)); - TEST_DO(verify_predicates(type("tensor<int8>(x{})"), false, false, true, true, false)); - TEST_DO(verify_predicates(type("tensor<int8>(x[5])"), false, false, true, false, true)); - TEST_DO(verify_predicates(type("tensor<int8>(x[5],y{})"), false, false, true, false, false)); + TEST_DO(verify_predicates(type("error"), true, false, false, false, false, false)); + TEST_DO(verify_predicates(type("double"), false, true, false, false, false, false)); + TEST_DO(verify_predicates(type("tensor()"), false, true, false, false, false, false)); + TEST_DO(verify_predicates(type("tensor(x{})"), false, false, true, true, false, false)); + TEST_DO(verify_predicates(type("tensor(x{},y{})"), false, false, true, true, false, false)); + TEST_DO(verify_predicates(type("tensor(x[5])"), false, false, true, false, true, false)); + TEST_DO(verify_predicates(type("tensor(x[5],y[10])"), false, false, true, false, true, false)); + TEST_DO(verify_predicates(type("tensor(x[5],y{})"), false, false, true, false, false, true)); + TEST_DO(verify_predicates(type("tensor<float>(x{})"), false, false, true, true, false, false)); + TEST_DO(verify_predicates(type("tensor<float>(x[5])"), false, false, true, false, true, false)); + TEST_DO(verify_predicates(type("tensor<float>(x[5],y{})"), false, false, true, false, false, true)); + TEST_DO(verify_predicates(type("tensor<bfloat16>(x{})"), false, false, true, true, false, false)); + TEST_DO(verify_predicates(type("tensor<bfloat16>(x[5])"), false, false, true, false, true, false)); + TEST_DO(verify_predicates(type("tensor<bfloat16>(x[5],y{})"), false, false, true, false, false, true)); + TEST_DO(verify_predicates(type("tensor<int8>(x{})"), false, false, true, true, false, false)); + TEST_DO(verify_predicates(type("tensor<int8>(x[5])"), false, false, true, false, true, false)); + TEST_DO(verify_predicates(type("tensor<int8>(x[5],y{})"), false, false, true, false, false, true)); } TEST("require that mapped and indexed dimensions can be counted") { diff --git a/eval/src/tests/instruction/mapped_lookup/CMakeLists.txt b/eval/src/tests/instruction/mapped_lookup/CMakeLists.txt new file mode 100644 index 00000000000..6bc598235ea --- /dev/null +++ b/eval/src/tests/instruction/mapped_lookup/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(eval_mapped_lookup_test_app TEST + SOURCES + mapped_lookup_test.cpp + DEPENDS + vespaeval + GTest::GTest +) +vespa_add_test(NAME eval_mapped_lookup_test_app COMMAND eval_mapped_lookup_test_app) diff --git a/eval/src/tests/instruction/mapped_lookup/mapped_lookup_test.cpp b/eval/src/tests/instruction/mapped_lookup/mapped_lookup_test.cpp new file mode 100644 index 00000000000..1ffc6b04f4b --- /dev/null +++ b/eval/src/tests/instruction/mapped_lookup/mapped_lookup_test.cpp @@ -0,0 +1,126 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/eval/eval/fast_value.h> +#include <vespa/eval/eval/simple_value.h> +#include <vespa/eval/instruction/mapped_lookup.h> +#include <vespa/eval/eval/test/eval_fixture.h> +#include <vespa/eval/eval/test/gen_spec.h> +#include <vespa/vespalib/gtest/gtest.h> + +using namespace vespalib::eval; +using namespace vespalib::eval::test; + +//----------------------------------------------------------------------------- + +struct FunInfo { + using LookFor = MappedLookup; + bool expect_mutable; + FunInfo(bool expect_mutable_in) + : expect_mutable(expect_mutable_in) {} + void verify(const LookFor &fun) const { + EXPECT_EQ(fun.result_is_mutable(), expect_mutable); + } +}; + +void verify_optimized_cell_types(const vespalib::string &expr) { + auto same_stable_types = CellTypeSpace(CellTypeUtils::list_stable_types(), 2).same(); + auto same_unstable_types = CellTypeSpace(CellTypeUtils::list_unstable_types(), 2).same(); + auto different_types = CellTypeSpace(CellTypeUtils::list_types(), 2).different(); + EvalFixture::verify<FunInfo>(expr, {FunInfo(false)}, same_stable_types); + EvalFixture::verify<FunInfo>(expr, {}, same_unstable_types); + EvalFixture::verify<FunInfo>(expr, {}, different_types); +} + +void verify_optimized(const vespalib::string &expr, bool expect_mutable = false) { + CellTypeSpace just_float({CellType::FLOAT}, 2); + EvalFixture::verify<FunInfo>(expr, {FunInfo(expect_mutable)}, just_float); +} + +void verify_not_optimized(const vespalib::string &expr) { + CellTypeSpace just_float({CellType::FLOAT}, 2); + EvalFixture::verify<FunInfo>(expr, {}, just_float); +} + +//----------------------------------------------------------------------------- + +TEST(MappedLookup, expression_can_be_optimized) { + verify_optimized_cell_types("reduce(x1_1*x5_1y5,sum,x)"); +} + +TEST(MappedLookup, key_and_map_can_be_swapped) { + verify_optimized("reduce(x5_1y5*x1_1,sum,x)"); +} + +TEST(MappedLookup, trivial_indexed_dimensions_are_ignored) { + verify_optimized("reduce(c1d1x1_1*a1b1x5_1y5,sum,x,c,d,a,b)"); + verify_optimized("reduce(c1d1x1_1*a1b1x5_1y5,sum,x,c,a)"); + verify_optimized("reduce(c1d1x1_1*a1b1x5_1y5,sum,x)"); +} + +TEST(MappedLookup, mutable_map_gives_mutable_result) { + verify_optimized("reduce(@x1_1*x5_1y5,sum,x)", false); + verify_optimized("reduce(x1_1*@x5_1y5,sum,x)", true); + verify_optimized("reduce(@x5_1y5*x1_1,sum,x)", true); + verify_optimized("reduce(x5_1y5*@x1_1,sum,x)", false); + verify_optimized("reduce(@x5_1y5*@x1_1,sum,x)", true); +} + +TEST(MappedLookup, similar_expressions_are_not_optimized) { + verify_not_optimized("reduce(x1_1*x5_1,sum,x)"); + verify_not_optimized("reduce(x1_1*x5_1y5,sum,y)"); + verify_not_optimized("reduce(x1_1*x5_1y5,sum)"); + verify_not_optimized("reduce(x1_1*x5_1y5z8,sum,x,y)"); + verify_not_optimized("reduce(x1_1*x5_1y5,prod,x)"); + verify_not_optimized("reduce(x1_1y3_3*x5_1y3_2z5,sum,x)"); + verify_not_optimized("reduce(x1_1y3_3*x5_1y3_2z5,sum,x,y)"); + verify_not_optimized("reduce(x1_1y5*x5_1z5,sum,x)"); +} + +enum class KeyType { EMPTY, UNIT, SCALING, MULTI }; +GenSpec make_key(KeyType type) { + switch (type) { + case KeyType::EMPTY: return GenSpec().cells_float().map("x", {}); + case KeyType::UNIT: return GenSpec().cells_float().map("x", {"1"}).seq({1.0}); + case KeyType::SCALING: return GenSpec().cells_float().map("x", {"1"}).seq({5.0}); + case KeyType::MULTI: return GenSpec().cells_float().map("x", {"1", "2", "3"}).seq({1.0}); + } + abort(); +} + +enum class MapType { EMPTY, SMALL, MEDIUM, LARGE1, LARGE2, LARGE3 }; +GenSpec make_map(MapType type) { + switch (type) { + case MapType::EMPTY: return GenSpec().cells_float().idx("y", 5).map("x", {}); + case MapType::SMALL: return GenSpec().cells_float().idx("y", 5).map("x", {"1"}).seq(N(10)); + case MapType::MEDIUM: return GenSpec().cells_float().idx("y", 5).map("x", {"1", "2"}).seq(N(10)); + case MapType::LARGE1: return GenSpec().cells_float().idx("y", 5).map("x", 5, 100).seq(N(10)); + case MapType::LARGE2: return GenSpec().cells_float().idx("y", 5).map("x", 5, 2).seq(N(10)); + case MapType::LARGE3: return GenSpec().cells_float().idx("y", 5).map("x", 5, 1).seq(N(10)); + } + abort(); +} + +std::vector<MapType> map_types_for(KeyType key_type) { + if (key_type == KeyType::MULTI) { + return {MapType::EMPTY, MapType::SMALL, MapType::MEDIUM, MapType::LARGE1, MapType::LARGE2, MapType::LARGE3}; + } else { + return {MapType::EMPTY, MapType::SMALL, MapType::MEDIUM}; + } +} + +TEST(MappedLookup, test_case_interactions) { + for (bool mutable_map: {false, true}) { + vespalib::string expr = mutable_map ? "reduce(a*@b,sum,x)" : "reduce(a*b,sum,x)"; + for (KeyType key_type: {KeyType::EMPTY, KeyType::UNIT, KeyType::SCALING, KeyType::MULTI}) { + auto key = make_key(key_type); + for (MapType map_type: map_types_for(key_type)) { + auto map = make_map(map_type); + EvalFixture::verify<FunInfo>(expr, {FunInfo(mutable_map)}, {key,map}); + } + } + } +} + +//----------------------------------------------------------------------------- + +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/eval/src/vespa/eval/eval/optimize_tensor_function.cpp b/eval/src/vespa/eval/eval/optimize_tensor_function.cpp index 63f0f00a13c..41fa550ce07 100644 --- a/eval/src/vespa/eval/eval/optimize_tensor_function.cpp +++ b/eval/src/vespa/eval/eval/optimize_tensor_function.cpp @@ -35,7 +35,7 @@ #include <vespa/eval/instruction/dense_hamming_distance.h> #include <vespa/eval/instruction/l2_distance.h> #include <vespa/eval/instruction/simple_join_count.h> - +#include <vespa/eval/instruction/mapped_lookup.h> #include <vespa/log/log.h> LOG_SETUP(".eval.eval.optimize_tensor_function"); @@ -85,6 +85,7 @@ const TensorFunction &optimize_for_factory(const ValueBuilderFactory &, const Te child.set(MixedInnerProductFunction::optimize(child.get(), stash)); child.set(DenseHammingDistance::optimize(child.get(), stash)); child.set(SimpleJoinCount::optimize(child.get(), stash)); + child.set(MappedLookup::optimize(child.get(), stash)); }); run_optimize_pass(root, [&stash](const Child &child) { diff --git a/eval/src/vespa/eval/eval/test/eval_fixture.h b/eval/src/vespa/eval/eval/test/eval_fixture.h index d2244cd4f5f..6cbbfca999b 100644 --- a/eval/src/vespa/eval/eval/test/eval_fixture.h +++ b/eval/src/vespa/eval/eval/test/eval_fixture.h @@ -131,6 +131,37 @@ public: static const ValueBuilderFactory &test_factory() { return SimpleValueBuilderFactory::get(); } // Verify the evaluation result and specific tensor function + // details for the given expression with the given parameters. A + // parameter can be tagged as mutable by giving it a name starting + // with '@'. Parameters must be given in automatic discovery order. + + template <typename FunInfo> + static void verify(const vespalib::string &expr, const std::vector<FunInfo> &fun_info, std::vector<GenSpec> param_specs) { + UNWIND_MSG("in verify(%s) with %zu FunInfo", expr.c_str(), fun_info.size()); + auto fun = Function::parse(expr); + REQUIRE_EQ(fun->num_params(), param_specs.size()); + EvalFixture::ParamRepo param_repo; + for (size_t i = 0; i < fun->num_params(); ++i) { + if (fun->param_name(i)[0] == '@') { + param_repo.add_mutable(fun->param_name(i), param_specs[i]); + } else { + param_repo.add(fun->param_name(i), param_specs[i]); + } + } + EvalFixture fixture(prod_factory(), expr, param_repo, true, true); + EvalFixture slow_fixture(prod_factory(), expr, param_repo, false, false); + EvalFixture test_fixture(test_factory(), expr, param_repo, true, true); + REQUIRE_EQ(fixture.result(), test_fixture.result()); + REQUIRE_EQ(fixture.result(), slow_fixture.result()); + REQUIRE_EQ(fixture.result(), EvalFixture::ref(expr, param_repo)); + auto info = fixture.find_all<typename FunInfo::LookFor>(); + REQUIRE_EQ(info.size(), fun_info.size()); + for (size_t i = 0; i < fun_info.size(); ++i) { + fixture.verify_callback<FunInfo>(fun_info[i], *info[i]); + } + } + + // Verify the evaluation result and specific tensor function // details for the given expression with different combinations of // cell types. Parameter names must be valid GenSpec descriptions // ('a5b8'), with an optional mutable prefix ('@a5b8') to denote @@ -138,10 +169,9 @@ public: // trailer starting with '$' ('a5b3$2') to allow multiple // parameters with the same description as well as scalars // ('$this_is_a_scalar'). - + template <typename FunInfo> static void verify(const vespalib::string &expr, const std::vector<FunInfo> &fun_info, CellTypeSpace cell_type_space) { - UNWIND_MSG("in verify(%s) with %zu FunInfo", expr.c_str(), fun_info.size()); auto fun = Function::parse(expr); REQUIRE_EQ(fun->num_params(), cell_type_space.n()); diff --git a/eval/src/vespa/eval/eval/test/gen_spec.cpp b/eval/src/vespa/eval/eval/test/gen_spec.cpp index a175111a429..988ff035bbb 100644 --- a/eval/src/vespa/eval/eval/test/gen_spec.cpp +++ b/eval/src/vespa/eval/eval/test/gen_spec.cpp @@ -54,7 +54,7 @@ DimSpec::make_dict(size_t size, size_t stride, const vespalib::string &prefix) { std::vector<vespalib::string> dict; for (size_t i = 0; i < size; ++i) { - dict.push_back(fmt("%s%zu", prefix.c_str(), i * stride)); + dict.push_back(fmt("%s%zu", prefix.c_str(), (i + 1) * stride)); } return dict; } diff --git a/eval/src/vespa/eval/eval/test/gen_spec.h b/eval/src/vespa/eval/eval/test/gen_spec.h index eab2924ac37..5f507eeb272 100644 --- a/eval/src/vespa/eval/eval/test/gen_spec.h +++ b/eval/src/vespa/eval/eval/test/gen_spec.h @@ -150,6 +150,7 @@ public: _seq = seq_in; return *this; } + GenSpec &seq(const std::vector<double> &numbers) { return seq(Seq({numbers})); } bool bad_scalar() const; ValueType type() const; TensorSpec gen() const; diff --git a/eval/src/vespa/eval/eval/value_type.cpp b/eval/src/vespa/eval/eval/value_type.cpp index 80e62b5f512..dc5ce645a8c 100644 --- a/eval/src/vespa/eval/eval/value_type.cpp +++ b/eval/src/vespa/eval/eval/value_type.cpp @@ -191,6 +191,18 @@ ValueType::is_dense() const return true; } +bool +ValueType::is_mixed() const +{ + bool seen_mapped = false; + bool seen_indexed = false; + for (const auto &dim : dimensions()) { + seen_mapped |= dim.is_mapped(); + seen_indexed |= dim.is_indexed(); + } + return (seen_mapped && seen_indexed); +} + size_t ValueType::count_indexed_dimensions() const { diff --git a/eval/src/vespa/eval/eval/value_type.h b/eval/src/vespa/eval/eval/value_type.h index 0822fa1e4b0..b7a7c92e137 100644 --- a/eval/src/vespa/eval/eval/value_type.h +++ b/eval/src/vespa/eval/eval/value_type.h @@ -60,6 +60,7 @@ public: bool has_dimensions() const { return !_dimensions.empty(); } bool is_sparse() const; bool is_dense() const; + bool is_mixed() const; size_t count_indexed_dimensions() const; size_t count_mapped_dimensions() const; size_t dense_subspace_size() const; diff --git a/eval/src/vespa/eval/instruction/CMakeLists.txt b/eval/src/vespa/eval/instruction/CMakeLists.txt index 2146e3ee8ab..7df3f745e79 100644 --- a/eval/src/vespa/eval/instruction/CMakeLists.txt +++ b/eval/src/vespa/eval/instruction/CMakeLists.txt @@ -31,6 +31,7 @@ vespa_add_library(eval_instruction OBJECT inplace_map_function.cpp join_with_number_function.cpp l2_distance.cpp + mapped_lookup.cpp mixed_112_dot_product.cpp mixed_inner_product_function.cpp mixed_simple_join_function.cpp diff --git a/eval/src/vespa/eval/instruction/mapped_lookup.cpp b/eval/src/vespa/eval/instruction/mapped_lookup.cpp new file mode 100644 index 00000000000..31d3c9766bf --- /dev/null +++ b/eval/src/vespa/eval/instruction/mapped_lookup.cpp @@ -0,0 +1,152 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "mapped_lookup.h" +#include <vespa/eval/eval/wrap_param.h> +#include <vespa/eval/eval/fast_value.hpp> +#include <vespa/vespalib/util/require.h> + +namespace vespalib::eval { + +using namespace tensor_function; +using namespace operation; +using namespace instruction; + +namespace { + +template <typename CT> +ConstArrayRef<CT> my_mapped_lookup_fallback(const Value::Index &key_idx, const Value::Index &map_idx, + const CT *key_cells, const CT *map_cells, size_t res_size, Stash &stash) __attribute__((noinline)); +template <typename CT> +ConstArrayRef<CT> my_mapped_lookup_fallback(const Value::Index &key_idx, const Value::Index &map_idx, + const CT *key_cells, const CT * map_cells, size_t res_size, Stash &stash) +{ + SparseJoinPlan plan(1); + auto result = stash.create_array<CT>(res_size); + SparseJoinState sparse(plan, key_idx, map_idx); + auto outer = sparse.first_index.create_view({}); + auto inner = sparse.second_index.create_view(sparse.second_view_dims); + outer->lookup({}); + while (outer->next_result(sparse.first_address, sparse.first_subspace)) { + inner->lookup(sparse.address_overlap); + if (inner->next_result(sparse.second_only_address, sparse.second_subspace)) { + auto factor = key_cells[sparse.lhs_subspace]; + const CT *match = map_cells + (res_size * sparse.rhs_subspace); + for (size_t i = 0; i < result.size(); ++i) { + result[i] += factor * match[i]; + } + } + } + return result; +} + +template <typename CT> +struct MappedLookupResult { + ArrayRef<CT> value; + MappedLookupResult(size_t res_size, Stash &stash) + : value(stash.create_array<CT>(res_size)) {} + void process_match(CT factor, const CT *match) { + for (size_t i = 0; i < value.size(); ++i) { + value[i] += factor * match[i]; + } + } +}; + +template <typename CT> +ConstArrayRef<CT> my_fast_mapped_lookup(const FastAddrMap &key_map, const FastAddrMap &map_map, + const CT *key_cells, const CT *map_cells, size_t res_size, Stash &stash) +{ + if ((key_map.size() == 1) && (key_cells[0] == 1.0)) { + auto subspace = map_map.lookup_singledim(key_map.labels()[0]); + if (subspace != FastAddrMap::npos()) { + return {map_cells + (res_size * subspace), res_size}; + } else { + return stash.create_array<CT>(res_size); + } + } + MappedLookupResult<CT> result(res_size, stash); + if (key_map.size() <= map_map.size()) { + const auto &labels = key_map.labels(); + for (size_t i = 0; i < labels.size(); ++i) { + auto subspace = map_map.lookup_singledim(labels[i]); + if (subspace != FastAddrMap::npos()) { + result.process_match(key_cells[i], map_cells + (res_size * subspace)); + } + } + } else { + const auto &labels = map_map.labels(); + for (size_t i = 0; i < labels.size(); ++i) { + auto subspace = key_map.lookup_singledim(labels[i]); + if (subspace != FastAddrMap::npos()) { + result.process_match(key_cells[subspace], map_cells + (res_size * i)); + } + } + } + return result.value; +} + +template <typename CT> +void my_mapped_lookup_op(InterpretedFunction::State &state, uint64_t param) { + const auto &res_type = unwrap_param<ValueType>(param); + const auto &key_idx = state.peek(1).index(); + const auto &map_idx = state.peek(0).index(); + const CT *key_cells = state.peek(1).cells().typify<CT>().cbegin(); + const CT *map_cells = state.peek(0).cells().typify<CT>().cbegin(); + auto result = __builtin_expect(are_fast(key_idx, map_idx), true) + ? my_fast_mapped_lookup<CT>(as_fast(key_idx).map, as_fast(map_idx).map, key_cells, map_cells, res_type.dense_subspace_size(), state.stash) + : my_mapped_lookup_fallback<CT>(key_idx, map_idx, key_cells, map_cells, res_type.dense_subspace_size(), state.stash); + state.pop_pop_push(state.stash.create<DenseValueView>(res_type, TypedCells(result))); +} + +bool check_types(const ValueType &res, const ValueType &key, const ValueType &map) { + return ((res.is_dense()) && (key.dense_subspace_size() == 1) && (map.is_mixed()) && + (res.cell_type() == key.cell_type()) && + (res.cell_type() == map.cell_type()) && + ((res.cell_type() == CellType::FLOAT) || (res.cell_type() == CellType::DOUBLE)) && + (key.mapped_dimensions().size() == 1) && + (key.mapped_dimensions() == map.mapped_dimensions()) && + (map.nontrivial_indexed_dimensions() == res.nontrivial_indexed_dimensions())); +} + +} // namespace <unnamed> + +MappedLookup::MappedLookup(const ValueType &res_type, + const TensorFunction &key_in, + const TensorFunction &map_in) + : tensor_function::Op2(res_type, key_in, map_in) +{ +} + +InterpretedFunction::Instruction +MappedLookup::compile_self(const ValueBuilderFactory &, Stash &) const +{ + uint64_t param = wrap_param<ValueType>(result_type()); + if (result_type().cell_type() == CellType::FLOAT) { + return {my_mapped_lookup_op<float>, param}; + } + if (result_type().cell_type() == CellType::DOUBLE) { + return {my_mapped_lookup_op<double>, param}; + } + REQUIRE_FAILED("cell types must be float or double"); +} + +const TensorFunction & +MappedLookup::optimize(const TensorFunction &expr, Stash &stash) +{ + auto reduce = as<Reduce>(expr); + if (reduce && (reduce->aggr() == Aggr::SUM)) { + auto join = as<Join>(reduce->child()); + if (join && (join->function() == Mul::f)) { + const TensorFunction &lhs = join->lhs(); + const TensorFunction &rhs = join->rhs(); + if (check_types(expr.result_type(), lhs.result_type(), rhs.result_type())) { + return stash.create<MappedLookup>(expr.result_type(), lhs, rhs); + } + if (check_types(expr.result_type(), rhs.result_type(), lhs.result_type())) { + return stash.create<MappedLookup>(expr.result_type(), rhs, lhs); + } + } + } + return expr; +} + +} // namespace diff --git a/eval/src/vespa/eval/instruction/mapped_lookup.h b/eval/src/vespa/eval/instruction/mapped_lookup.h new file mode 100644 index 00000000000..2b66c6cf4aa --- /dev/null +++ b/eval/src/vespa/eval/instruction/mapped_lookup.h @@ -0,0 +1,49 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/eval/eval/tensor_function.h> + +namespace vespalib::eval { + +/** + * Tensor function implementing generalized lookup of 'key' in 'map' + * with some type restrictions. + * + * 'key' may only contain the lookup dimension (called 'x' here) + * 'map' must have full mapped overlap with 'key' + * + * Both tensors must have the same cell type, which can be either + * float or double. + * + * The optimized expression looks like this: reduce(key*map,sum,x) + * + * If 'map' is also sparse, the lookup operation is a sparse dot + * product and will be optimized using SparseDotProductFunction + * instead. + * + * The best performance (simple hash lookup with a result referencing + * existing cells without having to copy them) is achieved when a + * single dense subspace in 'map' matches a cell with value 1.0 from + * 'key'. This fast-path can be ensured if this optimization is + * combined with the simple_join_count optimization: + * + * key = tensor(x{}):{my_key:1} + * map = tensor(x{},y[128]) + * fallback = tensor(y[128]) + * + * simple lookup with fallback: + * if(reduce(key*map,count)==128,reduce(key*map,sum,x),fallback) + **/ +class MappedLookup : public tensor_function::Op2 +{ +public: + MappedLookup(const ValueType &res_type, const TensorFunction &key_in, const TensorFunction &map_in); + const TensorFunction &key() const { return lhs(); } + const TensorFunction &map() const { return rhs(); } + InterpretedFunction::Instruction compile_self(const ValueBuilderFactory &factory, Stash &stash) const override; + bool result_is_mutable() const override { return map().result_is_mutable(); } + static const TensorFunction &optimize(const TensorFunction &expr, Stash &stash); +}; + +} // namespace diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index d8e0ceb01bd..ad9a9e094cb 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -79,7 +79,7 @@ public class Flags { public static final UnboundBooleanFlag KEEP_STORAGE_NODE_UP = defineFeatureFlag( "keep-storage-node-up", true, - List.of("hakonhall"), "2022-07-07", "2022-10-07", + List.of("hakonhall"), "2022-07-07", "2022-11-07", "Whether to leave the storage node (with wanted state) UP while the node is permanently down.", "Takes effect immediately for nodes transitioning to permanently down.", ZONE_ID, APPLICATION_ID); @@ -233,20 +233,6 @@ public class Flags { "Takes effect at redeployment", ZONE_ID, APPLICATION_ID); - public static final UnboundIntFlag MAX_CONCURRENT_MERGES_PER_NODE = defineIntFlag( - "max-concurrent-merges-per-node", 16, - List.of("balder", "vekterli"), "2021-06-06", "2022-12-01", - "Specifies max concurrent merges per content node.", - "Takes effect at redeploy", - ZONE_ID, APPLICATION_ID); - - public static final UnboundIntFlag MAX_MERGE_QUEUE_SIZE = defineIntFlag( - "max-merge-queue-size", 100, - List.of("balder", "vekterli"), "2021-06-06", "2022-12-01", - "Specifies max size of merge queue.", - "Takes effect at redeploy", - ZONE_ID, APPLICATION_ID); - public static final UnboundDoubleFlag MIN_NODE_RATIO_PER_GROUP = defineDoubleFlag( "min-node-ratio-per-group", 0.0, List.of("geirst", "vekterli"), "2021-07-16", "2022-11-01", @@ -262,13 +248,6 @@ public class Flags { TENANT_ID, CONSOLE_USER_EMAIL ); - public static final UnboundBooleanFlag UNORDERED_MERGE_CHAINING = defineFeatureFlag( - "unordered-merge-chaining", true, - List.of("vekterli", "geirst"), "2021-11-15", "2022-11-01", - "Enables the use of unordered merge chains for data merge operations", - "Takes effect at redeploy", - ZONE_ID, APPLICATION_ID); - public static final UnboundBooleanFlag IGNORE_THREAD_STACK_SIZES = defineFeatureFlag( "ignore-thread-stack-sizes", false, List.of("arnej"), "2021-11-12", "2022-12-01", @@ -283,7 +262,6 @@ public class Flags { "Takes effect at redeployment", ZONE_ID, APPLICATION_ID); - public static final UnboundIntFlag MAX_COMPACT_BUFFERS = defineIntFlag( "max-compact-buffers", 1, List.of("baldersheim", "geirst", "toregge"), "2021-12-15", "2023-01-01", @@ -291,56 +269,6 @@ public class Flags { "Takes effect at redeployment", ZONE_ID, APPLICATION_ID); - public static final UnboundStringFlag MERGE_THROTTLING_POLICY = defineStringFlag( - "merge-throttling-policy", "STATIC", - List.of("vekterli"), "2022-01-25", "2022-12-01", - "Sets the policy used for merge throttling on the content nodes. " + - "Valid values: STATIC, DYNAMIC", - "Takes effect at redeployment", - ZONE_ID, APPLICATION_ID); - - public static final UnboundDoubleFlag PERSISTENCE_THROTTLING_WS_DECREMENT_FACTOR = defineDoubleFlag( - "persistence-throttling-ws-decrement-factor", 1.2, - List.of("vekterli"), "2022-01-27", "2022-12-01", - "Sets the dynamic throttle policy window size decrement factor for persistence " + - "async throttling. Only applies if DYNAMIC policy is used.", - "Takes effect on redeployment", - ZONE_ID, APPLICATION_ID); - - public static final UnboundDoubleFlag PERSISTENCE_THROTTLING_WS_BACKOFF = defineDoubleFlag( - "persistence-throttling-ws-backoff", 0.95, - List.of("vekterli"), "2022-01-27", "2022-12-01", - "Sets the dynamic throttle policy window size backoff for persistence " + - "async throttling. Only applies if DYNAMIC policy is used. Valid range [0, 1]", - "Takes effect on redeployment", - ZONE_ID, APPLICATION_ID); - - public static final UnboundIntFlag PERSISTENCE_THROTTLING_WINDOW_SIZE = defineIntFlag( - "persistence-throttling-window-size", -1, - List.of("vekterli"), "2022-02-23", "2022-11-01", - "If greater than zero, sets both min and max window size to the given number, effectively " + - "turning dynamic throttling into a static throttling policy. " + - "Only applies if DYNAMIC policy is used.", - "Takes effect on redeployment", - ZONE_ID, APPLICATION_ID); - - public static final UnboundDoubleFlag PERSISTENCE_THROTTLING_WS_RESIZE_RATE = defineDoubleFlag( - "persistence-throttling-ws-resize-rate", 3.0, - List.of("vekterli"), "2022-02-23", "2022-11-01", - "Sets the dynamic throttle policy resize rate. Only applies if DYNAMIC policy is used.", - "Takes effect on redeployment", - ZONE_ID, APPLICATION_ID); - - public static final UnboundBooleanFlag PERSISTENCE_THROTTLING_OF_MERGE_FEED_OPS = defineFeatureFlag( - "persistence-throttling-of-merge-feed-ops", true, - List.of("vekterli"), "2022-02-24", "2022-11-01", - "If true, each put/remove contained within a merge is individually throttled as if it " + - "were a put/remove from a client. If false, merges are throttled at a persistence thread " + - "level, i.e. per ApplyBucketDiff message, regardless of how many document operations " + - "are contained within. Only applies if DYNAMIC policy is used.", - "Takes effect on redeployment", - ZONE_ID, APPLICATION_ID); - public static final UnboundBooleanFlag USE_QRSERVER_SERVICE_NAME = defineFeatureFlag( "use-qrserver-service-name", false, List.of("arnej"), "2022-01-18", "2022-12-31", @@ -471,7 +399,14 @@ public class Flags { List.of("andreer", "gjoranv"), "2022-09-28", "2023-04-01", "Set up a WireGuard endpoint on config servers", "Takes effect on configserver restart", - ZONE_ID, NODE_TYPE); + HOSTNAME); + + public static final UnboundBooleanFlag USE_WIREGUARD_ON_TENANT_HOSTS = defineFeatureFlag( + "use-wireguard-on-tenant-hosts", false, + List.of("andreer", "gjoranv"), "2022-09-28", "2023-04-01", + "Set up a WireGuard endpoint on tenant hosts", + "Takes effect on host admin restart", + HOSTNAME); /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, List<String> owners, diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java index c2a7ce56054..ad1242aa7e9 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/DimensionHelper.java @@ -11,7 +11,9 @@ import java.util.stream.Collectors; * @author hakonhall */ public class DimensionHelper { - private static Map<FetchVector.Dimension, String> serializedDimensions = new HashMap<>(); + + private static final Map<FetchVector.Dimension, String> serializedDimensions = new HashMap<>(); + static { serializedDimensions.put(FetchVector.Dimension.ZONE_ID, "zone"); serializedDimensions.put(FetchVector.Dimension.HOSTNAME, "hostname"); @@ -29,7 +31,7 @@ public class DimensionHelper { } } - private static Map<String, FetchVector.Dimension> deserializedDimensions = serializedDimensions. + private static final Map<String, FetchVector.Dimension> deserializedDimensions = serializedDimensions. entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); public static String toWire(FetchVector.Dimension dimension) { @@ -51,4 +53,5 @@ public class DimensionHelper { } private DimensionHelper() { } + } diff --git a/flags/src/main/java/com/yahoo/vespa/flags/json/FetchVectorHelper.java b/flags/src/main/java/com/yahoo/vespa/flags/json/FetchVectorHelper.java index 0b19be21b76..c0bb3128924 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/json/FetchVectorHelper.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/json/FetchVectorHelper.java @@ -10,6 +10,7 @@ import java.util.stream.Collectors; * @author hakonhall */ public class FetchVectorHelper { + public static Map<String, String> toWire(FetchVector vector) { Map<FetchVector.Dimension, String> map = vector.toMap(); if (map.isEmpty()) return null; @@ -24,4 +25,5 @@ public class FetchVectorHelper { entry -> DimensionHelper.fromWire(entry.getKey()), Map.Entry::getValue))); } + } diff --git a/functions.cmake b/functions.cmake index fd492a55c5b..7fa0b0db954 100644 --- a/functions.cmake +++ b/functions.cmake @@ -221,7 +221,7 @@ function(vespa_add_library TARGET) endif() if(NOT ARG_OBJECT AND NOT ARG_STATIC AND NOT ARG_INTERFACE AND NOT ARG_ALLOW_UNRESOLVED_SYMBOLS AND DEFINED VESPA_DISALLOW_UNRESOLVED_SYMBOLS_IN_SHARED_LIBRARIES) - __add_private_target_link_option(${TARGET} ${VESPA_DISALLOW_UNRESOLVED_SYMBOLS_IN_SHARED_LIBRARIES}) + target_link_options(${TARGET} PRIVATE ${VESPA_DISALLOW_UNRESOLVED_SYMBOLS_IN_SHARED_LIBRARIES}) endif() __add_target_to_module(${TARGET}) @@ -779,17 +779,3 @@ function(vespa_suppress_warnings_for_protobuf_sources) set_source_files_properties(${ARG_SOURCES} PROPERTIES COMPILE_FLAGS "-Wno-array-bounds -Wno-suggest-override -Wno-inline ${VESPA_DISABLE_UNUSED_WARNING}") endif() endfunction() - -function(__add_private_target_link_option TARGET TARGET_LINK_OPTION) - if(COMMAND target_link_options) - target_link_options(${TARGET} PRIVATE ${TARGET_LINK_OPTION}) - else() - get_target_property(TARGET_LINK_FLAGS ${TARGET} LINK_FLAGS) - if (NOT DEFINED TARGET_LINK_FLAGS OR ${TARGET_LINK_FLAGS} STREQUAL "" OR ${TARGET_LINK_FLAGS} STREQUAL "TARGET_LINK_FLAGS-NOTFOUND") - set(TARGET_LINK_FLAGS ${TARGET_LINK_OPTION}) - else() - set(TARGET_LINK_FLAGS "${TARGET_LINK_FLAGS} ${TARGET_LINK_OPTION}") - endif() - set_target_properties(${TARGET} PROPERTIES LINK_FLAGS "${TARGET_LINK_FLAGS}") - endif() -endfunction() diff --git a/hosted-zone-api/abi-spec.json b/hosted-zone-api/abi-spec.json index 0d9a6409759..213f2883da0 100644 --- a/hosted-zone-api/abi-spec.json +++ b/hosted-zone-api/abi-spec.json @@ -41,6 +41,8 @@ ], "methods": [ "public void <init>(int, java.util.List)", + "public void <init>(java.lang.String, int, java.util.List)", + "public java.lang.String id()", "public int size()", "public java.util.List indices()", "public boolean equals(java.lang.Object)", diff --git a/hosted-zone-api/src/main/java/ai/vespa/cloud/Cluster.java b/hosted-zone-api/src/main/java/ai/vespa/cloud/Cluster.java index 218545383e6..ce278848f29 100644 --- a/hosted-zone-api/src/main/java/ai/vespa/cloud/Cluster.java +++ b/hosted-zone-api/src/main/java/ai/vespa/cloud/Cluster.java @@ -12,15 +12,25 @@ import java.util.Objects; */ public class Cluster { + private final String id; private final int size; private final List<Integer> indices; + // TODO: Remove on Vespa 9 + @Deprecated(forRemoval = true) public Cluster(int size, List<Integer> indices) { - Objects.requireNonNull(indices, "Indices cannot be null!"); + this("default", size, indices); + } + + public Cluster(String id, int size, List<Integer> indices) { + this.id = Objects.requireNonNull(id); this.size = size; - this.indices = Collections.unmodifiableList(indices); + this.indices = List.copyOf(Objects.requireNonNull(indices)); } + /** Returns the id of this cluster set in services.xml */ + public String id() { return id; } + /** Returns the number of nodes in this cluster. */ public int size() { return size; } @@ -32,15 +42,16 @@ public class Cluster { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Cluster cluster = (Cluster) o; - return size == cluster.size && - indices.equals(cluster.indices); + if ( ! (o instanceof Cluster other)) return false; + if ( ! this.id.equals(other.id)) return false; + if ( this.size != other.size) return false; + if ( ! this.indices.equals(other.indices)) return false; + return true; } @Override public int hashCode() { - return Objects.hash(size, indices); + return Objects.hash(id, size, indices); } } diff --git a/hosted-zone-api/src/test/java/ai/vespa/cloud/SystemInfoTest.java b/hosted-zone-api/src/test/java/ai/vespa/cloud/SystemInfoTest.java index 2bae9f0c9e2..d97bba51c71 100644 --- a/hosted-zone-api/src/test/java/ai/vespa/cloud/SystemInfoTest.java +++ b/hosted-zone-api/src/test/java/ai/vespa/cloud/SystemInfoTest.java @@ -18,7 +18,7 @@ public class SystemInfoTest { ApplicationId application = new ApplicationId("tenant1", "application1", "instance1"); Zone zone = new Zone(Environment.dev, "us-west-1"); Cloud cloud = new Cloud("aws"); - Cluster cluster = new Cluster(1, List.of()); + Cluster cluster = new Cluster("clusterId", 1, List.of()); Node node = new Node(0); SystemInfo info = new SystemInfo(application, zone, cloud, cluster, node); @@ -59,9 +59,11 @@ public class SystemInfoTest { @Test void testCluster() { + String id = "clusterId"; int size = 1; var indices = List.of(1); - Cluster cluster = new Cluster(size, indices); + Cluster cluster = new Cluster("clusterId", size, indices); + assertEquals(id, cluster.id()); assertEquals(size, cluster.size()); assertEquals(indices, cluster.indices()); } diff --git a/integration/intellij/src/main/java/ai/vespa/intellij/schema/findUsages/FunctionDefinitionFinder.java b/integration/intellij/src/main/java/ai/vespa/intellij/schema/findUsages/FunctionDefinitionFinder.java index 7d8aafc8f8b..89e0165d55e 100644 --- a/integration/intellij/src/main/java/ai/vespa/intellij/schema/findUsages/FunctionDefinitionFinder.java +++ b/integration/intellij/src/main/java/ai/vespa/intellij/schema/findUsages/FunctionDefinitionFinder.java @@ -51,7 +51,7 @@ public class FunctionDefinitionFinder extends UsageFinder { private boolean findDefinitionIn(RankProfile profile) { for (var entry : profile.definedFunctions().entrySet()) { // TODO: Resolve the right function in the list by parameter count - if (entry.getKey().equals(functionNameToFind) && entry.getValue().size() > 0) + if (entry.key().equals(functionNameToFind) && entry.getValue().size() > 0) processor().process(new UsageInfo(entry.getValue().get(0).definition())); } return true; diff --git a/integration/intellij/src/main/java/ai/vespa/intellij/schema/model/Schema.java b/integration/intellij/src/main/java/ai/vespa/intellij/schema/model/Schema.java index 88cbdd3e07a..5479949d25a 100644 --- a/integration/intellij/src/main/java/ai/vespa/intellij/schema/model/Schema.java +++ b/integration/intellij/src/main/java/ai/vespa/intellij/schema/model/Schema.java @@ -92,7 +92,7 @@ public class Schema { Map<String, List<Function>> functions = new HashMap<>(); for (var profile : rankProfiles().values()) { for (var entry : profile.definedFunctions().entrySet()) - functions.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).addAll(entry.getValue()); + functions.computeIfAbsent(entry.key(), k -> new ArrayList<>()).addAll(entry.getValue()); } return functions; } diff --git a/jdisc_core/abi-spec.json b/jdisc_core/abi-spec.json index 5beb723465b..8171b416b0c 100644 --- a/jdisc_core/abi-spec.json +++ b/jdisc_core/abi-spec.json @@ -905,7 +905,8 @@ ], "methods": [ "public abstract void start()", - "public abstract void close()" + "public abstract void close()", + "public boolean isMultiplexed()" ], "fields": [] } diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java b/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java index af57cf39e73..fc8699c907f 100644 --- a/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/HeaderFields.java @@ -86,8 +86,8 @@ public class HeaderFields implements Map<String, List<String>> { } /** - * <p>Adds the given value to the entry of the specified key. If no entry exists for the given key, a new one is - * created containing only the given value.</p> + * Adds the given value to the entry of the specified key. If no entry exists for the given key, a new one is + * created containing only the given value. * * @param key The key with which the specified value is to be associated. * @param value The value to be added to the list associated with the specified key. @@ -102,11 +102,11 @@ public class HeaderFields implements Map<String, List<String>> { } /** - * <p>Adds the given values to the entry of the specified key. If no entry exists for the given key, a new one is - * created containing only the given values.</p> + * Adds the given values to the entry of the specified key. If no entry exists for the given key, a new one is + * created containing only the given values. * - * @param key The key with which the specified value is to be associated. - * @param values The values to be added to the list associated with the specified key. + * @param key the key with which the specified value is to be associated. + * @param values the values to be added to the list associated with the specified key. */ public void add(String key, List<String> values) { List<String> lst = content.get(key); @@ -118,10 +118,10 @@ public class HeaderFields implements Map<String, List<String>> { } /** - * <p>Adds all the entries of the given map to this. This is the same as calling {@link #add(String, List)} for each - * entry in <code>values</code>.</p> + * Adds all the entries of the given map to this. This is the same as calling {@link #add(String, List)} for each + * entry in <code>values</code>. * - * @param values The values to be added to this. + * @param values the values to be added to this. */ public void addAll(Map<? extends String, ? extends List<String>> values) { for (Entry<? extends String, ? extends List<String>> entry : values.entrySet()) { @@ -193,11 +193,11 @@ public class HeaderFields implements Map<String, List<String>> { } /** - * <p>Convenience method for retrieving the first value of a named header field. If the header is not set, or if the - * value list is empty, this method returns null.</p> + * Convenience method for retrieving the first value of a named header field. If the header is not set, or if the + * value list is empty, this method returns null. * - * @param key The key whose first value to return. - * @return The first value of the named header, or null. + * @param key the key whose first value to return + * @return the first value of the named header, or null */ public String getFirst(String key) { List<String> lst = get(key); @@ -208,12 +208,12 @@ public class HeaderFields implements Map<String, List<String>> { } /** - * <p>Convenience method for checking whether or not a named header field is <em>true</em>. To satisfy this, the + * Convenience method for checking whether a named header field is <em>true</em>. To satisfy this, the * header field needs to have at least 1 entry, and Boolean.valueOf() of all its values must parse as - * <em>true</em>.</p> + * <em>true</em>. * - * @param key The key whose values to parse as a boolean. - * @return The boolean value of the named header. + * @param key the key whose values to parse as a boolean. + * @return the boolean value of the named header. */ public boolean isTrue(String key) { List<String> lst = content.get(key); @@ -249,10 +249,10 @@ public class HeaderFields implements Map<String, List<String>> { } /** - * <p>Returns an unmodifiable list of all key-value pairs of this. This provides a flattened view on the content of - * this map.</p> + * Returns an unmodifiable list of all key-value pairs of this. This provides a flattened view on the content of + * this map. * - * @return The collection of entries. + * @return the collection of entries */ public List<Entry<String, String>> entries() { List<Entry<String, String>> list = new ArrayList<>(content.size()); @@ -304,6 +304,7 @@ public class HeaderFields implements Map<String, List<String>> { public String toString() { return key + '=' + value; } + } } diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java index d450d18d952..318ce5fa61b 100644 --- a/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/handler/ResponseHandler.java @@ -11,7 +11,7 @@ import com.yahoo.jdisc.service.ClientProvider; * {@link ClientProvider#handleRequest(Request, ResponseHandler)} and {@link RequestHandler#handleRequest(Request, * ResponseHandler)}).</p> * - * <p>The jDISC API has intentionally been designed as not to provide a implicit reference from Response to + * <p>The jDISC API is designed to not provide an implicit reference from Response to * corresponding Request, but rather leave that to the implementation of context-aware ResponseHandlers. By creating * light-weight ResponseHandlers on a per-Request basis, any necessary reference can be embedded within.</p> * diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/refcount/DebugReferencesWithStack.java b/jdisc_core/src/main/java/com/yahoo/jdisc/refcount/DebugReferencesWithStack.java index db6c266534f..1910c088263 100644 --- a/jdisc_core/src/main/java/com/yahoo/jdisc/refcount/DebugReferencesWithStack.java +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/refcount/DebugReferencesWithStack.java @@ -17,6 +17,7 @@ import java.util.logging.Logger; * @author baldersheim */ public class DebugReferencesWithStack implements References { + private static final Logger log = Logger.getLogger(DebugReferencesWithStack.class.getName()); private final Map<Throwable, Object> activeReferences = new HashMap<>(); private final DestructableResource resource; diff --git a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java index ef72e643dae..ecdc30400f1 100644 --- a/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java +++ b/jdisc_core/src/main/java/com/yahoo/jdisc/service/ServerProvider.java @@ -49,4 +49,12 @@ public interface ServerProvider extends SharedResource { * Application} shutdown code.</p> */ void close(); + + /** + * Whether multiple instances of this can coexist, by means of a multiplexer on top of any exclusive resource. + * If this is true, new instances to replace old ones, during a graph generation switch, will be started before + * the obsolete ones are stopped; otherwise, the old will be stopped, and then the new ones started. + */ + default boolean isMultiplexed() { return false; } + } diff --git a/jrt/pom.xml b/jrt/pom.xml index c4fb87d24c4..b43c2b3899c 100644 --- a/jrt/pom.xml +++ b/jrt/pom.xml @@ -38,7 +38,7 @@ </dependency> <dependency> <!-- required due to bug in maven dependency resolving - bouncycastle is compile scope in security-utils, yet it is not part of test scope here --> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>bcpkix-jdk18on</artifactId> <scope>test</scope> </dependency> </dependencies> diff --git a/linguistics/src/main/java/com/yahoo/language/opennlp/LanguageDetectorFactory.java b/linguistics/src/main/java/com/yahoo/language/opennlp/LanguageDetectorFactory.java index 305aead056b..0cf4634c6c3 100644 --- a/linguistics/src/main/java/com/yahoo/language/opennlp/LanguageDetectorFactory.java +++ b/linguistics/src/main/java/com/yahoo/language/opennlp/LanguageDetectorFactory.java @@ -2,10 +2,6 @@ package com.yahoo.language.opennlp; import opennlp.tools.langdetect.LanguageDetectorContextGenerator; -import opennlp.tools.util.normalizer.EmojiCharSequenceNormalizer; -import opennlp.tools.util.normalizer.NumberCharSequenceNormalizer; -import opennlp.tools.util.normalizer.ShrinkCharSequenceNormalizer; -import opennlp.tools.util.normalizer.TwitterCharSequenceNormalizer; /** * Overrides the UrlCharSequenceNormalizer, which has a bad regex, until fixed: https://issues.apache.org/jira/browse/OPENNLP-1350 @@ -18,11 +14,7 @@ public class LanguageDetectorFactory extends opennlp.tools.langdetect.LanguageDe @Override public LanguageDetectorContextGenerator getContextGenerator() { return new DefaultLanguageDetectorContextGenerator(1, 3, - EmojiCharSequenceNormalizer.getInstance(), - UrlCharSequenceNormalizer.getInstance(), - TwitterCharSequenceNormalizer.getInstance(), - NumberCharSequenceNormalizer.getInstance(), - ShrinkCharSequenceNormalizer.getInstance()); + VespaCharSequenceNormalizer.getInstance()); } } diff --git a/linguistics/src/main/java/com/yahoo/language/opennlp/VespaCharSequenceNormalizer.java b/linguistics/src/main/java/com/yahoo/language/opennlp/VespaCharSequenceNormalizer.java new file mode 100644 index 00000000000..df8f3fad520 --- /dev/null +++ b/linguistics/src/main/java/com/yahoo/language/opennlp/VespaCharSequenceNormalizer.java @@ -0,0 +1,51 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.language.opennlp; + +import opennlp.tools.util.normalizer.CharSequenceNormalizer; + +import java.util.function.IntConsumer; +import java.util.stream.IntStream; + +/** + * Simple normalizer + * + * @author arnej + */ +public class VespaCharSequenceNormalizer implements CharSequenceNormalizer { + + private static final VespaCharSequenceNormalizer INSTANCE = new VespaCharSequenceNormalizer(); + + public static VespaCharSequenceNormalizer getInstance() { + return INSTANCE; + } + + // filter replacing sequences of non-letters with a single space + static class OnlyLetters implements IntStream.IntMapMultiConsumer { + boolean addSpace = false; + public void accept(int codepoint, IntConsumer target) { + if (WordCharDetector.isWordChar(codepoint)) { + if (addSpace) { + target.accept(' '); + addSpace = false; + } + target.accept(Character.toLowerCase(codepoint)); + } else { + addSpace = true; + } + } + } + + public CharSequence normalize(CharSequence text) { + if (text.isEmpty()) { + return text; + } + var r = text + .codePoints() + .mapMulti(new OnlyLetters()) + .collect(StringBuilder::new, + StringBuilder::appendCodePoint, + StringBuilder::append); + return r; + } + +} diff --git a/linguistics/src/main/java/com/yahoo/language/opennlp/WordCharDetector.java b/linguistics/src/main/java/com/yahoo/language/opennlp/WordCharDetector.java new file mode 100644 index 00000000000..d7e3f88ae8d --- /dev/null +++ b/linguistics/src/main/java/com/yahoo/language/opennlp/WordCharDetector.java @@ -0,0 +1,48 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.language.opennlp; + +class WordCharDetector { + public static boolean isWordChar(int codepoint) { + int unicodeGeneralCategory = Character.getType(codepoint); + switch (unicodeGeneralCategory) { + case Character.LOWERCASE_LETTER: + case Character.OTHER_LETTER: + case Character.TITLECASE_LETTER: + case Character.UPPERCASE_LETTER: + case Character.MODIFIER_LETTER: + return true; +/* + * these are the other categories, currently considered non-word-chars: + * + case Character.CONNECTOR_PUNCTUATION: + case Character.CONTROL: + case Character.CURRENCY_SYMBOL: + case Character.DASH_PUNCTUATION: + case Character.ENCLOSING_MARK: + case Character.END_PUNCTUATION: + case Character.FINAL_QUOTE_PUNCTUATION: + case Character.FORMAT: + case Character.INITIAL_QUOTE_PUNCTUATION: + case Character.MATH_SYMBOL: + case Character.MODIFIER_SYMBOL: + case Character.NON_SPACING_MARK: + case Character.OTHER_PUNCTUATION: + case Character.OTHER_SYMBOL: + case Character.PRIVATE_USE: + case Character.START_PUNCTUATION: + case Character.SURROGATE: + case Character.UNASSIGNED: + case Character.DECIMAL_DIGIT_NUMBER: + case Character.LETTER_NUMBER: + case Character.OTHER_NUMBER: + case Character.COMBINING_SPACING_MARK: + case Character.LINE_SEPARATOR: + case Character.SPACE_SEPARATOR: + case Character.PARAGRAPH_SEPARATOR: + * + */ + default: + return false; + } + } +} diff --git a/logserver/src/main/java/com/yahoo/logserver/handlers/archive/LogWriterLRUCache.java b/logserver/src/main/java/com/yahoo/logserver/handlers/archive/LogWriterLRUCache.java index c499c1c3054..c2be7f98f15 100644 --- a/logserver/src/main/java/com/yahoo/logserver/handlers/archive/LogWriterLRUCache.java +++ b/logserver/src/main/java/com/yahoo/logserver/handlers/archive/LogWriterLRUCache.java @@ -10,7 +10,6 @@ import java.util.Map; /** * @author Bjorn Borud */ -@SuppressWarnings("serial") public class LogWriterLRUCache extends LinkedHashMap<Integer, LogWriter> { private static final Logger log = Logger.getLogger(LogWriterLRUCache.class.getName()); diff --git a/logserver/src/test/java/com/yahoo/logserver/handlers/logmetrics/test/LogMetricsTestCase.java b/logserver/src/test/java/com/yahoo/logserver/handlers/logmetrics/test/LogMetricsTestCase.java index 5602d5be0d6..ffd9ce15778 100644 --- a/logserver/src/test/java/com/yahoo/logserver/handlers/logmetrics/test/LogMetricsTestCase.java +++ b/logserver/src/test/java/com/yahoo/logserver/handlers/logmetrics/test/LogMetricsTestCase.java @@ -17,6 +17,7 @@ import static org.junit.Assert.*; * @author hmusum */ public class LogMetricsTestCase { + // Some of the tests depend upon the number of messages for a // host, log level etc. to succeed, so you may have update the // tests if you change something in mStrings. config, debug and @@ -72,7 +73,7 @@ public class LogMetricsTestCase { * each level). */ @Test - public void testLevelCountAggregated() throws java.io.IOException, InvalidLogFormatException { + public void testLevelCountAggregated() { LogMetricsHandler a = new LogMetricsHandler(); for (LogMessage aMsg : msg) { @@ -84,28 +85,13 @@ public class LogMetricsTestCase { for (Map.Entry<String, Long> entry : levelCount.entrySet()) { String key = entry.getKey(); switch (key) { - case "config": - assertEquals(entry.getValue(), Long.valueOf(1)); - break; - case "info": - assertEquals(entry.getValue(), Long.valueOf(4)); - break; - case "warning": - assertEquals(entry.getValue(), Long.valueOf(1)); - break; - case "severe": - assertEquals(entry.getValue(), Long.valueOf(0)); - break; - case "error": - assertEquals(entry.getValue(), Long.valueOf(1)); - break; - case "fatal": - assertEquals(entry.getValue(), Long.valueOf(1)); - break; - case "debug": - assertEquals(entry.getValue(), Long.valueOf(0)); // always 0 - - break; + case "config" -> assertEquals(entry.getValue(), Long.valueOf(1)); + case "info" -> assertEquals(entry.getValue(), Long.valueOf(4)); + case "warning" -> assertEquals(entry.getValue(), Long.valueOf(1)); + case "severe" -> assertEquals(entry.getValue(), Long.valueOf(0)); + case "error" -> assertEquals(entry.getValue(), Long.valueOf(1)); + case "fatal" -> assertEquals(entry.getValue(), Long.valueOf(1)); + case "debug" -> assertEquals(entry.getValue(), Long.valueOf(0)); // always 0 } } a.close(); diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/NetworkMultiplexer.java b/messagebus/src/main/java/com/yahoo/messagebus/network/NetworkMultiplexer.java index d36579d5be1..959fff488b1 100644 --- a/messagebus/src/main/java/com/yahoo/messagebus/network/NetworkMultiplexer.java +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/NetworkMultiplexer.java @@ -59,7 +59,7 @@ public class NetworkMultiplexer implements NetworkOwner { net.registerSession(session); } else if (owners.contains(owner)) - throw new IllegalArgumentException("Session '" + session + "' with owner '" + owner + "' already registered with this"); + throw new IllegalArgumentException("Session '" + session + "' with owner '" + owner + "' already registered with " + this); owners.push(owner); return owners; @@ -68,12 +68,12 @@ public class NetworkMultiplexer implements NetworkOwner { public void unregisterSession(String session, NetworkOwner owner, boolean broadcast) { sessions.computeIfPresent(session, (name, owners) -> { - owners.remove(owner); - if (owners.isEmpty()) { + if (owners.size() == 1 && owners.contains(owner)) { if (broadcast) net.unregisterSession(session); return null; } + owners.remove(owner); return owners; }); } @@ -103,7 +103,7 @@ public class NetworkMultiplexer implements NetworkOwner { /** Attach the network owner to this, allowing this to forward messages to it. */ public void attach(NetworkOwner owner) { if (owners.contains(owner)) - throw new IllegalArgumentException(owner + " is already attached to this"); + throw new IllegalArgumentException(owner + " is already attached to " + this); owners.add(owner); } @@ -111,7 +111,7 @@ public class NetworkMultiplexer implements NetworkOwner { /** Detach the network owner from this, no longer allowing messages to it, and shutting down this is ownerless. */ public void detach(NetworkOwner owner) { if ( ! owners.remove(owner)) - throw new IllegalArgumentException(owner + " not attached to this"); + throw new IllegalArgumentException(owner + " not attached to " + this); destroyIfOwnerless(); } @@ -137,12 +137,7 @@ public class NetworkMultiplexer implements NetworkOwner { @Override public String toString() { - return "NetworkMultiplexer{" + - "net=" + net + - ", owners=" + owners + - ", sessions=" + sessions + - ", destructible=" + disowned + - '}'; + return "network multiplexer with owners: " + owners + ", sessions: " + sessions + " and destructible: " + disowned.get(); } }
\ No newline at end of file diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTable.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTable.java index 30c1a2fd5b3..74ae46353c5 100644 --- a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTable.java +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTable.java @@ -165,43 +165,29 @@ public class RoutingTable { * Constructs a new iterator based on a given map. This is private so that only a {@link RoutingTable} can * create one. * - * @param hops The map to iterate through. + * @param hops the map to iterate through */ private HopIterator(Map<String, HopBlueprint> hops) { it = hops.entrySet().iterator(); next(); } - /** - * Steps to the next hop in the map. - */ + /** Steps to the next hop in the map. */ public void next() { entry = it.hasNext() ? it.next() : null; } - /** - * Returns whether or not this iterator is valid. - * - * @return True if valid. - */ + /** Returns whether this iterator is valid. */ public boolean isValid() { return entry != null; } - /** - * Returns the name of the current hop. - * - * @return The name. - */ + /** Returns the name of the current hop. */ public String getName() { return entry.getKey(); } - /** - * Returns the current hop. - * - * @return The hop. - */ + /** Returns the current hop. */ public HopBlueprint getHop() { return entry.getValue(); } @@ -220,43 +206,29 @@ public class RoutingTable { * Constructs a new iterator based on a given map. This is private so that only a {@link RoutingTable} can * create one. * - * @param routes The map to iterate through. + * @param routes the map to iterate through */ private RouteIterator(Map<String, Route> routes) { it = routes.entrySet().iterator(); next(); } - /** - * Steps to the next route in the map. - */ + /** Steps to the next route in the map. */ public void next() { entry = it.hasNext() ? it.next() : null; } - /** - * Returns whether or not this iterator is valid. - * - * @return True if valid. - */ + /** Returns whether this iterator is valid. */ public boolean isValid() { return entry != null; } - /** - * Returns the name of the current route. - * - * @return The name. - */ + /** Returns the name of the current route. */ public String getName() { return entry.getKey(); } - /** - * Returns the current route. - * - * @return The route. - */ + /** Returns the current route. */ public Route getRoute() { return entry.getValue(); } diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/NetworkMultiplexerTest.java b/messagebus/src/test/java/com/yahoo/messagebus/network/NetworkMultiplexerTest.java index 60da1b49d90..57ded9f750d 100644 --- a/messagebus/src/test/java/com/yahoo/messagebus/network/NetworkMultiplexerTest.java +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/NetworkMultiplexerTest.java @@ -43,7 +43,8 @@ public class NetworkMultiplexerTest { fail("Illegal to register same session multiple times with the same owner"); } catch (IllegalArgumentException expected) { - assertEquals("Session 's1' with owner 'mock owner' already registered with this", expected.getMessage()); + assertEquals("Session 's1' with owner 'mock owner' already registered with network multiplexer with owners: [mock owner], sessions: {s1=[mock owner]} and destructible: false", + expected.getMessage()); } assertEquals(Set.of("s1"), net.registered); diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/ApplicationMetricsRetriever.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/ApplicationMetricsRetriever.java index 1cb6e1d665b..900f7df6ea2 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/ApplicationMetricsRetriever.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/http/application/ApplicationMetricsRetriever.java @@ -167,7 +167,7 @@ public class ApplicationMetricsRetriever extends AbstractComponent implements Ru } catch (InterruptedException | ExecutionException | TimeoutException e) { Throwable cause = e.getCause(); if ( e instanceof ExecutionException && ((cause instanceof SocketException) || cause instanceof ConnectTimeoutException)) { - log.log(Level.FINE, "Failed retrieving metrics for '" + entry.getKey() + "' : " + cause.getMessage()); + log.log(Level.FINE, "Failed retrieving metrics for '" + entry.getKey() + "' : " + cause.getMessage()); } else { log.log(Level.WARNING, "Failed retrieving metrics for '" + entry.getKey() + "' : ", e); } diff --git a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/YamasJsonUtil.java b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/YamasJsonUtil.java index b5b8e30a218..13b2a8d859c 100644 --- a/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/YamasJsonUtil.java +++ b/metrics-proxy/src/main/java/ai/vespa/metricsproxy/metric/model/json/YamasJsonUtil.java @@ -27,6 +27,7 @@ import static java.util.logging.Level.WARNING; * @author gjoranv */ public class YamasJsonUtil { + private static final Logger log = Logger.getLogger(YamasJsonUtil.class.getName()); private static final JsonFactory factory = JsonFactory.builder() .enable(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/WireguardMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/WireguardMaintainer.java new file mode 100644 index 00000000000..f7f1a421cd8 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/WireguardMaintainer.java @@ -0,0 +1,14 @@ +package com.yahoo.vespa.hosted.node.admin.maintenance; + +import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAgentContext; + +/** + * Ensures that wireguard-go is running on the host. + * + * @author gjoranv + */ +public interface WireguardMaintainer { + + void converge(NodeAgentContext context); + +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java index ece494a34d7..54aa136d877 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java @@ -167,7 +167,7 @@ public class CoredumpHandler { */ String getMetadata(NodeAgentContext context, ContainerPath coredumpDirectory, Supplier<Map<String, Object>> nodeAttributesSupplier) throws IOException { UnixPath metadataPath = new UnixPath(coredumpDirectory.resolve(METADATA_FILE_NAME)); - if (!Files.exists(metadataPath.toPath())) { + if (!metadataPath.exists()) { ContainerPath coredumpFile = findCoredumpFileInProcessingDirectory(coredumpDirectory); Map<String, Object> metadata = new HashMap<>(coreCollector.collect(context, coredumpFile)); metadata.putAll(nodeAttributesSupplier.get()); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java index ea352791b36..20ea29381f3 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java @@ -24,6 +24,7 @@ import com.yahoo.vespa.hosted.node.admin.container.ContainerResources; import com.yahoo.vespa.hosted.node.admin.container.RegistryCredentials; import com.yahoo.vespa.hosted.node.admin.container.RegistryCredentialsProvider; import com.yahoo.vespa.hosted.node.admin.maintenance.StorageMaintainer; +import com.yahoo.vespa.hosted.node.admin.maintenance.WireguardMaintainer; import com.yahoo.vespa.hosted.node.admin.maintenance.acl.AclMaintainer; import com.yahoo.vespa.hosted.node.admin.maintenance.identity.CredentialsMaintainer; import com.yahoo.vespa.hosted.node.admin.maintenance.servicedump.VespaServiceDumper; @@ -71,6 +72,7 @@ public class NodeAgentImpl implements NodeAgent { private final Duration warmUpDuration; private final DoubleFlag containerCpuCap; private final VespaServiceDumper serviceDumper; + private final Optional<WireguardMaintainer> wireguardMaintainer; private Thread loopThread; private ContainerState containerState = UNKNOWN; @@ -101,16 +103,15 @@ public class NodeAgentImpl implements NodeAgent { } - // Created in NodeAdminImpl public NodeAgentImpl(NodeAgentContextSupplier contextSupplier, NodeRepository nodeRepository, Orchestrator orchestrator, ContainerOperations containerOperations, RegistryCredentialsProvider registryCredentialsProvider, StorageMaintainer storageMaintainer, FlagSource flagSource, List<CredentialsMaintainer> credentialsMaintainers, Optional<AclMaintainer> aclMaintainer, Optional<HealthChecker> healthChecker, Clock clock, - VespaServiceDumper serviceDumper) { + VespaServiceDumper serviceDumper, Optional<WireguardMaintainer> wireguardMaintainer) { this(contextSupplier, nodeRepository, orchestrator, containerOperations, registryCredentialsProvider, storageMaintainer, flagSource, credentialsMaintainers, aclMaintainer, healthChecker, clock, - DEFAULT_WARM_UP_DURATION, serviceDumper); + DEFAULT_WARM_UP_DURATION, serviceDumper, wireguardMaintainer); } public NodeAgentImpl(NodeAgentContextSupplier contextSupplier, NodeRepository nodeRepository, @@ -118,7 +119,8 @@ public class NodeAgentImpl implements NodeAgent { RegistryCredentialsProvider registryCredentialsProvider, StorageMaintainer storageMaintainer, FlagSource flagSource, List<CredentialsMaintainer> credentialsMaintainers, Optional<AclMaintainer> aclMaintainer, Optional<HealthChecker> healthChecker, Clock clock, - Duration warmUpDuration, VespaServiceDumper serviceDumper) { + Duration warmUpDuration, VespaServiceDumper serviceDumper, + Optional<WireguardMaintainer> wireguardMaintainer) { this.contextSupplier = contextSupplier; this.nodeRepository = nodeRepository; this.orchestrator = orchestrator; @@ -132,6 +134,7 @@ public class NodeAgentImpl implements NodeAgent { this.warmUpDuration = warmUpDuration; this.containerCpuCap = PermanentFlags.CONTAINER_CPU_CAP.bindTo(flagSource); this.serviceDumper = serviceDumper; + this.wireguardMaintainer = wireguardMaintainer; } @Override @@ -495,6 +498,7 @@ public class NodeAgentImpl implements NodeAgent { } aclMaintainer.ifPresent(maintainer -> maintainer.converge(context)); + wireguardMaintainer.ifPresent(maintainer -> maintainer.converge(context)); startServicesIfNeeded(context); resumeNodeIfNeeded(context); if (healthChecker.isPresent()) { diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java index 1773eb4be25..2f9b282c44e 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integration/ContainerTester.java @@ -95,7 +95,7 @@ public class ContainerTester implements AutoCloseable { new NodeAgentImpl(contextSupplier, nodeRepository, orchestrator, containerOperations, () -> RegistryCredentials.none, storageMaintainer, flagSource, Collections.emptyList(), Optional.empty(), Optional.empty(), clock, Duration.ofSeconds(-1), - VespaServiceDumper.DUMMY_INSTANCE) { + VespaServiceDumper.DUMMY_INSTANCE, Optional.empty()) { @Override public void converge(NodeAgentContext context) { super.converge(context); phaser.arriveAndAwaitAdvance(); diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java index a7697e5cb5f..fb132c9b717 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImplTest.java @@ -789,7 +789,7 @@ public class NodeAgentImplTest { return new NodeAgentImpl(contextSupplier, nodeRepository, orchestrator, containerOperations, () -> RegistryCredentials.none, storageMaintainer, flagSource, List.of(credentialsMaintainer), Optional.of(aclMaintainer), Optional.of(healthChecker), - clock, warmUpDuration, VespaServiceDumper.DUMMY_INSTANCE); + clock, warmUpDuration, VespaServiceDumper.DUMMY_INSTANCE, Optional.empty()); } private void mockGetContainer(DockerImage dockerImage, boolean isRunning) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java index 9cba823500b..d9e0e4292e8 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/Node.java @@ -468,10 +468,13 @@ public final class Node implements Nodelike { /** Returns a copy of this node with the current reboot generation set to the given number at the given instant */ public Node withCurrentRebootGeneration(long generation, Instant instant) { - if (generation < status.reboot().current()) + if (generation == status.reboot().current()) { + return this; // No change + } + if (generation < status.reboot().current()) { throw new IllegalArgumentException("Cannot set reboot generation to " + generation + - ": lower than current generation: " + status.reboot().current()); - + ": lower than current generation: " + status.reboot().current()); + } Status newStatus = status().withReboot(status().reboot().withCurrent(generation)); History newHistory = history.with(new History.Event(History.Event.Type.rebooted, Agent.system, instant)); return this.with(newStatus).with(newHistory); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java index 822ed338b56..87b5719cc19 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/DynamicProvisioningMaintainer.java @@ -168,7 +168,7 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { nodeRepository().nodes().write(updatedNode, mutex); } } catch (RuntimeException e) { - log.log(Level.WARNING, "Failed to rebuild " + host.hostname() + ": " + Exceptions.toMessageString(e) + ", will retry in " + interval()); + log.log(Level.WARNING, "Failed to rebuild " + host.hostname() + ", will retry in " + interval(), e); } } } @@ -293,7 +293,8 @@ public class DynamicProvisioningMaintainer extends NodeRepositoryMaintainer { nodeRepository().nodes().addNodes(hosts, Agent.DynamicProvisioningMaintainer); return hosts; } catch (NodeAllocationException | IllegalArgumentException | IllegalStateException e) { - throw new NodeAllocationException("Failed to provision " + count + " " + nodeResources + ": " + e.getMessage()); + throw new NodeAllocationException("Failed to provision " + count + " " + nodeResources + ": " + e.getMessage(), + ! (e instanceof NodeAllocationException nae) || nae.retryable()); } catch (RuntimeException e) { throw new RuntimeException("Failed to provision " + count + " " + nodeResources + ", will retry in " + interval(), e); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java index 3e7abe8f053..32eac49a288 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailer.java @@ -77,23 +77,6 @@ public class NodeFailer extends NodeRepositoryMaintainer { int throttledHostFailures = 0; int throttledNodeFailures = 0; - // Ready nodes - try (Mutex lock = nodeRepository().nodes().lockUnallocated()) { - for (FailingNode failing : findReadyFailingNodes()) { - attempts++; - if (throttle(failing.node())) { - failures++; - if (failing.node().type().isHost()) - throttledHostFailures++; - else - throttledNodeFailures++; - continue; - } - nodeRepository().nodes().fail(failing.node().hostname(), Agent.NodeFailer, failing.reason()); - } - } - - // Active nodes for (FailingNode failing : findActiveFailingNodes()) { attempts++; if (!failAllowedFor(failing.node().type())) continue; @@ -116,22 +99,6 @@ public class NodeFailer extends NodeRepositoryMaintainer { return asSuccessFactor(attempts, failures); } - private Collection<FailingNode> findReadyFailingNodes() { - Set<FailingNode> failingNodes = new HashSet<>(); - for (Node node : nodeRepository().nodes().list(Node.State.ready)) { - Node hostNode = node.parentHostname().flatMap(parent -> nodeRepository().nodes().node(parent)).orElse(node); - List<String> failureReports = reasonsToFailHost(hostNode); - if (failureReports.size() > 0) { - if (hostNode.equals(node)) { - failingNodes.add(new FailingNode(node, "Host has failure reports: " + failureReports)); - } else { - failingNodes.add(new FailingNode(node, "Parent (" + hostNode + ") has failure reports: " + failureReports)); - } - } - } - return failingNodes; - } - private Collection<FailingNode> findActiveFailingNodes() { Set<FailingNode> failingNodes = new HashSet<>(); NodeList activeNodes = nodeRepository().nodes().list(Node.State.active); @@ -150,7 +117,7 @@ public class NodeFailer extends NodeRepositoryMaintainer { for (Node node : activeNodes) { if (allSuspended(node, activeNodes)) { - Node host = node.parentHostname().flatMap(parent -> activeNodes.node(parent)).orElse(node); + Node host = node.parentHostname().flatMap(activeNodes::node).orElse(node); if (host.type().isHost()) { List<String> failureReports = reasonsToFailHost(host); if ( ! failureReports.isEmpty()) { @@ -175,7 +142,7 @@ public class NodeFailer extends NodeRepositoryMaintainer { /** Returns whether node has any kind of hardware issue */ static boolean hasHardwareIssue(Node node, NodeList allNodes) { - Node host = node.parentHostname().flatMap(parent -> allNodes.node(parent)).orElse(node); + Node host = node.parentHostname().flatMap(allNodes::node).orElse(node); return reasonsToFailHost(host).size() > 0; } @@ -344,30 +311,6 @@ public class NodeFailer extends NodeRepositoryMaintainer { } - private static class FailingNode { - - private final Node node; - private final String reason; - - public FailingNode(Node node, String reason) { - this.node = node; - this.reason = reason; - } - - public Node node() { return node; } - public String reason() { return reason; } - - @Override - public boolean equals(Object other) { - if ( ! (other instanceof FailingNode)) return false; - return ((FailingNode)other).node().equals(this.node()); - } - - @Override - public int hashCode() { - return node.hashCode(); - } - - } + private record FailingNode(Node node, String reason) { } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeHealthTracker.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeHealthTracker.java index b43e2ae051f..624492a14f3 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeHealthTracker.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeHealthTracker.java @@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.provision.maintenance; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationLockException; -import com.yahoo.config.provision.HostLivenessTracker; import com.yahoo.jdisc.Metric; import com.yahoo.lang.MutableInteger; import com.yahoo.transaction.Mutex; @@ -13,12 +12,10 @@ import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Agent; -import com.yahoo.vespa.hosted.provision.node.History; import com.yahoo.vespa.service.monitor.ServiceMonitor; import com.yahoo.yolean.Exceptions; import java.time.Duration; -import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; @@ -34,41 +31,18 @@ import static java.util.stream.Collectors.counting; */ public class NodeHealthTracker extends NodeRepositoryMaintainer { - /** Provides information about the status of ready hosts */ - private final HostLivenessTracker hostLivenessTracker; - /** Provides (more accurate) information about the status of active hosts */ private final ServiceMonitor serviceMonitor; - public NodeHealthTracker(HostLivenessTracker hostLivenessTracker, - ServiceMonitor serviceMonitor, NodeRepository nodeRepository, + public NodeHealthTracker(ServiceMonitor serviceMonitor, NodeRepository nodeRepository, Duration interval, Metric metric) { super(nodeRepository, interval, metric); - this.hostLivenessTracker = hostLivenessTracker; this.serviceMonitor = serviceMonitor; } @Override protected double maintain() { - return ( updateReadyNodeLivenessEvents() + updateActiveNodeDownState() ) / 2; - } - - private double updateReadyNodeLivenessEvents() { - // Update node last request events through ZooKeeper to collect request to all config servers. - // We do this here ("lazily") to avoid writing to zk for each config request. - try (Mutex lock = nodeRepository().nodes().lockUnallocated()) { - for (Node node : nodeRepository().nodes().list(Node.State.ready)) { - Optional<Instant> lastLocalRequest = hostLivenessTracker.lastRequestFrom(node.hostname()); - if (lastLocalRequest.isEmpty()) continue; - - if (!node.history().hasEventAfter(History.Event.Type.requested, lastLocalRequest.get())) { - History updatedHistory = node.history() - .with(new History.Event(History.Event.Type.requested, Agent.NodeHealthTracker, lastLocalRequest.get())); - nodeRepository().nodes().write(node.with(updatedHistory), lock); - } - } - } - return 1.0; + return updateActiveNodeDownState(); } /** diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java index dac6ee61ef3..708d8b59eb0 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/NodeRepositoryMaintenance.java @@ -5,7 +5,6 @@ import com.yahoo.component.AbstractComponent; import com.yahoo.component.annotation.Inject; import com.yahoo.concurrent.maintenance.Maintainer; import com.yahoo.config.provision.Deployer; -import com.yahoo.config.provision.HostLivenessTracker; import com.yahoo.config.provision.InfraDeployer; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.Zone; @@ -33,7 +32,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent { @SuppressWarnings("unused") @Inject public NodeRepositoryMaintenance(NodeRepository nodeRepository, Deployer deployer, InfraDeployer infraDeployer, - HostLivenessTracker hostLivenessTracker, ServiceMonitor serviceMonitor, + ServiceMonitor serviceMonitor, Zone zone, Metric metric, ProvisionServiceProvider provisionServiceProvider, FlagSource flagSource, MetricsFetcher metricsFetcher) { @@ -46,7 +45,7 @@ public class NodeRepositoryMaintenance extends AbstractComponent { maintainers.add(infrastructureProvisioner); maintainers.add(new NodeFailer(deployer, nodeRepository, defaults.failGrace, defaults.nodeFailerInterval, defaults.throttlePolicy, metric)); - maintainers.add(new NodeHealthTracker(hostLivenessTracker, serviceMonitor, nodeRepository, defaults.nodeFailureStatusUpdateInterval, metric)); + maintainers.add(new NodeHealthTracker(serviceMonitor, nodeRepository, defaults.nodeFailureStatusUpdateInterval, metric)); maintainers.add(new ExpeditedChangeApplicationMaintainer(deployer, metric, nodeRepository, defaults.expeditedChangeRedeployInterval)); maintainers.add(new ReservationExpirer(nodeRepository, defaults.reservationExpiry, metric)); maintainers.add(new RetiredExpirer(nodeRepository, deployer, metric, defaults.retiredInterval, defaults.retiredExpiry)); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/History.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/History.java index 14fa2e1c8ff..4aca1cbd056 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/History.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/History.java @@ -46,8 +46,10 @@ public class History { private static ImmutableMap<Event.Type, Event> toImmutableMap(Collection<Event> events) { ImmutableMap.Builder<Event.Type, Event> builder = new ImmutableMap.Builder<>(); - for (Event event : events) + for (Event event : events) { + if (event.type() == Event.Type.requested) continue; // TODO (freva): Remove requested event after 8.70 builder.put(event.type(), event); + } return builder.build(); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java index 89fdf9d4b2a..91219ed0ce2 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/OsVersions.java @@ -2,6 +2,8 @@ package com.yahoo.vespa.hosted.provision.os; import com.yahoo.component.Version; +import com.yahoo.config.provision.Cloud; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.curator.Lock; import com.yahoo.vespa.flags.BooleanFlag; @@ -38,20 +40,20 @@ public class OsVersions { private final NodeRepository nodeRepository; private final CuratorDatabaseClient db; - private final boolean dynamicProvisioning; private final int maxDelegatedUpgrades; private final BooleanFlag softRebuildFlag; + private final Cloud cloud; public OsVersions(NodeRepository nodeRepository) { - this(nodeRepository, nodeRepository.zone().getCloud().dynamicProvisioning(), MAX_DELEGATED_UPGRADES); + this(nodeRepository, nodeRepository.zone().getCloud(), MAX_DELEGATED_UPGRADES); } - OsVersions(NodeRepository nodeRepository, boolean dynamicProvisioning, int maxDelegatedUpgrades) { + OsVersions(NodeRepository nodeRepository, Cloud cloud, int maxDelegatedUpgrades) { this.nodeRepository = Objects.requireNonNull(nodeRepository); this.db = nodeRepository.database(); - this.dynamicProvisioning = dynamicProvisioning; this.maxDelegatedUpgrades = maxDelegatedUpgrades; this.softRebuildFlag = Flags.SOFT_REBUILD.bindTo(nodeRepository.flagSource()); + this.cloud = Objects.requireNonNull(cloud); // Read and write all versions to make sure they are stored in the latest version of the serialized format try (var lock = db.lockOsVersionChange()) { @@ -141,13 +143,13 @@ public class OsVersions { /** Returns the upgrader to use when upgrading given node type to target */ private OsUpgrader chooseUpgrader(NodeType nodeType, Optional<Version> target) { - if (dynamicProvisioning) { - boolean softRebuild = softRebuildFlag.value(); - RetiringOsUpgrader retiringOsUpgrader = new RetiringOsUpgrader(nodeRepository, softRebuild); - if (softRebuild) { + if (cloud.dynamicProvisioning()) { + boolean canSoftRebuild = cloud.name().equals(CloudName.AWS) && softRebuildFlag.value(); + RetiringOsUpgrader retiringOsUpgrader = new RetiringOsUpgrader(nodeRepository, canSoftRebuild); + if (canSoftRebuild) { // If soft rebuild is enabled, we can use RebuildingOsUpgrader for hosts with remote storage. // RetiringOsUpgrader is then only used for hosts with local storage. - return new CompositeOsUpgrader(List.of(new RebuildingOsUpgrader(nodeRepository, softRebuild), + return new CompositeOsUpgrader(List.of(new RebuildingOsUpgrader(nodeRepository, canSoftRebuild), retiringOsUpgrader)); } return retiringOsUpgrader; diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java index b4e304155a6..a5ebc6b3efc 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/persistence/CuratorDatabaseClient.java @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.provision.persistence; +import ai.vespa.http.DomainName; import com.yahoo.component.Version; import com.yahoo.concurrent.UncheckedTimeoutException; import com.yahoo.config.provision.ApplicationId; @@ -484,15 +485,12 @@ public class CuratorDatabaseClient { transaction.onCommitted(() -> { for (var lb : loadBalancers) { if (lb.state() == fromState) continue; + Optional<String> target = lb.instance().flatMap(instance -> instance.hostname().map(DomainName::value).or(instance::ipAddress)); if (fromState == null) { - log.log(Level.INFO, () -> "Creating " + lb.id() + lb.instance() - .map(instance -> " (" + instance.hostname() + ")") - .orElse("") + + log.log(Level.INFO, () -> "Creating " + lb.id() + target.map(t -> " (" + t + ")").orElse("") + " in " + lb.state()); } else { - log.log(Level.INFO, () -> "Moving " + lb.id() + lb.instance() - .map(instance -> " (" + instance.hostname() + ")") - .orElse("") + + log.log(Level.INFO, () -> "Moving " + lb.id() + target.map(t -> " (" + t + ")").orElse("") + " from " + fromState + " to " + lb.state()); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java index 5d9d13c48dc..35f04683157 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/GroupPreparer.java @@ -132,9 +132,9 @@ public class GroupPreparer { } if (! allocation.fulfilled() && requestedNodes.canFail()) - throw new NodeAllocationException((cluster.group().isPresent() ? "Node allocation failure on " + - cluster.group().get() : "") + - allocation.allocationFailureDetails()); + throw new NodeAllocationException((cluster.group().isPresent() ? "Node allocation failure on " + cluster.group().get() + : "") + allocation.allocationFailureDetails(), + true); // Carry out and return allocation nodeRepository.nodes().reserve(allocation.reservableNodes()); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java index ef6c0da9169..820a654c620 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/provisioning/Preparer.java @@ -43,7 +43,8 @@ class Preparer { catch (NodeAllocationException e) { throw new NodeAllocationException("Could not satisfy " + requestedNodes + ( wantedGroups > 1 ? " (in " + wantedGroups + " groups)" : "") + - " in " + application + " " + cluster + ": " + e.getMessage()); + " in " + application + " " + cluster + ": " + e.getMessage(), + e.retryable()); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/ContainerConfig.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/ContainerConfig.java index 83a80847ec8..024f071abb1 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/ContainerConfig.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/ContainerConfig.java @@ -21,7 +21,6 @@ public class ContainerConfig { " <component id='com.yahoo.vespa.hosted.provision.testutils.MockDeployer'/>\n" + " <component id='com.yahoo.vespa.hosted.provision.testutils.MockInfraDeployer'/>\n" + " <component id='com.yahoo.vespa.hosted.provision.testutils.MockProvisioner'/>\n" + - " <component id='com.yahoo.vespa.hosted.provision.testutils.TestHostLivenessTracker'/>\n" + " <component id='com.yahoo.vespa.hosted.provision.testutils.ServiceMonitorStub'/>\n" + " <component id='com.yahoo.vespa.hosted.provision.testutils.MockDuperModel'/>\n" + " <component id='com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors'/>\n" + diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java index 13753c12664..3ebaf764115 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockHostProvisioner.java @@ -66,7 +66,7 @@ public class MockHostProvisioner implements HostProvisioner { Optional<CloudAccount> cloudAccount) { Flavor hostFlavor = this.hostFlavor.orElseGet(() -> flavors.stream().filter(f -> compatible(f, resources)) .findFirst() - .orElseThrow(() -> new NodeAllocationException("No host flavor matches " + resources))); + .orElseThrow(() -> new NodeAllocationException("No host flavor matches " + resources, true))); List<ProvisionedHost> hosts = new ArrayList<>(); for (int index : provisionIndices) { String hostHostname = hostType == NodeType.host ? "hostname" + index : hostType.name() + index; diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/TestHostLivenessTracker.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/TestHostLivenessTracker.java deleted file mode 100644 index 28d0d5f89d7..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/TestHostLivenessTracker.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.provision.testutils; - -import com.yahoo.config.provision.HostLivenessTracker; - -import java.time.Clock; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** This is a fully functional implementation */ -public class TestHostLivenessTracker implements HostLivenessTracker { - - private final Clock clock; - private final Map<String, Instant> lastRequestFromHost = new HashMap<>(); - - public TestHostLivenessTracker(Clock clock) { - this.clock = clock; - } - - @Override - public void receivedRequestFrom(String hostname) { - lastRequestFromHost.put(hostname, clock.instant()); - } - - @Override - public Optional<Instant> lastRequestFrom(String hostname) { - return Optional.ofNullable(lastRequestFromHost.get(hostname)); - } - -} diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/QuestMetricsDbTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/QuestMetricsDbTest.java index 9e3213fc977..90e67a9b0cc 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/QuestMetricsDbTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/autoscale/QuestMetricsDbTest.java @@ -13,12 +13,13 @@ import java.io.File; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -35,9 +36,7 @@ public class QuestMetricsDbTest { @Test public void testNodeMetricsReadWrite() { - String dataDir = "data/QuestMetricsDbReadWrite"; - IOUtils.recursiveDeleteDir(new File(dataDir)); - IOUtils.createDirectory(dataDir + "/metrics"); + String dataDir = createEmptyDataDir("QuestMetricsDbReadWrite", "metrics"); ManualClock clock = new ManualClock("2020-10-01T00:00:00"); QuestMetricsDb db = new QuestMetricsDb(dataDir, clock); Instant startTime = clock.instant(); @@ -81,9 +80,7 @@ public class QuestMetricsDbTest { @Test public void testClusterMetricsReadWrite() { - String dataDir = "data/QuestMetricsDbReadWrite"; - IOUtils.recursiveDeleteDir(new File(dataDir)); - IOUtils.createDirectory(dataDir + "/clusterMetrics"); + String dataDir = createEmptyDataDir("QuestMetricsDbReadWrite", "clusterMetrics"); ManualClock clock = new ManualClock("2020-10-01T00:00:00"); QuestMetricsDb db = new QuestMetricsDb(dataDir, clock); Instant startTime = clock.instant(); @@ -134,9 +131,7 @@ public class QuestMetricsDbTest { @Test public void testWriteOldData() { - String dataDir = "data/QuestMetricsDbWriteOldData"; - IOUtils.recursiveDeleteDir(new File(dataDir)); - IOUtils.createDirectory(dataDir + "/metrics"); + String dataDir = createEmptyDataDir("QuestMetricsDbWriteOldData", "metrics"); ManualClock clock = new ManualClock("2020-10-01T00:00:00"); QuestMetricsDb db = new QuestMetricsDb(dataDir, clock); Instant startTime = clock.instant(); @@ -161,9 +156,7 @@ public class QuestMetricsDbTest { @Test public void testGc() { - String dataDir = "data/QuestMetricsDbGc"; - IOUtils.recursiveDeleteDir(new File(dataDir)); - IOUtils.createDirectory(dataDir + "/metrics"); + String dataDir = createEmptyDataDir("QuestMetricsDbGc", "metrics"); ManualClock clock = new ManualClock(); int days = 10; // The first metrics are this many days in the past clock.retreat(Duration.ofDays(10)); @@ -189,7 +182,7 @@ public class QuestMetricsDbTest { @Ignore @Test public void testReadingAndAppendingToExistingData() { - String dataDir = "data/QuestMetricsDbExistingData"; + String dataDir = dataDir("QuestMetricsDbExistingData"); if ( ! new File(dataDir).exists()) { System.out.println("No existing data to check"); return; @@ -219,9 +212,7 @@ public class QuestMetricsDbTest { @Ignore @Test public void updateExistingData() { - String dataDir = "data/QuestMetricsDbExistingData"; - IOUtils.recursiveDeleteDir(new File(dataDir)); - IOUtils.createDirectory(dataDir + "/metrics"); + String dataDir = createEmptyDataDir("QuestMetricsDbExistingData", "metrics"); ManualClock clock = new ManualClock("2020-10-01T00:00:00"); QuestMetricsDb db = new QuestMetricsDb(dataDir, clock); Instant startTime = clock.instant(); @@ -249,16 +240,6 @@ public class QuestMetricsDbTest { return timeseries; } - private List<ClusterMetricSnapshot> clusterTimeseries(int count, Duration sampleRate, ManualClock clock, - ClusterSpec.Id cluster) { - List<ClusterMetricSnapshot> timeseries = new ArrayList<>(); - for (int i = 1; i <= count; i++) { - timeseries.add(new ClusterMetricSnapshot(clock.instant(), 30.0, 0.0)); - clock.advance(sampleRate); - } - return timeseries; - } - private Collection<Pair<String, NodeMetricSnapshot>> timeseriesAt(int countPerHost, Instant at, String ... hosts) { Collection<Pair<String, NodeMetricSnapshot>> timeseries = new ArrayList<>(); for (int i = 1; i <= countPerHost; i++) { @@ -272,4 +253,17 @@ public class QuestMetricsDbTest { return timeseries; } + private static String dataDir(String name) { + return "target/questdb/" + name; + } + + private static String createEmptyDataDir(String name, String... subPath) { + String dataDir = dataDir(name); + IOUtils.recursiveDeleteDir(new File(dataDir)); + String path = Stream.concat(Stream.of(dataDir), Arrays.stream(subPath)) + .collect(Collectors.joining("/")); + IOUtils.createDirectory(path); + return dataDir; + } + } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java index f67e9cd8345..977d72c11ea 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java @@ -27,7 +27,6 @@ import com.yahoo.vespa.hosted.provision.provisioning.NodeRepositoryProvisioner; import com.yahoo.vespa.hosted.provision.provisioning.ProvisioningTester; import com.yahoo.vespa.hosted.provision.testutils.MockDeployer; import com.yahoo.vespa.hosted.provision.testutils.ServiceMonitorStub; -import com.yahoo.vespa.hosted.provision.testutils.TestHostLivenessTracker; import java.time.Clock; import java.time.Duration; @@ -63,7 +62,6 @@ public class NodeFailTester { public ServiceMonitorStub serviceMonitor; public MockDeployer deployer; public TestMetric metric; - private final TestHostLivenessTracker hostLivenessTracker; private final NodeRepositoryProvisioner provisioner; private final Curator curator; @@ -74,7 +72,6 @@ public class NodeFailTester { curator = tester.getCurator(); nodeRepository = tester.nodeRepository(); provisioner = tester.provisioner(); - hostLivenessTracker = new TestHostLivenessTracker(clock); } private void initializeMaintainers(Map<ApplicationId, MockDeployer.ApplicationContext> apps) { @@ -112,7 +109,7 @@ public class NodeFailTester { /** Create hostCount hosts, one app with containerCount containers, and one app with contentCount content nodes. */ public static NodeFailTester withTwoApplications(int hostCount, int containerCount, int contentCount) { NodeFailTester tester = new NodeFailTester(); - tester.createHostNodes(hostCount); + tester.tester.makeReadyHosts(hostCount, new NodeResources(2, 8, 20, 10)); // Create tenant host application ClusterSpec clusterNodeAdminApp = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("node-admin")).vespaVersion("6.42").build(); @@ -139,13 +136,7 @@ public class NodeFailTester { public static NodeFailTester withTwoApplications(int numberOfHosts) { NodeFailTester tester = new NodeFailTester(); - - int nodesPerHost = 3; - List<Node> hosts = tester.createHostNodes(numberOfHosts); - for (int i = 0; i < hosts.size(); i++) { - tester.createReadyNodes(nodesPerHost, i * nodesPerHost, Optional.of("parent" + (i + 1)), - new NodeResources(1, 4, 100, 0.3), NodeType.tenant); - } + tester.tester.makeReadyNodes(numberOfHosts, new NodeResources(4, 16, 400, 10), NodeType.host, 8); // Create applications ClusterSpec clusterNodeAdminApp = ClusterSpec.request(ClusterSpec.Type.container, ClusterSpec.Id.from("node-admin")).vespaVersion("6.42").build(); @@ -230,26 +221,11 @@ public class NodeFailTester { } public NodeHealthTracker createUpdater() { - return new NodeHealthTracker(hostLivenessTracker, serviceMonitor, nodeRepository, Duration.ofMinutes(5), metric); - } - - public void allNodesMakeAConfigRequestExcept(Node ... deadNodeArray) { - allNodesMakeAConfigRequestExcept(List.of(deadNodeArray)); - } - - public void allNodesMakeAConfigRequestExcept(List<Node> deadNodes) { - for (Node node : nodeRepository.nodes().list()) { - if ( ! deadNodes.contains(node)) - hostLivenessTracker.receivedRequestFrom(node.hostname()); - } + return new NodeHealthTracker(serviceMonitor, nodeRepository, Duration.ofMinutes(5), metric); } public Clock clock() { return clock; } - public List<Node> createReadyNodes(int count) { - return createReadyNodes(count, 0); - } - public List<Node> createReadyNodes(int count, NodeResources resources) { return createReadyNodes(count, 0, resources); } @@ -258,22 +234,10 @@ public class NodeFailTester { return createReadyNodes(count, 0, Optional.empty(), hostFlavors.getFlavorOrThrow("default"), nodeType); } - public List<Node> createReadyNodes(int count, int startIndex) { - return createReadyNodes(count, startIndex, "default"); - } - - public List<Node> createReadyNodes(int count, int startIndex, String flavor) { - return createReadyNodes(count, startIndex, Optional.empty(), hostFlavors.getFlavorOrThrow(flavor), NodeType.tenant); - } - public List<Node> createReadyNodes(int count, int startIndex, NodeResources resources) { return createReadyNodes(count, startIndex, Optional.empty(), new Flavor(resources), NodeType.tenant); } - private List<Node> createReadyNodes(int count, int startIndex, Optional<String> parentHostname, NodeResources resources, NodeType nodeType) { - return createReadyNodes(count, startIndex, parentHostname, new Flavor(resources), nodeType); - } - private List<Node> createReadyNodes(int count, int startIndex, Optional<String> parentHostname, Flavor flavor, NodeType nodeType) { List<Node> nodes = new ArrayList<>(count); int lastOctetOfPoolAddress = 0; diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailerTest.java index 3ba536ee4d7..f3a4b5a9284 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailerTest.java @@ -13,7 +13,6 @@ import com.yahoo.vespa.hosted.provision.NodeList; import com.yahoo.vespa.hosted.provision.NodeRepository; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.vespa.hosted.provision.node.Report; -import com.yahoo.vespa.hosted.provision.node.Reports; import org.junit.Test; import java.time.Duration; @@ -21,6 +20,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; @@ -62,23 +62,21 @@ public class NodeFailerTest { } private void testNodeFailingWith(NodeFailTester tester, String hostWithHwFailure) { - // The host should have 2 nodes in active and 1 ready + // The host should have 2 nodes in active Map<Node.State, List<String>> hostnamesByState = tester.nodeRepository.nodes().list().childrenOf(hostWithHwFailure).asList().stream() .collect(Collectors.groupingBy(Node::state, Collectors.mapping(Node::hostname, Collectors.toList()))); assertEquals(2, hostnamesByState.get(Node.State.active).size()); - assertEquals(1, hostnamesByState.get(Node.State.ready).size()); // Suspend the first of the active nodes tester.suspend(hostnamesByState.get(Node.State.active).get(0)); tester.runMaintainers(); tester.clock.advance(Duration.ofHours(25)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); // The first (and the only) ready node and the 1st active node that was allowed to fail should be failed Map<Node.State, List<String>> expectedHostnamesByState1Iter = Map.of( - Node.State.failed, List.of(hostnamesByState.get(Node.State.ready).get(0), hostnamesByState.get(Node.State.active).get(0)), + Node.State.failed, List.of(hostnamesByState.get(Node.State.active).get(0)), Node.State.active, hostnamesByState.get(Node.State.active).subList(1, 2)); Map<Node.State, List<String>> hostnamesByState1Iter = tester.nodeRepository.nodes().list().childrenOf(hostWithHwFailure).asList().stream() .collect(Collectors.groupingBy(Node::state, Collectors.mapping(Node::hostname, Collectors.toList()))); @@ -88,7 +86,6 @@ public class NodeFailerTest { tester.suspend(hostnamesByState.get(Node.State.active).get(1)); tester.clock.advance(Duration.ofHours(25)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); // All of the children should be failed now @@ -101,7 +98,7 @@ public class NodeFailerTest { tester.suspend(hostWithHwFailure); tester.runMaintainers(); assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(hostWithHwFailure).get().state()); - assertEquals(4, tester.nodeRepository.nodes().list(Node.State.failed).size()); + assertEquals(3, tester.nodeRepository.nodes().list(Node.State.failed).size()); } @Test @@ -110,14 +107,12 @@ public class NodeFailerTest { String hostWithFailureReports = selectFirstParentHostWithNActiveNodesExcept(tester.nodeRepository, 2); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(hostWithFailureReports).get().state()); - // The host has 2 nodes in active and 1 ready + // The host has 2 nodes in active Map<Node.State, List<String>> hostnamesByState = tester.nodeRepository.nodes().list().childrenOf(hostWithFailureReports).asList().stream() .collect(Collectors.groupingBy(Node::state, Collectors.mapping(Node::hostname, Collectors.toList()))); assertEquals(2, hostnamesByState.get(Node.State.active).size()); String activeChild1 = hostnamesByState.get(Node.State.active).get(0); String activeChild2 = hostnamesByState.get(Node.State.active).get(1); - assertEquals(1, hostnamesByState.get(Node.State.ready).size()); - String readyChild = hostnamesByState.get(Node.State.ready).get(0); // Set failure report to the parent and all its children. Report badTotalMemorySizeReport = Report.basicReport("badTotalMemorySize", HARD_FAIL, Instant.now(), "too low"); @@ -128,20 +123,16 @@ public class NodeFailerTest { tester.nodeRepository.nodes().write(updatedNode, () -> {}); }); - // The ready node will be failed, but neither the host nor the 2 active nodes since they have not been suspended - tester.allNodesMakeAConfigRequestExcept(); + // Neither the host nor the 2 active nodes are failed out because they have not been suspended tester.runMaintainers(); - assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(readyChild).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(hostWithFailureReports).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(activeChild1).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(activeChild2).get().state()); - // Suspending the host will not fail any more since none of the children are suspened + // Suspending the host will not fail any more since none of the children are suspended tester.suspend(hostWithFailureReports); tester.clock.advance(Duration.ofHours(25)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); - assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(readyChild).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(hostWithFailureReports).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(activeChild1).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(activeChild2).get().state()); @@ -149,9 +140,7 @@ public class NodeFailerTest { // Suspending one child node will fail that out. tester.suspend(activeChild1); tester.clock.advance(Duration.ofHours(25)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); - assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(readyChild).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(hostWithFailureReports).get().state()); assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(activeChild1).get().state()); assertEquals(Node.State.active, tester.nodeRepository.nodes().node(activeChild2).get().state()); @@ -159,9 +148,8 @@ public class NodeFailerTest { // Suspending the second child node will fail that out and the host. tester.suspend(activeChild2); tester.clock.advance(Duration.ofHours(25)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); - assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(readyChild).get().state()); + tester.runMaintainers(); // hosts are typically failed in the 2. maintain() assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(hostWithFailureReports).get().state()); assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(activeChild1).get().state()); assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(activeChild2).get().state()); @@ -210,31 +198,18 @@ public class NodeFailerTest { @Test public void node_failing() { - NodeFailTester tester = NodeFailTester.withTwoApplications(); + NodeFailTester tester = NodeFailTester.withTwoApplications(6); // For a day all nodes work so nothing happens for (int minutes = 0; minutes < 24 * 60; minutes +=5 ) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); - assertEquals( 0, tester.deployer.redeployments); - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals( 0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); - assertEquals( 4, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); + assertEquals(0, tester.deployer.redeployments); + assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); + assertEquals(0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); } - // Hardware failures are detected on two ready nodes, which are then failed - Node readyFail1 = tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).asList().get(2); - Node readyFail2 = tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).asList().get(3); - tester.nodeRepository.nodes().write(readyFail1.with(new Reports().withReport(badTotalMemorySizeReport)), () -> {}); - tester.nodeRepository.nodes().write(readyFail2.with(new Reports().withReport(badTotalMemorySizeReport)), () -> {}); - assertEquals(4, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); - tester.runMaintainers(); - assertEquals(2, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); - assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(readyFail1.hostname()).get().state()); - assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(readyFail2.hostname()).get().state()); - String downHost1 = tester.nodeRepository.nodes().list(Node.State.active).owner(NodeFailTester.app1).asList().get(1).hostname(); String downHost2 = tester.nodeRepository.nodes().list(Node.State.active).owner(NodeFailTester.app2).asList().get(3).hostname(); tester.serviceMonitor.setHostDown(downHost1); @@ -243,41 +218,34 @@ public class NodeFailerTest { for (int minutes = 0; minutes < 45; minutes +=5 ) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); - assertEquals( 0, tester.deployer.redeployments); - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals( 2, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); - assertEquals( 2, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); + assertEquals(0, tester.deployer.redeployments); + assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); + assertEquals(0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); } tester.serviceMonitor.setHostUp(downHost1); // downHost2 should now be failed and replaced, but not downHost1 tester.clock.advance(Duration.ofDays(1)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); - assertEquals( 1, tester.deployer.redeployments); - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals( 3, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); - assertEquals( 1, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); + assertEquals(1, tester.deployer.redeployments); + assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); + assertEquals(1, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(downHost2, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).asList().get(0).hostname()); // downHost1 fails again tester.serviceMonitor.setHostDown(downHost1); tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); // the system goes down tester.clock.advance(Duration.ofMinutes(120)); tester.failer = tester.createFailer(); tester.runMaintainers(); // the host is still down and fails tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); - assertEquals( 2, tester.deployer.redeployments); - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals( 4, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); - assertEquals( 0, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); + assertEquals(2, tester.deployer.redeployments); + assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); + assertEquals(2, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); // the last host goes down Node lastNode = tester.highestIndex(tester.nodeRepository.nodes().list(Node.State.active).owner(NodeFailTester.app1)); @@ -286,23 +254,19 @@ public class NodeFailerTest { for (int minutes = 0; minutes < 75; minutes +=5 ) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); - assertEquals( 2, tester.deployer.redeployments); - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals( 4, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); - assertEquals( 0, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); + assertEquals(2, tester.deployer.redeployments); + assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); + assertEquals(2, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); } // A new node is available tester.createReadyNodes(1, 16, NodeFailTester.nodeResources); tester.clock.advance(Duration.ofDays(1)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); // The node is now failed - assertEquals( 3, tester.deployer.redeployments); - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals( 5, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); - assertEquals( 0, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); + assertEquals(3, tester.deployer.redeployments); + assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); + assertEquals(3, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertTrue("The index of the last failed node is not reused", tester.highestIndex(tester.nodeRepository.nodes().list(Node.State.active).owner(NodeFailTester.app1)).allocation().get().membership().index() > @@ -319,12 +283,10 @@ public class NodeFailerTest { String downNode = tester.nodeRepository.nodes().list(Node.State.active).owner(NodeFailTester.app1).asList().get(1).hostname(); tester.serviceMonitor.setHostDown(downNode); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); tester.clock.advance(Duration.ofMinutes(75)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(1, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(downNode).get().state()); @@ -332,12 +294,10 @@ public class NodeFailerTest { // Re-activate the node. It is still down, but should not be failed out until the grace period has passed again tester.nodeRepository.nodes().reactivate(downNode, Agent.system, getClass().getSimpleName()); tester.clock.advance(Duration.ofMinutes(30)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); tester.clock.advance(Duration.ofMinutes(45)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(1, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(Node.State.failed, tester.nodeRepository.nodes().node(downNode).get().state()); @@ -360,7 +320,6 @@ public class NodeFailerTest { for (int minutes = 0; minutes < 45; minutes +=5 ) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); assertEquals(0, tester.deployer.redeployments); assertEquals(3, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); assertEquals(0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); @@ -368,7 +327,6 @@ public class NodeFailerTest { // downHost should now be failed and replaced tester.clock.advance(Duration.ofDays(1)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(1, tester.deployer.redeployments); assertEquals(1, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); @@ -403,37 +361,15 @@ public class NodeFailerTest { } @Test - public void host_not_failed_without_config_requests() { - NodeFailTester tester = NodeFailTester.withTwoApplications(); - - // For a day all nodes work so nothing happens - for (int minutes = 0, interval = 30; minutes < 24 * 60; minutes += interval) { - tester.clock.advance(Duration.ofMinutes(interval)); - tester.allNodesMakeAConfigRequestExcept(); - tester.runMaintainers(); - assertEquals( 3, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.host).size()); - assertEquals( 0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.host).size()); - } - - tester.clock.advance(Duration.ofMinutes(180)); - Node host = tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.host).first().get(); - tester.allNodesMakeAConfigRequestExcept(host); - tester.runMaintainers(); - assertEquals( 3, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.host).size()); - assertEquals( 0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.host).size()); - } - - @Test public void failing_hosts() { NodeFailTester tester = NodeFailTester.withTwoApplications(7); // For a day all nodes work so nothing happens for (int minutes = 0, interval = 30; minutes < 24 * 60; minutes += interval) { tester.clock.advance(Duration.ofMinutes(interval)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals(13, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); + assertEquals(0, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); assertEquals(7, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.host).size()); } @@ -446,21 +382,17 @@ public class NodeFailerTest { for (int minutes = 0; minutes < 45; minutes += 5 ) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); assertEquals(0, tester.deployer.redeployments); assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals(13, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); assertEquals(7, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.host).size()); } tester.clock.advance(Duration.ofMinutes(30)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(2, tester.deployer.redeployments); - assertEquals(3, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); + assertEquals(2, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals(10, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); assertEquals(7, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.host).size()); assertEquals(0, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.host).size()); @@ -468,9 +400,8 @@ public class NodeFailerTest { tester.runMaintainers(); assertEquals(2 + 1, tester.deployer.redeployments); - assertEquals(3, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); + assertEquals(2, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals(10, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); assertEquals(6, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.host).size()); assertEquals(1, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.host).size()); @@ -483,18 +414,15 @@ public class NodeFailerTest { for (int minutes = 0, interval = 30; minutes < 24 * 60; minutes += interval) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(interval)); - tester.allNodesMakeAConfigRequestExcept(); - assertEquals(3 + 1, tester.nodeRepository.nodes().list(Node.State.failed).size()); + assertEquals(2 + 1, tester.nodeRepository.nodes().list(Node.State.failed).size()); } tester.clock.advance(Duration.ofMinutes(30)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(3 + 1, tester.deployer.redeployments); - assertEquals(4, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); + assertEquals(3, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals(9, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); assertEquals(6, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.host).size()); @@ -503,14 +431,12 @@ public class NodeFailerTest { tester.serviceMonitor.setHostDown(downHost2); tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(90)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); tester.runMaintainers(); // The host is failed in the 2. maintain() assertEquals(5 + 2, tester.deployer.redeployments); - assertEquals(7, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); + assertEquals(5, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals(6, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); assertEquals(5, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.host).size()); @@ -520,13 +446,11 @@ public class NodeFailerTest { tester.serviceMonitor.setHostDown(downHost3); tester.runMaintainers(); tester.clock.advance(Duration.ofDays(1)); - tester.allNodesMakeAConfigRequestExcept(); tester.runMaintainers(); assertEquals(6 + 2, tester.deployer.redeployments); - assertEquals(9, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); + assertEquals(6, tester.nodeRepository.nodes().list(Node.State.failed).nodeType(NodeType.tenant).size()); assertEquals(8, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.tenant).size()); - assertEquals(4, tester.nodeRepository.nodes().list(Node.State.ready).nodeType(NodeType.tenant).size()); assertEquals(5, tester.nodeRepository.nodes().list(Node.State.active).nodeType(NodeType.host).size()); } @@ -547,7 +471,6 @@ public class NodeFailerTest { for (int minutes = 0; minutes < 24 * 60; minutes +=5 ) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); assertEquals(count, tester.nodeRepository.nodes().list(Node.State.active).nodeType(nodeType).size()); } @@ -560,7 +483,6 @@ public class NodeFailerTest { for (int minutes = 0; minutes < 45; minutes +=5 ) { tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(5)); - tester.allNodesMakeAConfigRequestExcept(); assertEquals( 0, tester.deployer.redeployments); assertEquals(count, tester.nodeRepository.nodes().list(Node.State.active).nodeType(nodeType).size()); } @@ -582,38 +504,30 @@ public class NodeFailerTest { } @Test - public void failing_divergent_ready_nodes() { - NodeFailTester tester = NodeFailTester.withNoApplications(); - - Node readyNode = tester.createReadyNodes(1).get(0); - - tester.runMaintainers(); - assertEquals(Node.State.ready, readyNode.state()); - - tester.nodeRepository.nodes().write(readyNode.with(new Reports().withReport(badTotalMemorySizeReport)), () -> {}); - - tester.runMaintainers(); - assertEquals(1, tester.nodeRepository.nodes().list(Node.State.failed).size()); - } - - @Test public void node_failing_throttle() { // Throttles based on an absolute number in small zone { - // 10 hosts with 3 tenant nodes each, total 40 nodes - NodeFailTester tester = NodeFailTester.withTwoApplications(10); - NodeList hosts = tester.nodeRepository.nodes().list().nodeType(NodeType.host); + // 10 hosts with 7 container and 7 content nodes, total 24 nodes + NodeFailTester tester = NodeFailTester.withTwoApplications(10, 7, 7); + + List<String> failedHostHostnames = tester.nodeRepository.nodes().list().stream() + .flatMap(node -> node.parentHostname().stream()) + .collect(Collectors.groupingBy(h -> h, Collectors.counting())) + .entrySet().stream() + .sorted(Comparator.comparingLong((Map.Entry<String, Long> e) -> e.getValue()).reversed()) + .limit(3) + .map(Map.Entry::getKey) + .toList(); // 3 hosts fail. 2 of them and all of their children are allowed to fail - List<Node> failedHosts = hosts.asList().subList(0, 3); - failedHosts.forEach(host -> tester.serviceMonitor.setHostDown(host.hostname())); + failedHostHostnames.forEach(hostname -> tester.serviceMonitor.setHostDown(hostname)); tester.runMaintainers(); tester.clock.advance(Duration.ofMinutes(61)); tester.runMaintainers(); tester.runMaintainers(); // hosts are typically failed in the 2. maintain() assertEquals(2 + /* hosts */ - (2 * 3) /* containers per host */, + (2 * 2) /* containers per host */, tester.nodeRepository.nodes().list(Node.State.failed).size()); assertEquals("Throttling is indicated by the metric", 1, tester.metric.values.get(NodeFailer.throttlingActiveMetric)); assertEquals("Throttled host failures", 1, tester.metric.values.get(NodeFailer.throttledHostFailuresMetric)); @@ -623,7 +537,7 @@ public class NodeFailerTest { tester.clock.advance(Duration.ofMinutes(interval)); } tester.runMaintainers(); - assertEquals(8, tester.nodeRepository.nodes().list(Node.State.failed).size()); + assertEquals(6, tester.nodeRepository.nodes().list(Node.State.failed).size()); assertEquals("Throttling is indicated by the metric", 1, tester.metric.values.get(NodeFailer.throttlingActiveMetric)); assertEquals("Throttled host failures", 1, tester.metric.values.get(NodeFailer.throttledHostFailuresMetric)); @@ -631,14 +545,14 @@ public class NodeFailerTest { tester.clock.advance(Duration.ofMinutes(30)); tester.runMaintainers(); tester.runMaintainers(); // hosts are failed in the 2. maintain() - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.failed).size()); + assertEquals(9, tester.nodeRepository.nodes().list(Node.State.failed).size()); assertEquals("Throttling is not indicated by the metric, as no throttled attempt is made", 0, tester.metric.values.get(NodeFailer.throttlingActiveMetric)); assertEquals("No throttled node failures", 0, tester.metric.values.get(NodeFailer.throttledNodeFailuresMetric)); // Nothing else to fail tester.clock.advance(Duration.ofHours(25)); tester.runMaintainers(); - assertEquals(12, tester.nodeRepository.nodes().list(Node.State.failed).size()); + assertEquals(9, tester.nodeRepository.nodes().list(Node.State.failed).size()); assertEquals("Throttling is not indicated by the metric", 0, tester.metric.values.get(NodeFailer.throttlingActiveMetric)); assertEquals("No throttled node failures", 0, tester.metric.values.get(NodeFailer.throttledNodeFailuresMetric)); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java index 4d75b8a5acc..adcd40866d0 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/os/OsVersionsTest.java @@ -3,6 +3,8 @@ package com.yahoo.vespa.hosted.provision.os; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Cloud; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.HostSpec; import com.yahoo.config.provision.NodeResources; @@ -96,7 +98,7 @@ public class OsVersionsTest { public void max_active_upgrades() { int totalNodes = 20; int maxActiveUpgrades = 5; - var versions = new OsVersions(tester.nodeRepository(), false, maxActiveUpgrades); + var versions = new OsVersions(tester.nodeRepository(), Cloud.defaultCloud(), maxActiveUpgrades); provisionInfraApplication(totalNodes); Supplier<NodeList> hostNodes = () -> tester.nodeRepository().nodes().list().state(Node.State.active).hosts(); @@ -161,7 +163,7 @@ public class OsVersionsTest { @Test public void upgrade_by_retiring() { - var versions = new OsVersions(tester.nodeRepository(), true, Integer.MAX_VALUE); + var versions = new OsVersions(tester.nodeRepository(), Cloud.builder().dynamicProvisioning(true).build(), Integer.MAX_VALUE); var clock = (ManualClock) tester.nodeRepository().clock(); int hostCount = 10; // Provision hosts and children @@ -229,7 +231,7 @@ public class OsVersionsTest { @Test public void upgrade_by_retiring_everything_at_once() { - var versions = new OsVersions(tester.nodeRepository(), true, Integer.MAX_VALUE); + var versions = new OsVersions(tester.nodeRepository(), Cloud.builder().dynamicProvisioning(true).build(), Integer.MAX_VALUE); int hostCount = 3; provisionInfraApplication(hostCount, infraApplication, NodeType.confighost); Supplier<NodeList> hostNodes = () -> tester.nodeRepository().nodes().list() @@ -254,7 +256,7 @@ public class OsVersionsTest { @Test public void upgrade_by_rebuilding() { tester.flagSource().withIntFlag(PermanentFlags.MAX_REBUILDS.id(), 1); - var versions = new OsVersions(tester.nodeRepository(), false, Integer.MAX_VALUE); + var versions = new OsVersions(tester.nodeRepository(), Cloud.defaultCloud(), Integer.MAX_VALUE); int hostCount = 10; provisionInfraApplication(hostCount + 1); Supplier<NodeList> hostNodes = () -> tester.nodeRepository().nodes().list().nodeType(NodeType.host); @@ -333,7 +335,7 @@ public class OsVersionsTest { tester.flagSource().withIntFlag(PermanentFlags.MAX_REBUILDS.id(), maxRebuilds); tester.flagSource().withBooleanFlag(Flags.SOFT_REBUILD.id(), softRebuild); - var versions = new OsVersions(tester.nodeRepository(), true, Integer.MAX_VALUE); + var versions = new OsVersions(tester.nodeRepository(), Cloud.builder().dynamicProvisioning(true).name(CloudName.AWS).build(), Integer.MAX_VALUE); provisionInfraApplication(hostCount, infraApplication, NodeType.host, NodeResources.StorageType.remote); Supplier<NodeList> hostNodes = () -> tester.nodeRepository().nodes().list().nodeType(NodeType.host); @@ -378,7 +380,7 @@ public class OsVersionsTest { @Test public void upgrade_by_rebuilding_multiple_host_types() { tester.flagSource().withIntFlag(PermanentFlags.MAX_REBUILDS.id(), 1); - var versions = new OsVersions(tester.nodeRepository(), false, Integer.MAX_VALUE); + var versions = new OsVersions(tester.nodeRepository(), Cloud.defaultCloud(), Integer.MAX_VALUE); int hostCount = 3; provisionInfraApplication(hostCount, infraApplication, NodeType.host); provisionInfraApplication(hostCount, ApplicationId.from("hosted-vespa", "confighost", "default"), NodeType.confighost); @@ -411,7 +413,7 @@ public class OsVersionsTest { @Test public void upgrade_by_rebuilding_is_limited_by_stateful_clusters() { tester.flagSource().withIntFlag(PermanentFlags.MAX_REBUILDS.id(), 3); - var versions = new OsVersions(tester.nodeRepository(), false, Integer.MAX_VALUE); + var versions = new OsVersions(tester.nodeRepository(), Cloud.defaultCloud(), Integer.MAX_VALUE); int hostCount = 5; ApplicationId app1 = ApplicationId.from("t1", "a1", "i1"); ApplicationId app2 = ApplicationId.from("t2", "a2", "i2"); @@ -489,7 +491,7 @@ public class OsVersionsTest { public void upgrade_by_rebuilding_limits_infrastructure_host() { int hostCount = 3; tester.flagSource().withIntFlag(PermanentFlags.MAX_REBUILDS.id(), hostCount); - var versions = new OsVersions(tester.nodeRepository(), false, Integer.MAX_VALUE); + var versions = new OsVersions(tester.nodeRepository(), Cloud.defaultCloud(), Integer.MAX_VALUE); provisionInfraApplication(hostCount, infraApplication, NodeType.proxyhost); Supplier<NodeList> hosts = () -> tester.nodeRepository().nodes().list().nodeType(NodeType.proxyhost); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java index 03ab18c15d9..faf3f60e8af 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java @@ -74,6 +74,8 @@ import static org.junit.Assert.assertTrue; */ public class ProvisioningTester { + public static final ApplicationId tenantHostApp = ApplicationId.from("hosted-vespa", "tenant-host", "default"); + private final Curator curator; private final NodeFlavors nodeFlavors; private final ManualClock clock; @@ -543,7 +545,7 @@ public class ProvisioningTester { } public void activateTenantHosts() { - prepareAndActivateInfraApplication(applicationId(), NodeType.host); + prepareAndActivateInfraApplication(tenantHostApp, NodeType.host); } public static ClusterSpec containerClusterSpec() { diff --git a/parent/pom.xml b/parent/pom.xml index f9155acbfd5..e6db5ff3492 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -275,7 +275,7 @@ <goal>run</goal> </goals> <configuration> - <protocVersion>3.21.2</protocVersion> + <protocVersion>${protobuf.version}</protocVersion> <addSources>main</addSources> <outputDirectory>${project.build.directory}/generated-sources/protobuf</outputDirectory> <inputDirectories> @@ -400,7 +400,7 @@ <failOnError>${javadoc.failOnError}</failOnError> <quiet>true</quiet> <show>protected</show> - <header><a href="https://docs.vespa.ai"><img src="https://docs.vespa.ai/img/vespa-logo.png" width="100" height="28" style="padding-top:7px"/></a></header> + <header><a href="https://docs.vespa.ai"><img src="https://docs.vespa.ai/assets/logos/vespa-logo-full-white.svg" width="100" height="28" style="padding-top:7px"/></a></header> </configuration> </plugin> </plugins> @@ -791,12 +791,17 @@ </dependency> <dependency> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>bcpkix-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> - <artifactId>bcprov-jdk15on</artifactId> + <artifactId>bcprov-jdk18on</artifactId> + <version>${bouncycastle.version}</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcutil-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <dependency> @@ -1024,7 +1029,7 @@ find zkfacade/src/main/java/org/apache/curator -name package-info.java | \ xargs perl -pi -e 's/major = [0-9]+, minor = [0-9]+, micro = [0-9]+/major = 5, minor = 3, micro = 0/g' --> - <bouncycastle.version>1.68</bouncycastle.version> + <bouncycastle.version>1.72</bouncycastle.version> <curator.version>5.3.0</curator.version> <commons.codec.version>1.15</commons.codec.version> <commons.math3.version>3.6.1</commons.math3.version> diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java index 19f6df2bf97..6317d1256f1 100644 --- a/predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java +++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/BooleanPredicate.java @@ -36,10 +36,9 @@ public class BooleanPredicate extends PredicateValue { if (obj == this) { return true; } - if (!(obj instanceof BooleanPredicate)) { + if (!(obj instanceof BooleanPredicate rhs)) { return false; } - BooleanPredicate rhs = (BooleanPredicate)obj; if (value != rhs.value) { return false; } diff --git a/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java index 496e84fd4a5..15b6d5d3d62 100644 --- a/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java +++ b/predicate-search-core/src/main/java/com/yahoo/document/predicate/FeatureRange.java @@ -13,8 +13,8 @@ public class FeatureRange extends PredicateValue { private String key; private Long from; private Long to; - private List<RangePartition> partitions; - private List<RangeEdgePartition> edgePartitions; + private final List<RangePartition> partitions; + private final List<RangeEdgePartition> edgePartitions; public FeatureRange(String key) { this(key, null, null); @@ -98,10 +98,9 @@ public class FeatureRange extends PredicateValue { if (obj == this) { return true; } - if (!(obj instanceof FeatureRange)) { + if (!(obj instanceof FeatureRange rhs)) { return false; } - FeatureRange rhs = (FeatureRange)obj; if (!key.equals(rhs.key)) { return false; } diff --git a/searchcore/src/tests/proton/attribute/attribute_manager/attribute_manager_test.cpp b/searchcore/src/tests/proton/attribute/attribute_manager/attribute_manager_test.cpp index 95f0cff088d..0e3445d0785 100644 --- a/searchcore/src/tests/proton/attribute/attribute_manager/attribute_manager_test.cpp +++ b/searchcore/src/tests/proton/attribute/attribute_manager/attribute_manager_test.cpp @@ -43,8 +43,6 @@ #include <vespa/log/log.h> LOG_SETUP("attribute_manager_test"); -namespace vespa { namespace config { namespace search {}}} - using std::string; using namespace vespa::config::search; using namespace config; @@ -258,7 +256,7 @@ ParallelAttributeManager::ParallelAttributeManager(search::SerialNum configSeria masterExecutor(1, 128_Ki), master(masterExecutor), initializer(std::make_shared<AttributeManagerInitializer>(configSerialNum, documentMetaStoreInitTask, - documentMetaStore, baseAttrMgr, attrCfg, + documentMetaStore, *baseAttrMgr, attrCfg, alloc_strategy, fastAccessAttributesOnly, master, mgr)) { diff --git a/searchcore/src/tests/proton/attribute/attribute_populator/attribute_populator_test.cpp b/searchcore/src/tests/proton/attribute/attribute_populator/attribute_populator_test.cpp index c66b2dd15dc..19b8348fb7a 100644 --- a/searchcore/src/tests/proton/attribute/attribute_populator/attribute_populator_test.cpp +++ b/searchcore/src/tests/proton/attribute/attribute_populator/attribute_populator_test.cpp @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include <vespa/searchcore/proton/attribute/attribute_populator.h> +#include <vespa/document/repo/documenttyperepo.h> #include <vespa/searchcore/proton/attribute/attributemanager.h> #include <vespa/searchcore/proton/common/hw_info.h> #include <vespa/searchcore/proton/test/test.h> diff --git a/searchcore/src/tests/proton/attribute/attribute_test.cpp b/searchcore/src/tests/proton/attribute/attribute_test.cpp index ea264c506a6..a6e9f8fe7ee 100644 --- a/searchcore/src/tests/proton/attribute/attribute_test.cpp +++ b/searchcore/src/tests/proton/attribute/attribute_test.cpp @@ -18,12 +18,12 @@ #include <vespa/searchlib/attribute/interlock.h> #include <vespa/searchlib/attribute/predicate_attribute.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> -#include <vespa/searchlib/index/empty_doc_builder.h> #include <vespa/searchlib/predicate/predicate_hash.h> #include <vespa/searchlib/predicate/predicate_index.h> #include <vespa/searchlib/tensor/dense_tensor_attribute.h> #include <vespa/searchlib/tensor/tensor_attribute.h> #include <vespa/searchlib/test/directory_handler.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/searchcommon/attribute/attributecontent.h> #include <vespa/searchcommon/attribute/iattributevector.h> #include <vespa/searchcommon/attribute/config.h> @@ -82,13 +82,13 @@ using search::attribute::ImportedAttributeVector; using search::attribute::ImportedAttributeVectorFactory; using search::attribute::ReferenceAttribute; using search::index::DummyFileHeaderContext; -using search::index::EmptyDocBuilder; using search::predicate::PredicateHash; using search::predicate::PredicateIndex; using search::tensor::DenseTensorAttribute; using search::tensor::PrepareResult; using search::tensor::TensorAttribute; using search::test::DirectoryHandler; +using search::test::DocBuilder; using std::string; using vespalib::ForegroundTaskExecutor; using vespalib::ForegroundThreadExecutor; @@ -221,12 +221,12 @@ AttributeWriterTest::~AttributeWriterTest() = default; TEST_F(AttributeWriterTest, handles_put) { - EmptyDocBuilder edb([](auto& header) - { using namespace document::config_builder; - header.addField("a1", DataType::T_INT) - .addField("a2", Array(DataType::T_INT)) - .addField("a3", DataType::T_FLOAT) - .addField("a4", DataType::T_STRING); }); + DocBuilder db([](auto& header) + { using namespace document::config_builder; + header.addField("a1", DataType::T_INT) + .addField("a2", Array(DataType::T_INT)) + .addField("a3", DataType::T_FLOAT) + .addField("a4", DataType::T_STRING); }); auto a1 = addAttribute("a1"); auto a2 = addAttribute({"a2", AVConfig(AVBasicType::INT32, AVCollectionType::ARRAY)}); auto a3 = addAttribute({"a3", AVConfig(AVBasicType::FLOAT)}); @@ -238,7 +238,7 @@ TEST_F(AttributeWriterTest, handles_put) attribute::ConstCharContent sbuf; { // empty document should give default values EXPECT_EQ(1u, a1->getNumDocs()); - put(1, *edb.make_document("id:ns:searchdocument::1"), 1); + put(1, *db.make_document("id:ns:searchdocument::1"), 1); EXPECT_EQ(2u, a1->getNumDocs()); EXPECT_EQ(2u, a2->getNumDocs()); EXPECT_EQ(2u, a3->getNumDocs()); @@ -260,9 +260,9 @@ TEST_F(AttributeWriterTest, handles_put) EXPECT_EQ(strcmp("", sbuf[0]), 0); } { // document with single value & multi value attribute - auto doc = edb.make_document("id:ns:searchdocument::2"); + auto doc = db.make_document("id:ns:searchdocument::2"); doc->setValue("a1", IntFieldValue(10)); - ArrayFieldValue int_array(edb.get_data_type("Array<Int>")); + auto int_array = db.make_array("a2"); int_array.add(IntFieldValue(20)); int_array.add(IntFieldValue(30)); doc->setValue("a2",int_array); @@ -282,9 +282,9 @@ TEST_F(AttributeWriterTest, handles_put) EXPECT_EQ(30u, ibuf[1]); } { // replace existing document - auto doc = edb.make_document("id:ns:searchdocument::2"); + auto doc = db.make_document("id:ns:searchdocument::2"); doc->setValue("a1", IntFieldValue(100)); - ArrayFieldValue int_array(edb.get_data_type("Array<Int>")); + auto int_array = db.make_array("a2"); int_array.add(IntFieldValue(200)); int_array.add(IntFieldValue(300)); int_array.add(IntFieldValue(400)); @@ -309,7 +309,7 @@ TEST_F(AttributeWriterTest, handles_put) TEST_F(AttributeWriterTest, handles_predicate_put) { - EmptyDocBuilder edb([](auto& header) { header.addField("a1", DataType::T_PREDICATE); }); + DocBuilder db([](auto& header) { header.addField("a1", DataType::T_PREDICATE); }); auto a1 = addAttribute({"a1", AVConfig(AVBasicType::PREDICATE)}); allocAttributeWriter(); @@ -317,14 +317,14 @@ TEST_F(AttributeWriterTest, handles_predicate_put) // empty document should give default values EXPECT_EQ(1u, a1->getNumDocs()); - put(1, *edb.make_document("id:ns:searchdocument::1"), 1); + put(1, *db.make_document("id:ns:searchdocument::1"), 1); EXPECT_EQ(2u, a1->getNumDocs()); EXPECT_EQ(1u, a1->getStatus().getLastSyncToken()); EXPECT_EQ(0u, index.getZeroConstraintDocs().size()); // document with single value attribute PredicateSlimeBuilder builder; - auto doc = edb.make_document("id:ns:searchdocument::2"); + auto doc = db.make_document("id:ns:searchdocument::2"); doc->setValue("a1", PredicateFieldValue(builder.true_predicate().build())); put(2, *doc, 2); EXPECT_EQ(3u, a1->getNumDocs()); @@ -335,7 +335,7 @@ TEST_F(AttributeWriterTest, handles_predicate_put) EXPECT_FALSE(it.valid()); // replace existing document - doc = edb.make_document("id:ns:searchdocument::2"); + doc = db.make_document("id:ns:searchdocument::2"); doc->setValue("a1", PredicateFieldValue(builder.feature("foo").value("bar").build())); put(3, *doc, 2); EXPECT_EQ(3u, a1->getNumDocs()); @@ -407,10 +407,10 @@ TEST_F(AttributeWriterTest, visibility_delay_is_honoured) auto a1 = addAttribute({"a1", AVConfig(AVBasicType::STRING)}); allocAttributeWriter(); - EmptyDocBuilder edb([](auto& header) { header.addField("a1", DataType::T_STRING); }); + DocBuilder db([](auto& header) { header.addField("a1", DataType::T_STRING); }); EXPECT_EQ(1u, a1->getNumDocs()); EXPECT_EQ(0u, a1->getStatus().getLastSyncToken()); - auto doc = edb.make_document("id:ns:searchdocument::1"); + auto doc = db.make_document("id:ns:searchdocument::1"); doc->setValue("a1", StringFieldValue("10")); put(3, *doc, 1); EXPECT_EQ(2u, a1->getNumDocs()); @@ -433,13 +433,13 @@ TEST_F(AttributeWriterTest, visibility_delay_is_honoured) EXPECT_EQ(8u, a1->getStatus().getLastSyncToken()); verifyAttributeContent(*a1, 2, "10"); - doc = edb.make_document("id:ns:searchdocument::1"); + doc = db.make_document("id:ns:searchdocument::1"); doc->setValue("a1", StringFieldValue("11")); awDelayed.put(9, *doc, 2, emptyCallback); - doc = edb.make_document("id:ns:searchdocument::1"); + doc = db.make_document("id:ns:searchdocument::1"); doc->setValue("a1", StringFieldValue("20")); awDelayed.put(10, *doc, 2, emptyCallback); - doc = edb.make_document("id:ns:searchdocument::1"); + doc = db.make_document("id:ns:searchdocument::1"); doc->setValue("a1", StringFieldValue("30")); awDelayed.put(11, *doc, 2, emptyCallback); EXPECT_EQ(8u, a1->getStatus().getLastSyncToken()); @@ -454,10 +454,10 @@ TEST_F(AttributeWriterTest, handles_predicate_remove) auto a1 = addAttribute({"a1", AVConfig(AVBasicType::PREDICATE)}); allocAttributeWriter(); - EmptyDocBuilder edb([](auto& header) { header.addField("a1", DataType::T_PREDICATE); }); + DocBuilder db([](auto& header) { header.addField("a1", DataType::T_PREDICATE); }); PredicateSlimeBuilder builder; - auto doc = edb.make_document("id:ns:searchdocument::1"); + auto doc = db.make_document("id:ns:searchdocument::1"); doc->setValue("a1", PredicateFieldValue(builder.true_predicate().build())); put(1, *doc, 1); EXPECT_EQ(2u, a1->getNumDocs()); @@ -477,10 +477,10 @@ TEST_F(AttributeWriterTest, handles_update) fillAttribute(a1, 1, 10, 1); fillAttribute(a2, 1, 20, 1); - EmptyDocBuilder edb([](auto& header) + DocBuilder db([](auto& header) { header.addField("a1", DataType::T_INT) .addField("a2", DataType::T_INT); }); - DocumentUpdate upd(edb.get_repo(), edb.get_document_type(), DocumentId("id:ns:searchdocument::1")); + DocumentUpdate upd(db.get_repo(), db.get_document_type(), DocumentId("id:ns:searchdocument::1")); upd.addUpdate(FieldUpdate(upd.getType().getField("a1")) .addUpdate(std::make_unique<ArithmeticValueUpdate>(ArithmeticValueUpdate::Add, 5))); upd.addUpdate(FieldUpdate(upd.getType().getField("a2")) @@ -511,14 +511,14 @@ TEST_F(AttributeWriterTest, handles_predicate_update) { auto a1 = addAttribute({"a1", AVConfig(AVBasicType::PREDICATE)}); allocAttributeWriter(); - EmptyDocBuilder edb([](auto& header) { header.addField("a1", DataType::T_PREDICATE); }); + DocBuilder db([](auto& header) { header.addField("a1", DataType::T_PREDICATE); }); PredicateSlimeBuilder builder; - auto doc = edb.make_document("id:ns:searchdocument::1"); + auto doc = db.make_document("id:ns:searchdocument::1"); doc->setValue("a1", PredicateFieldValue(builder.true_predicate().build())); put(1, *doc, 1); EXPECT_EQ(2u, a1->getNumDocs()); - DocumentUpdate upd(edb.get_repo(), edb.get_document_type(), DocumentId("id:ns:searchdocument::1")); + DocumentUpdate upd(db.get_repo(), db.get_document_type(), DocumentId("id:ns:searchdocument::1")); upd.addUpdate(FieldUpdate(upd.getType().getField("a1")) .addUpdate(std::make_unique<AssignValueUpdate>(std::make_unique<PredicateFieldValue>(builder.feature("foo").value("bar").build())))); @@ -662,7 +662,7 @@ createTensorAttribute(AttributeWriterTest &t) { } Document::UP -createTensorPutDoc(EmptyDocBuilder& builder, const Value &tensor) { +createTensorPutDoc(DocBuilder& builder, const Value &tensor) { auto doc = builder.make_document("id:ns:searchdocument::1"); TensorFieldValue fv(*doc->getField("a1").getDataType().cast_tensor()); fv = SimpleValue::from_value(tensor); @@ -676,7 +676,7 @@ TEST_F(AttributeWriterTest, can_write_to_tensor_attribute) { auto a1 = createTensorAttribute(*this); allocAttributeWriter(); - EmptyDocBuilder builder([](auto& header) { header.addTensorField("a1", sparse_tensor); }); + DocBuilder builder([](auto& header) { header.addTensorField("a1", sparse_tensor); }); auto tensor = make_tensor(TensorSpec(sparse_tensor) .add({{"x", "4"}, {"y", "5"}}, 7)); Document::UP doc = createTensorPutDoc(builder, *tensor); @@ -693,7 +693,7 @@ TEST_F(AttributeWriterTest, handles_tensor_assign_update) { auto a1 = createTensorAttribute(*this); allocAttributeWriter(); - EmptyDocBuilder builder([](auto& header) { header.addTensorField("a1", sparse_tensor); }); + DocBuilder builder([](auto& header) { header.addTensorField("a1", sparse_tensor); }); auto tensor = make_tensor(TensorSpec(sparse_tensor) .add({{"x", "6"}, {"y", "7"}}, 9)); auto doc = createTensorPutDoc(builder, *tensor); @@ -746,10 +746,10 @@ putAttributes(AttributeWriterTest &t, std::vector<uint32_t> expExecuteHistory) vespalib::string a2_name = "a2x"; vespalib::string a3_name = "a3y"; - EmptyDocBuilder edb([&](auto& header) - { header.addField(a1_name, DataType::T_INT) - .addField(a2_name, DataType::T_INT) - .addField(a3_name, DataType::T_INT); }); + DocBuilder db([&](auto& header) + { header.addField(a1_name, DataType::T_INT) + .addField(a2_name, DataType::T_INT) + .addField(a3_name, DataType::T_INT); }); auto a1 = t.addAttribute(a1_name); auto a2 = t.addAttribute(a2_name); @@ -759,7 +759,7 @@ putAttributes(AttributeWriterTest &t, std::vector<uint32_t> expExecuteHistory) EXPECT_EQ(1u, a1->getNumDocs()); EXPECT_EQ(1u, a2->getNumDocs()); EXPECT_EQ(1u, a3->getNumDocs()); - auto doc = edb.make_document("id:ns:searchdocument::1"); + auto doc = db.make_document("id:ns:searchdocument::1"); doc->setValue(a1_name, IntFieldValue(10)); doc->setValue(a2_name, IntFieldValue(15)); doc->setValue(a3_name, IntFieldValue(20)); @@ -871,7 +871,7 @@ TEST_F(AttributeWriterTest, tensor_attributes_using_two_phase_put_are_in_separat class TwoPhasePutTest : public AttributeWriterTest { public: - EmptyDocBuilder builder; + DocBuilder builder; vespalib::string doc_id; std::shared_ptr<MockDenseTensorAttribute> attr; std::unique_ptr<Value> tensor; diff --git a/searchcore/src/tests/proton/attribute/document_field_populator/CMakeLists.txt b/searchcore/src/tests/proton/attribute/document_field_populator/CMakeLists.txt index dea03cbf040..41b2037b6cb 100644 --- a/searchcore/src/tests/proton/attribute/document_field_populator/CMakeLists.txt +++ b/searchcore/src/tests/proton/attribute/document_field_populator/CMakeLists.txt @@ -5,5 +5,6 @@ vespa_add_executable(searchcore_document_field_populator_test_app TEST DEPENDS searchcore_attribute searchcore_pcommon + searchlib_test ) vespa_add_test(NAME searchcore_document_field_populator_test_app COMMAND searchcore_document_field_populator_test_app) diff --git a/searchcore/src/tests/proton/attribute/document_field_populator/document_field_populator_test.cpp b/searchcore/src/tests/proton/attribute/document_field_populator/document_field_populator_test.cpp index b08764289e6..e831e8b2ac7 100644 --- a/searchcore/src/tests/proton/attribute/document_field_populator/document_field_populator_test.cpp +++ b/searchcore/src/tests/proton/attribute/document_field_populator/document_field_populator_test.cpp @@ -1,12 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/document/datatype/datatype.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/vespalib/testkit/testapp.h> +#include <vespa/searchcommon/attribute/config.h> #include <vespa/searchcore/proton/attribute/document_field_populator.h> #include <vespa/searchlib/attribute/attributefactory.h> #include <vespa/searchlib/attribute/integerbase.h> -#include <vespa/searchlib/index/docbuilder.h> -#include <vespa/searchcommon/common/schema.h> -#include <vespa/searchcommon/attribute/config.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/vespalib/util/stringfmt.h> #include <vespa/log/log.h> @@ -15,38 +16,22 @@ LOG_SETUP("document_field_populator_test"); using namespace document; using namespace proton; using namespace search; -using namespace search::index; +using search::test::DocBuilder; typedef search::attribute::Config AVConfig; typedef search::attribute::BasicType AVBasicType; -Schema::AttributeField -createAttributeField() -{ - return Schema::AttributeField("a1", Schema::DataType::INT32); -} - -Schema -createSchema() -{ - Schema schema; - schema.addAttributeField(createAttributeField()); - return schema; -} - struct DocContext { - Schema _schema; DocBuilder _builder; DocContext() - : _schema(createSchema()), - _builder(_schema) + : _builder([](auto& header) { header.addField("a1", DataType::T_INT); }) { } Document::UP create(uint32_t id) { vespalib::string docId = vespalib::make_string("id:searchdocument:searchdocument::%u", id); - return _builder.startDocument(docId).endDocument(); + return _builder.make_document(docId); } }; diff --git a/searchcore/src/tests/proton/attribute/imported_attributes_context/imported_attributes_context_test.cpp b/searchcore/src/tests/proton/attribute/imported_attributes_context/imported_attributes_context_test.cpp index c52978261a7..7a5c76b201a 100644 --- a/searchcore/src/tests/proton/attribute/imported_attributes_context/imported_attributes_context_test.cpp +++ b/searchcore/src/tests/proton/attribute/imported_attributes_context/imported_attributes_context_test.cpp @@ -56,10 +56,11 @@ hasActiveEnumGuards(AttributeVector &attr) } void -assertGuards(AttributeVector &attr, generation_t expCurrentGeneration, generation_t expFirstUsedGeneration, bool expHasActiveEnumGuards) +assertGuards(AttributeVector &attr, generation_t expCurrentGeneration, generation_t exp_oldest_used_generation, + bool expHasActiveEnumGuards) { EXPECT_EQUAL(expCurrentGeneration, attr.getCurrentGeneration()); - EXPECT_EQUAL(expFirstUsedGeneration, attr.getFirstUsedGeneration()); + EXPECT_EQUAL(exp_oldest_used_generation, attr.get_oldest_used_generation()); EXPECT_EQUAL(expHasActiveEnumGuards, hasActiveEnumGuards(attr)); } diff --git a/searchcore/src/tests/proton/docsummary/docsummary.cpp b/searchcore/src/tests/proton/docsummary/docsummary.cpp index e76a7f72cbb..677419d9cee 100644 --- a/searchcore/src/tests/proton/docsummary/docsummary.cpp +++ b/searchcore/src/tests/proton/docsummary/docsummary.cpp @@ -8,6 +8,7 @@ #include <vespa/document/annotation/spanlist.h> #include <vespa/document/annotation/spantree.h> #include <vespa/document/config/documenttypes_config_fwd.h> +#include <vespa/document/datatype/annotationtype.h> #include <vespa/document/datatype/documenttype.h> #include <vespa/document/datatype/tensor_data_type.h> #include <vespa/document/datatype/urldatatype.h> @@ -51,8 +52,8 @@ #include <vespa/searchlib/attribute/interlock.h> #include <vespa/searchlib/engine/docsumapi.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> -#include <vespa/searchlib/index/empty_doc_builder.h> #include <vespa/searchlib/tensor/tensor_attribute.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/searchlib/transactionlog/nosyncproxy.h> #include <vespa/searchlib/transactionlog/translogserver.h> #include <vespa/searchsummary/docsummary/i_docsum_field_writer_factory.h> @@ -90,7 +91,7 @@ using document::test::makeBucketSpace; using search::TuneFileDocumentDB; using search::index::DummyFileHeaderContext; using search::linguistics::SPANTREE_NAME; -using search::linguistics::TERM; +using search::test::DocBuilder; using storage::spi::Timestamp; using vespa::config::search::core::ProtonConfig; using vespa::config::content::core::BucketspacesConfig; @@ -128,7 +129,7 @@ private: vespalib::string _dir; }; -class BuildContext : public EmptyDocBuilder +class BuildContext : public DocBuilder { public: DirMaker _dmk; @@ -155,7 +156,7 @@ public: }; BuildContext::BuildContext(AddFieldsType add_fields) - : EmptyDocBuilder(add_fields), + : DocBuilder(add_fields), _dmk("summary"), _fixed_repo(get_repo(), get_document_type()), _summaryExecutor(4, 128_Ki), @@ -181,9 +182,9 @@ BuildContext::make_annotated_string() auto span_list_up = std::make_unique<SpanList>(); auto span_list = span_list_up.get(); auto tree = std::make_unique<SpanTree>(SPANTREE_NAME, std::move(span_list_up)); - tree->annotate(span_list->add(std::make_unique<Span>(0, 3)), *TERM); + tree->annotate(span_list->add(std::make_unique<Span>(0, 3)), *AnnotationType::TERM); tree->annotate(span_list->add(std::make_unique<Span>(4, 3)), - Annotation(*TERM, std::make_unique<StringFieldValue>("baz"))); + Annotation(*AnnotationType::TERM, std::make_unique<StringFieldValue>("baz"))); StringFieldValue value("foo bar"); StringFieldValue::SpanTrees trees; trees.push_back(std::move(tree)); @@ -627,27 +628,27 @@ TEST("requireThatAttributesAreUsed") doc->setValue("ba", IntFieldValue(10)); doc->setValue("bb", FloatFieldValue(10.1250)); doc->setValue("bc", StringFieldValue("foo")); - ArrayFieldValue int_array(bc.get_data_type("Array<Int>")); + auto int_array = bc.make_array("bd"); int_array.add(IntFieldValue(20)); int_array.add(IntFieldValue(30)); doc->setValue("bd", int_array); - ArrayFieldValue float_array(bc.get_data_type("Array<Float>")); + auto float_array = bc.make_array("be"); float_array.add(FloatFieldValue(20.25000)); float_array.add(FloatFieldValue(30.31250)); doc->setValue("be", float_array); - ArrayFieldValue string_array(bc.get_data_type("Array<String>")); + auto string_array = bc.make_array("bf"); string_array.add(StringFieldValue("bar")); string_array.add(StringFieldValue("baz")); doc->setValue("bf", string_array); - WeightedSetFieldValue int_wset(bc.get_data_type("WeightedSet<Int>")); + auto int_wset = bc.make_wset("bg"); int_wset.add(IntFieldValue(40), 2); int_wset.add(IntFieldValue(50), 3); doc->setValue("bg", int_wset); - WeightedSetFieldValue float_wset(bc.get_data_type("WeightedSet<Float>")); + auto float_wset = bc.make_wset("bh"); float_wset.add(FloatFieldValue(40.4375), 4); float_wset.add(FloatFieldValue(50.5625), 5); doc->setValue("bh", float_wset); - WeightedSetFieldValue string_wset(bc.get_data_type("WeightedSet<String>")); + auto string_wset = bc.make_wset("bi"); string_wset.add(StringFieldValue("quux"), 7); string_wset.add(StringFieldValue("qux"), 6); doc->setValue("bi", string_wset); @@ -774,7 +775,7 @@ TEST_F("requireThatUrisAreUsed", Fixture) .addField("uriwset", Wset(UrlDataType::getInstance().getId())); }); DBContext dc(bc.get_repo_sp(), getDocTypeName()); auto exp = bc.make_document("id:ns:searchdocument::0"); - StructFieldValue uri(bc.get_data_type("url")); + auto uri = bc.make_url(); uri.setValue("all", StringFieldValue("http://www.example.com:81/fluke?ab=2#4")); uri.setValue("scheme", StringFieldValue("http")); uri.setValue("host", StringFieldValue("www.example.com")); @@ -783,7 +784,7 @@ TEST_F("requireThatUrisAreUsed", Fixture) uri.setValue("query", StringFieldValue("ab=2")); uri.setValue("fragment", StringFieldValue("4")); exp->setValue("urisingle", uri); - ArrayFieldValue uri_array(bc.get_data_type("Array<url>")); + auto uri_array = bc.make_array("uriarray"); uri.setValue("all", StringFieldValue("http://www.example.com:82/fluke?ab=2#8")); uri.setValue("scheme", StringFieldValue("http")); uri.setValue("host", StringFieldValue("www.example.com")); @@ -801,7 +802,7 @@ TEST_F("requireThatUrisAreUsed", Fixture) uri.setValue("fragment", StringFieldValue("9")); uri_array.add(uri); exp->setValue("uriarray", uri_array); - WeightedSetFieldValue uri_wset(bc.get_data_type("WeightedSet<url>")); + auto uri_wset = bc.make_wset("uriwset"); uri.setValue("all", StringFieldValue("http://www.example.com:83/fluke?ab=2#12")); uri.setValue("scheme", StringFieldValue("http")); uri.setValue("host", StringFieldValue("www.example.com")); @@ -867,11 +868,11 @@ TEST("requireThatPositionsAreUsed") DBContext dc(bc.get_repo_sp(), getDocTypeName()); auto exp = bc.make_document("id:ns:searchdocument::1"); exp->setValue("sp2", LongFieldValue(ZCurve::encode(1002, 1003))); - ArrayFieldValue pos_array(bc.get_data_type("Array<Long>")); + auto pos_array = bc.make_array("ap2"); pos_array.add(LongFieldValue(ZCurve::encode(1006, 1007))); pos_array.add(LongFieldValue(ZCurve::encode(1008, 1009))); exp->setValue("ap2", pos_array); - WeightedSetFieldValue pos_wset(bc.get_data_type("WeightedSet<Long>")); + auto pos_wset = bc.make_wset("wp2"); pos_wset.add(LongFieldValue(ZCurve::encode(1012, 1013)), 43); pos_wset.add(LongFieldValue(ZCurve::encode(1014, 1015)), 44); exp->setValue("wp2", pos_wset); @@ -928,11 +929,11 @@ TEST_F("requireThatRawFieldsWorks", Fixture) DBContext dc(bc.get_repo_sp(), getDocTypeName()); auto exp = bc.make_document("id:ns:searchdocument::0"); exp->setValue("i", RawFieldValue(raw1s)); - ArrayFieldValue raw_array(bc.get_data_type("Array<Raw>")); + auto raw_array = bc.make_array("araw"); raw_array.add(RawFieldValue(raw1a0)); raw_array.add(RawFieldValue(raw1a1)); exp->setValue("araw", raw_array); - WeightedSetFieldValue raw_wset(bc.get_data_type("WeightedSet<Raw>")); + auto raw_wset = bc.make_wset("wraw"); raw_wset.add(RawFieldValue(raw1w1), 46); raw_wset.add(RawFieldValue(raw1w0), 45); exp->setValue("wraw", raw_wset); diff --git a/searchcore/src/tests/proton/documentdb/document_subdbs/document_subdbs_test.cpp b/searchcore/src/tests/proton/documentdb/document_subdbs/document_subdbs_test.cpp index 45ec3824c11..7394ef1214c 100644 --- a/searchcore/src/tests/proton/documentdb/document_subdbs/document_subdbs_test.cpp +++ b/searchcore/src/tests/proton/documentdb/document_subdbs/document_subdbs_test.cpp @@ -1,5 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/document/datatype/datatype.h> +#include <vespa/document/fieldvalue/intfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/searchcore/proton/attribute/imported_attributes_repo.h> #include <vespa/searchcore/proton/bucketdb/bucketdbhandler.h> #include <vespa/searchcore/proton/bucketdb/bucket_db_owner.h> @@ -27,8 +30,8 @@ #include <vespa/searchcore/proton/test/thread_utils.h> #include <vespa/searchcore/proton/test/transport_helper.h> #include <vespa/searchlib/attribute/interlock.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/test/directory_handler.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/searchcommon/attribute/config.h> #include <vespa/config-bucketspaces.h> #include <vespa/config/subscription/sourcespec.h> @@ -60,6 +63,7 @@ using proton::bucketdb::IBucketDBHandler; using proton::bucketdb::IBucketDBHandlerInitializer; using vespalib::IDestructorCallback; using search::test::DirectoryHandler; +using search::test::DocBuilder; using searchcorespi::IFlushTarget; using searchcorespi::index::IThreadingService; using storage::spi::Timestamp; @@ -258,6 +262,17 @@ struct TwoAttrSchema : public OneAttrSchema } }; +DocBuilder::AddFieldsType +get_add_fields(bool has_attr2) +{ + return [has_attr2](auto& header) { + header.addField("attr1", DataType::T_INT); + if (has_attr2) { + header.addField("attr2", DataType::T_INT); + } + }; +} + struct MyConfigSnapshot { typedef std::unique_ptr<MyConfigSnapshot> UP; @@ -267,15 +282,15 @@ struct MyConfigSnapshot BootstrapConfig::SP _bootstrap; MyConfigSnapshot(FNET_Transport & transport, const Schema &schema, const vespalib::string &cfgDir) : _schema(schema), - _builder(_schema), + _builder(get_add_fields(_schema.getNumAttributeFields() > 1)), _cfg(), _bootstrap() { - auto documenttypesConfig = std::make_shared<DocumenttypesConfig>(_builder.getDocumenttypesConfig()); + auto documenttypesConfig = std::make_shared<DocumenttypesConfig>(_builder.get_documenttypes_config()); auto tuneFileDocumentDB = std::make_shared<TuneFileDocumentDB>(); _bootstrap = std::make_shared<BootstrapConfig>(1, documenttypesConfig, - _builder.getDocumentTypeRepo(), + _builder.get_repo_sp(), std::make_shared<ProtonConfig>(), std::make_shared<FiledistributorrpcConfig>(), std::make_shared<BucketspacesConfig>(), @@ -747,7 +762,7 @@ struct DocumentHandler { FixtureType &_f; DocBuilder _builder; - DocumentHandler(FixtureType &f) : _f(f), _builder(f._baseSchema) {} + DocumentHandler(FixtureType &f) : _f(f), _builder(get_add_fields(f._baseSchema.getNumAttributeFields() > 1)) {} static constexpr uint32_t BUCKET_USED_BITS = 8; static DocumentId createDocId(uint32_t docId) { @@ -755,16 +770,16 @@ struct DocumentHandler "searchdocument::%u", docId)); } Document::UP createEmptyDoc(uint32_t docId) { - return _builder.startDocument - (vespalib::make_string("id:searchdocument:searchdocument::%u", - docId)). - endDocument(); + auto id = vespalib::make_string("id:searchdocument:searchdocument::%u", + docId); + return _builder.make_document(id); } Document::UP createDoc(uint32_t docId, int64_t attr1Value, int64_t attr2Value) { - return _builder.startDocument - (vespalib::make_string("id:searchdocument:searchdocument::%u", docId)). - startAttributeField("attr1").addInt(attr1Value).endField(). - startAttributeField("attr2").addInt(attr2Value).endField().endDocument(); + auto id = vespalib::make_string("id:searchdocument:searchdocument::%u", docId); + auto doc = _builder.make_document(id); + doc->setValue("attr1", IntFieldValue(attr1Value)); + doc->setValue("attr2", IntFieldValue(attr2Value)); + return doc; } PutOperation createPut(Document::UP doc, Timestamp timestamp, SerialNum serialNum) { proton::test::Document testDoc(Document::SP(doc.release()), 0, timestamp); diff --git a/searchcore/src/tests/proton/documentdb/feedhandler/feedhandler_test.cpp b/searchcore/src/tests/proton/documentdb/feedhandler/feedhandler_test.cpp index e89d5eef078..abd3fba65fd 100644 --- a/searchcore/src/tests/proton/documentdb/feedhandler/feedhandler_test.cpp +++ b/searchcore/src/tests/proton/documentdb/feedhandler/feedhandler_test.cpp @@ -3,7 +3,11 @@ #include <vespa/persistence/spi/result.h> #include <vespa/document/datatype/tensor_data_type.h> #include <vespa/document/datatype/documenttype.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/fieldvalue/tensorfieldvalue.h> #include <vespa/document/update/assignvalueupdate.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/document/repo/documenttyperepo.h> #include <vespa/document/update/documentupdate.h> #include <vespa/document/update/clearvalueupdate.h> @@ -29,8 +33,8 @@ #include <vespa/searchcore/proton/server/ireplayconfig.h> #include <vespa/searchcore/proton/test/dummy_feed_view.h> #include <vespa/searchcore/proton/test/transport_helper.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/searchlib/transactionlog/translogserver.h> #include <vespa/vespalib/testkit/testapp.h> #include <vespa/vespalib/util/lambdatask.h> @@ -55,6 +59,7 @@ using search::SerialNum; using search::index::schema::CollectionType; using search::index::schema::DataType; using vespalib::makeLambdaTask; +using search::test::DocBuilder; using search::transactionlog::TransLogServer; using search::transactionlog::DomainConfig; using storage::spi::RemoveResult; @@ -271,20 +276,33 @@ MyFeedView::~MyFeedView() = default; struct SchemaContext { - Schema::SP schema; - std::unique_ptr<DocBuilder> builder; + Schema::SP schema; + DocBuilder builder; SchemaContext(); + SchemaContext(bool has_i2); ~SchemaContext(); DocTypeName getDocType() const { - return DocTypeName(builder->getDocumentType().getName()); + return DocTypeName(builder.get_document_type().getName()); } - const std::shared_ptr<const document::DocumentTypeRepo> &getRepo() const { return builder->getDocumentTypeRepo(); } + std::shared_ptr<const document::DocumentTypeRepo> getRepo() const { return builder.get_repo_sp(); } void addField(vespalib::stringref fieldName); }; SchemaContext::SchemaContext() + : SchemaContext(false) +{ +} + +SchemaContext::SchemaContext(bool has_i2) : schema(std::make_shared<Schema>()), - builder() + builder([has_i2](auto& header) { + header.addTensorField("tensor", "tensor(x{},y{})") + .addTensorField("tensor2", "tensor(x{},y{})") + .addField("i1", document::DataType::T_STRING); + if (has_i2) { + header.addField("i2", document::DataType::T_STRING); + } + }) { schema->addAttributeField(Schema::AttributeField("tensor", DataType::TENSOR, CollectionType::SINGLE, "tensor(x{},y{})")); schema->addAttributeField(Schema::AttributeField("tensor2", DataType::TENSOR, CollectionType::SINGLE, "tensor(x{},y{})")); @@ -298,14 +316,13 @@ void SchemaContext::addField(vespalib::stringref fieldName) { schema->addIndexField(Schema::IndexField(fieldName, DataType::STRING, CollectionType::SINGLE)); - builder = std::make_unique<DocBuilder>(*schema); } struct DocumentContext { Document::SP doc; BucketId bucketId; DocumentContext(const vespalib::string &docId, DocBuilder &builder) : - doc(builder.startDocument(docId).endDocument().release()), + doc(builder.make_document(docId)), bucketId(BucketFactory::getBucketId(doc->getId())) { } @@ -313,7 +330,7 @@ struct DocumentContext { struct TwoFieldsSchemaContext : public SchemaContext { TwoFieldsSchemaContext() - : SchemaContext() + : SchemaContext(true) { addField("i2"); } @@ -325,7 +342,7 @@ struct UpdateContext { DocumentUpdate::SP update; BucketId bucketId; UpdateContext(const vespalib::string &docId, DocBuilder &builder) : - update(std::make_shared<DocumentUpdate>(*builder.getDocumentTypeRepo(), builder.getDocumentType(), DocumentId(docId))), + update(std::make_shared<DocumentUpdate>(builder.get_repo(), builder.get_document_type(), DocumentId(docId))), bucketId(BucketFactory::getBucketId(update->getId())) { } @@ -464,7 +481,7 @@ TEST_F("require that heartBeat calls FeedView's heartBeat", TEST_F("require that outdated remove is ignored", FeedHandlerFixture) { - DocumentContext doc_context("id:ns:searchdocument::foo", *f.schema.builder); + DocumentContext doc_context("id:ns:searchdocument::foo", f.schema.builder); auto op = std::make_unique<RemoveOperationWithDocId>(doc_context.bucketId, Timestamp(10), doc_context.doc->getId()); static_cast<DocumentOperation &>(*op).setPrevDbDocumentId(DbDocumentId(4)); static_cast<DocumentOperation &>(*op).setPrevTimestamp(Timestamp(10000)); @@ -476,7 +493,7 @@ TEST_F("require that outdated remove is ignored", FeedHandlerFixture) TEST_F("require that outdated put is ignored", FeedHandlerFixture) { - DocumentContext doc_context("id:ns:searchdocument::foo", *f.schema.builder); + DocumentContext doc_context("id:ns:searchdocument::foo", f.schema.builder); auto op =std::make_unique<PutOperation>(doc_context.bucketId, Timestamp(10), std::move(doc_context.doc)); static_cast<DocumentOperation &>(*op).setPrevTimestamp(Timestamp(10000)); FeedTokenContext token_context; @@ -496,7 +513,7 @@ addLidToRemove(RemoveDocumentsOperation &op) TEST_F("require that handleMove calls FeedView", FeedHandlerFixture) { - DocumentContext doc_context("id:ns:searchdocument::foo", *f.schema.builder); + DocumentContext doc_context("id:ns:searchdocument::foo", f.schema.builder); MoveOperation op(doc_context.bucketId, Timestamp(2), doc_context.doc, DbDocumentId(0, 2), 1); op.setDbDocumentId(DbDocumentId(1, 2)); f.runAsMaster([&]() { f.handler.handleMove(op, IDestructorCallback::SP()); }); @@ -556,7 +573,7 @@ TEST_F("require that flush cannot unprune", FeedHandlerFixture) TEST_F("require that remove of unknown document with known data type stores remove", FeedHandlerFixture) { - DocumentContext doc_context("id:test:searchdocument::foo", *f.schema.builder); + DocumentContext doc_context("id:test:searchdocument::foo", f.schema.builder); auto op = std::make_unique<RemoveOperationWithDocId>(doc_context.bucketId, Timestamp(10), doc_context.doc->getId()); FeedTokenContext token_context; f.handler.performOperation(std::move(token_context.token), std::move(op)); @@ -566,7 +583,7 @@ TEST_F("require that remove of unknown document with known data type stores remo TEST_F("require that partial update for non-existing document is tagged as such", FeedHandlerFixture) { - UpdateContext upCtx("id:test:searchdocument::foo", *f.schema.builder); + UpdateContext upCtx("id:test:searchdocument::foo", f.schema.builder); auto op = std::make_unique<UpdateOperation>(upCtx.bucketId, Timestamp(10), upCtx.update); FeedTokenContext token_context; f.handler.performOperation(std::move(token_context.token), std::move(op)); @@ -582,7 +599,7 @@ TEST_F("require that partial update for non-existing document is tagged as such" TEST_F("require that partial update for non-existing document is created if specified", FeedHandlerFixture) { f.handler.setSerialNum(15); - UpdateContext upCtx("id:test:searchdocument::foo", *f.schema.builder); + UpdateContext upCtx("id:test:searchdocument::foo", f.schema.builder); upCtx.update->setCreateIfNonExistent(true); f.feedView.metaStore.insert(upCtx.update->getId().getGlobalId(), MyDocumentMetaStore::Entry(5, 5, Timestamp(10))); auto op = std::make_unique<UpdateOperation>(upCtx.bucketId, Timestamp(10), upCtx.update); @@ -605,7 +622,7 @@ TEST_F("require that put is rejected if resource limit is reached", FeedHandlerF f.writeFilter._acceptWriteOperation = false; f.writeFilter._message = "Attribute resource limit reached"; - DocumentContext docCtx("id:test:searchdocument::foo", *f.schema.builder); + DocumentContext docCtx("id:test:searchdocument::foo", f.schema.builder); auto op = std::make_unique<PutOperation>(docCtx.bucketId, Timestamp(10), std::move(docCtx.doc)); FeedTokenContext token; f.handler.performOperation(std::move(token.token), std::move(op)); @@ -620,7 +637,7 @@ TEST_F("require that update is rejected if resource limit is reached", FeedHandl f.writeFilter._acceptWriteOperation = false; f.writeFilter._message = "Attribute resource limit reached"; - UpdateContext updCtx("id:test:searchdocument::foo", *f.schema.builder); + UpdateContext updCtx("id:test:searchdocument::foo", f.schema.builder); updCtx.addFieldUpdate("tensor"); auto op = std::make_unique<UpdateOperation>(updCtx.bucketId, Timestamp(10), updCtx.update); FeedTokenContext token; @@ -637,7 +654,7 @@ TEST_F("require that remove is NOT rejected if resource limit is reached", FeedH f.writeFilter._acceptWriteOperation = false; f.writeFilter._message = "Attribute resource limit reached"; - DocumentContext docCtx("id:test:searchdocument::foo", *f.schema.builder); + DocumentContext docCtx("id:test:searchdocument::foo", f.schema.builder); auto op = std::make_unique<RemoveOperationWithDocId>(docCtx.bucketId, Timestamp(10), docCtx.doc->getId()); FeedTokenContext token; f.handler.performOperation(std::move(token.token), std::move(op)); @@ -651,7 +668,7 @@ checkUpdate(FeedHandlerFixture &f, SchemaContext &schemaContext, const vespalib::string &fieldName, bool expectReject, bool existing) { f.handler.setSerialNum(15); - UpdateContext updCtx("id:test:searchdocument::foo", *schemaContext.builder); + UpdateContext updCtx("id:test:searchdocument::foo", schemaContext.builder); updCtx.addFieldUpdate(fieldName); if (existing) { f.feedView.metaStore.insert(updCtx.update->getId().getGlobalId(), MyDocumentMetaStore::Entry(5, 5, Timestamp(9))); @@ -733,7 +750,7 @@ TEST_F("require that tensor update with wrong tensor type fails", FeedHandlerFix TEST_F("require that put with different document type repo is ok", FeedHandlerFixture) { TwoFieldsSchemaContext schema; - DocumentContext doc_context("id:ns:searchdocument::foo", *schema.builder); + DocumentContext doc_context("id:ns:searchdocument::foo", schema.builder); auto op = std::make_unique<PutOperation>(doc_context.bucketId, Timestamp(10), std::move(doc_context.doc)); FeedTokenContext token_context; @@ -747,7 +764,7 @@ TEST_F("require that put with different document type repo is ok", FeedHandlerFi TEST_F("require that feed stats are updated", FeedHandlerFixture) { - DocumentContext doc_context("id:ns:searchdocument::foo", *f.schema.builder); + DocumentContext doc_context("id:ns:searchdocument::foo", f.schema.builder); auto op =std::make_unique<PutOperation>(doc_context.bucketId, Timestamp(10), std::move(doc_context.doc)); FeedTokenContext token_context; f.handler.performOperation(std::move(token_context.token), std::move(op)); diff --git a/searchcore/src/tests/proton/documentdb/feedview/feedview_test.cpp b/searchcore/src/tests/proton/documentdb/feedview/feedview_test.cpp index d0b7b03c3ab..5758e69dea4 100644 --- a/searchcore/src/tests/proton/documentdb/feedview/feedview_test.cpp +++ b/searchcore/src/tests/proton/documentdb/feedview/feedview_test.cpp @@ -26,7 +26,7 @@ #include <vespa/searchcore/proton/test/threading_service_observer.h> #include <vespa/searchcore/proton/test/transport_helper.h> #include <vespa/searchlib/attribute/attributefactory.h> -#include <vespa/searchlib/index/empty_doc_builder.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/searchcommon/attribute/config.h> #include <vespa/vespalib/util/destructor_callbacks.h> #include <vespa/vespalib/stllike/asciistream.h> @@ -49,6 +49,7 @@ using vespalib::GateCallback; using search::SearchableStats; using search::index::schema::CollectionType; using search::index::schema::DataType; +using search::test::DocBuilder; using searchcorespi::IndexSearchable; using storage::spi::BucketChecksum; using storage::spi::BucketInfo; @@ -436,7 +437,7 @@ MyTransport::~MyTransport() = default; struct SchemaContext { Schema::SP _schema; - EmptyDocBuilder _builder; + DocBuilder _builder; SchemaContext(); ~SchemaContext(); std::shared_ptr<const document::DocumentTypeRepo> getRepo() const { return _builder.get_repo_sp(); } @@ -466,16 +467,16 @@ struct DocumentContext BucketId bid; Timestamp ts; typedef std::vector<DocumentContext> List; - DocumentContext(const vespalib::string &docId, uint64_t timestamp, EmptyDocBuilder &builder); + DocumentContext(const vespalib::string &docId, uint64_t timestamp, DocBuilder &builder); ~DocumentContext(); - void addFieldUpdate(EmptyDocBuilder &builder, const vespalib::string &fieldName) { + void addFieldUpdate(DocBuilder &builder, const vespalib::string &fieldName) { const document::Field &field = builder.get_document_type().getField(fieldName); upd->addUpdate(document::FieldUpdate(field)); } document::GlobalId gid() const { return doc->getId().getGlobalId(); } }; -DocumentContext::DocumentContext(const vespalib::string &docId, uint64_t timestamp, EmptyDocBuilder& builder) +DocumentContext::DocumentContext(const vespalib::string &docId, uint64_t timestamp, DocBuilder& builder) : doc(builder.make_document(docId)), upd(std::make_shared<DocumentUpdate>(builder.get_repo(), builder.get_document_type(), doc->getId())), bid(BucketFactory::getNumBucketBits(), doc->getId().getGlobalId().convertToBucketId().getRawId()), @@ -555,7 +556,7 @@ struct FixtureBase return getMetaStore().getMetaData(doc_.doc->getId().getGlobalId()); } - EmptyDocBuilder &getBuilder() { return sc._builder; } + DocBuilder &getBuilder() { return sc._builder; } DocumentContext doc(const vespalib::string &docId, uint64_t timestamp) { return DocumentContext(docId, timestamp, getBuilder()); diff --git a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.cpp b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.cpp index 9c68d7d5974..b3a2e9cad83 100644 --- a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.cpp +++ b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.cpp @@ -127,7 +127,8 @@ MyHandler::handleCompactLidSpace(const CompactLidSpaceOperation &op, std::shared } MyHandler::MyHandler(bool storeMoveDoneContexts, bool bucketIdEqualLid) - : _stats(), + : _builder(), + _stats(), _moveFromLid(0), _moveToLid(0), _handleMoveCnt(0), @@ -140,9 +141,8 @@ MyHandler::MyHandler(bool storeMoveDoneContexts, bool bucketIdEqualLid) _rm_listener(), _docs() { - DocBuilder builder = DocBuilder(Schema()); for (uint32_t i(0); i < 10; i++) { - auto doc = builder.startDocument(fmt("%s%d", DOC_ID.c_str(), i)).endDocument(); + auto doc = _builder.make_document(fmt("%s%d", DOC_ID.c_str(), i)); _docs.emplace_back(DocumentMetaData(i, TIMESTAMP_1, createBucketId(i), doc->getId().getGlobalId()), std::move(doc)); } } diff --git a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.h b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.h index b404fc6956a..af984cb357e 100644 --- a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.h +++ b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_common.h @@ -17,11 +17,14 @@ #include <vespa/searchcore/proton/test/test.h> #include <vespa/searchcore/proton/test/dummy_document_store.h> #include <vespa/vespalib/util/idestructorcallback.h> -#include <vespa/searchlib/index/docbuilder.h> -using namespace document; +using document::BucketId; +using document::GlobalId; +using document::Document; +using document::DocumentId; +using document::DocumentTypeRepo; using namespace proton; -using namespace search::index; +using search::test::DocBuilder; using namespace search; using namespace vespalib; using vespalib::IDestructorCallback; @@ -60,6 +63,7 @@ struct MyScanIterator : public IDocumentScanIterator { }; struct MyHandler : public ILidSpaceCompactionHandler { + DocBuilder _builder; std::vector<LidUsageStats> _stats; std::vector<LidVector> _lids; mutable uint32_t _moveFromLid; @@ -103,14 +107,14 @@ struct MyStorer : public IOperationStorer { CommitResult startCommit(DoneCallback) override; }; -struct MyFeedView : public test::DummyFeedView { +struct MyFeedView : public proton::test::DummyFeedView { explicit MyFeedView(std::shared_ptr<const DocumentTypeRepo> repo) - : test::DummyFeedView(std::move(repo)) + : proton::test::DummyFeedView(std::move(repo)) { } }; -struct MyDocumentStore : public test::DummyDocumentStore { +struct MyDocumentStore : public proton::test::DummyDocumentStore { Document::SP _readDoc; mutable uint32_t _readLid; MyDocumentStore(); diff --git a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_handler_test.cpp b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_handler_test.cpp index bc9cd9a93fa..011de4fc298 100644 --- a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_handler_test.cpp +++ b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_handler_test.cpp @@ -15,13 +15,13 @@ struct HandlerTest : public ::testing::Test { }; HandlerTest::HandlerTest() - : _docBuilder(Schema()), + : _docBuilder(), _bucketDB(std::make_shared<bucketdb::BucketDBOwner>()), _docStore(), - _subDb(_bucketDB, _docStore, _docBuilder.getDocumentTypeRepo()), + _subDb(_bucketDB, _docStore, _docBuilder.get_repo_sp()), _handler(_subDb.maintenance_sub_db, "test") { - _docStore._readDoc = _docBuilder.startDocument(DOC_ID).endDocument(); + _docStore._readDoc = _docBuilder.make_document(DOC_ID); } HandlerTest::~HandlerTest() = default; diff --git a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.cpp b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.cpp index 8f88d678c0c..b941161cc35 100644 --- a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.cpp +++ b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.cpp @@ -116,7 +116,7 @@ JobTestBase::compact() { void JobTestBase::notifyNodeRetired(bool nodeRetired) { - test::BucketStateCalculator::SP calc = std::make_shared<test::BucketStateCalculator>(); + proton::test::BucketStateCalculator::SP calc = std::make_shared<proton::test::BucketStateCalculator>(); calc->setNodeRetired(nodeRetired); _clusterStateHandler.notifyClusterStateChanged(calc); } diff --git a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.h b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.h index 5875910f4d9..fb4c6d0478a 100644 --- a/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.h +++ b/searchcore/src/tests/proton/documentdb/lid_space_compaction/lid_space_jobtest.h @@ -10,8 +10,8 @@ namespace storage::spi::dummy { class DummyBucketExecutor; } struct JobTestBase : public ::testing::Test { vespalib::MonitoredRefCount _refCount; - test::ClusterStateHandler _clusterStateHandler; - test::DiskMemUsageNotifier _diskMemUsageNotifier; + proton::test::ClusterStateHandler _clusterStateHandler; + proton::test::DiskMemUsageNotifier _diskMemUsageNotifier; std::unique_ptr<storage::spi::dummy::DummyBucketExecutor> _bucketExecutor; std::unique_ptr<vespalib::SyncableThreadExecutor> _singleExecutor; std::unique_ptr<searchcorespi::index::ISyncableThreadService> _master; diff --git a/searchcore/src/tests/proton/documentdb/maintenancecontroller/maintenancecontroller_test.cpp b/searchcore/src/tests/proton/documentdb/maintenancecontroller/maintenancecontroller_test.cpp index ea4d556c502..915402122b8 100644 --- a/searchcore/src/tests/proton/documentdb/maintenancecontroller/maintenancecontroller_test.cpp +++ b/searchcore/src/tests/proton/documentdb/maintenancecontroller/maintenancecontroller_test.cpp @@ -35,7 +35,6 @@ #include <vespa/searchcore/proton/test/test.h> #include <vespa/searchcore/proton/test/transport_helper.h> #include <vespa/searchlib/common/idocumentmetastore.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/vespalib/data/slime/slime.h> #include <vespa/vespalib/testkit/testapp.h> #include <vespa/vespalib/util/destructor_callbacks.h> @@ -99,11 +98,11 @@ class MyDocumentSubDB uint32_t _subDBId; DocumentMetaStore::SP _metaStoreSP; DocumentMetaStore & _metaStore; - const std::shared_ptr<const document::DocumentTypeRepo> &_repo; + std::shared_ptr<const document::DocumentTypeRepo> _repo; const DocTypeName &_docTypeName; public: - MyDocumentSubDB(uint32_t subDBId, SubDbType subDbType, const std::shared_ptr<const document::DocumentTypeRepo> &repo, + MyDocumentSubDB(uint32_t subDBId, SubDbType subDbType, std::shared_ptr<const document::DocumentTypeRepo> repo, std::shared_ptr<bucketdb::BucketDBOwner> bucketDB, const DocTypeName &docTypeName); ~MyDocumentSubDB(); @@ -136,7 +135,7 @@ public: const IDocumentMetaStore &getMetaStore() const { return _metaStore; } }; -MyDocumentSubDB::MyDocumentSubDB(uint32_t subDBId, SubDbType subDbType, const std::shared_ptr<const document::DocumentTypeRepo> &repo, +MyDocumentSubDB::MyDocumentSubDB(uint32_t subDBId, SubDbType subDbType, std::shared_ptr<const document::DocumentTypeRepo> repo, std::shared_ptr<bucketdb::BucketDBOwner> bucketDB, const DocTypeName &docTypeName) : _docs(), _subDBId(subDBId), @@ -144,7 +143,7 @@ MyDocumentSubDB::MyDocumentSubDB(uint32_t subDBId, SubDbType subDbType, const st std::move(bucketDB), DocumentMetaStore::getFixedName(), search::GrowStrategy(), subDbType)), _metaStore(*_metaStoreSP), - _repo(repo), + _repo(std::move(repo)), _docTypeName(docTypeName) { _metaStore.constructFreeList(); diff --git a/searchcore/src/tests/proton/documentdb/storeonlyfeedview/storeonlyfeedview_test.cpp b/searchcore/src/tests/proton/documentdb/storeonlyfeedview/storeonlyfeedview_test.cpp index 00694b6b78f..b8f5fd232de 100644 --- a/searchcore/src/tests/proton/documentdb/storeonlyfeedview/storeonlyfeedview_test.cpp +++ b/searchcore/src/tests/proton/documentdb/storeonlyfeedview/storeonlyfeedview_test.cpp @@ -2,6 +2,7 @@ #include <vespa/document/base/documentid.h> #include <vespa/document/datatype/datatype.h> +#include <vespa/document/fieldvalue/document.h> #include <vespa/searchcommon/common/schema.h> #include <vespa/searchcore/proton/server/putdonecontext.h> #include <vespa/searchcore/proton/server/removedonecontext.h> @@ -13,7 +14,7 @@ #include <vespa/searchcore/proton/test/mock_summary_adapter.h> #include <vespa/searchcore/proton/test/transport_helper.h> #include <vespa/searchcore/proton/test/thread_utils.h> -#include <vespa/searchlib/index/docbuilder.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/vespalib/util/destructor_callbacks.h> #include <vespa/vespalib/util/size_literals.h> #include <vespa/vespalib/testkit/testapp.h> @@ -32,8 +33,8 @@ using namespace proton; using search::DocumentIdT; using vespalib::IDestructorCallback; using search::SerialNum; -using search::index::DocBuilder; using search::index::Schema; +using search::test::DocBuilder; using storage::spi::Timestamp; using vespalib::make_string; @@ -59,9 +60,8 @@ public: }; std::shared_ptr<const DocumentTypeRepo> myGetDocumentTypeRepo() { - Schema schema; - DocBuilder builder(schema); - std::shared_ptr<const DocumentTypeRepo> repo = builder.getDocumentTypeRepo(); + DocBuilder builder; + std::shared_ptr<const DocumentTypeRepo> repo = builder.get_repo_sp(); ASSERT_TRUE(repo.get()); return repo; } diff --git a/searchcore/src/tests/proton/documentmetastore/documentmetastore_test.cpp b/searchcore/src/tests/proton/documentmetastore/documentmetastore_test.cpp index c62226ad363..4668b8c65ab 100644 --- a/searchcore/src/tests/proton/documentmetastore/documentmetastore_test.cpp +++ b/searchcore/src/tests/proton/documentmetastore/documentmetastore_test.cpp @@ -1802,7 +1802,7 @@ TEST(DocumentMetaStoreTest, shrink_via_flush_target_works) ft->getApproxMemoryGain().getAfter()); g.reset(); - dms->removeAllOldGenerations(); + dms->reclaim_unused_memory(); assertLidSpace(10, shrinkTarget, shrinkTarget - 1, true, true, *dms); EXPECT_TRUE(ft->getApproxMemoryGain().getBefore() > ft->getApproxMemoryGain().getAfter()); @@ -1965,6 +1965,38 @@ TEST(DocumentMetaStoreTest, multiple_lids_can_be_removed_with_removeBatch) dms.removes_complete({1, 3}); } +TEST(DocumentMetaStoreTest, serialize_for_sort) +{ + DocumentMetaStore dms(createBucketDB()); + dms.constructFreeList(); + addLid(dms, 1); + addLid(dms, 2); + assertLidGidFound(1, dms); + assertLidGidFound(2, dms); + + constexpr size_t SZ = document::GlobalId::LENGTH; + EXPECT_EQ(12u, SZ); + EXPECT_EQ(SZ, dms.getFixedWidth()); + uint8_t asc_dest[SZ]; + EXPECT_EQ(0, dms.serializeForAscendingSort(3, asc_dest, sizeof(asc_dest), nullptr)); + EXPECT_EQ(-1, dms.serializeForAscendingSort(1, asc_dest, sizeof(asc_dest) - 1, nullptr)); + document::GlobalId gid; + + EXPECT_EQ(SZ, dms.serializeForAscendingSort(1, asc_dest, sizeof(asc_dest), nullptr)); + EXPECT_TRUE(dms.getGid(1, gid)); + EXPECT_EQ(0, memcmp(asc_dest, gid.get(), SZ)); + + EXPECT_EQ(SZ, dms.serializeForAscendingSort(2, asc_dest, sizeof(asc_dest), nullptr)); + EXPECT_TRUE(dms.getGid(2, gid)); + EXPECT_EQ(0, memcmp(asc_dest, gid.get(), SZ)); + + uint8_t desc_dest[SZ]; + EXPECT_EQ(SZ, dms.serializeForDescendingSort(2, desc_dest, sizeof(desc_dest), nullptr)); + for (size_t i(0); i < SZ; i++) { + EXPECT_EQ(0xff - asc_dest[i], desc_dest[i]); + } +} + class MockOperationListener : public documentmetastore::OperationListener { public: size_t remove_batch_cnt; @@ -2008,7 +2040,7 @@ namespace { void try_compact_document_meta_store(DocumentMetaStore &dms) { - dms.removeAllOldGenerations(); + dms.reclaim_unused_memory(); dms.commit(true); } diff --git a/searchcore/src/tests/proton/documentmetastore/lid_allocator/lid_allocator_test.cpp b/searchcore/src/tests/proton/documentmetastore/lid_allocator/lid_allocator_test.cpp index 2d675e82db2..8d8674da4f0 100644 --- a/searchcore/src/tests/proton/documentmetastore/lid_allocator/lid_allocator_test.cpp +++ b/searchcore/src/tests/proton/documentmetastore/lid_allocator/lid_allocator_test.cpp @@ -28,7 +28,7 @@ protected: ~LidAllocatorTest() { - _gen_hold.clearHoldLists(); + _gen_hold.reclaim_all(); } uint32_t get_size() { return _allocator.getActiveLids().size(); } @@ -66,8 +66,8 @@ protected: _allocator.holdLids(lids, get_size(), 0); } - void trim_hold_lists() { - _allocator.trimHoldLists(1); + void reclaim_memory() { + _allocator.reclaim_memory(1); } std::vector<uint32_t> get_valid_lids() { @@ -117,7 +117,7 @@ TEST_F(LidAllocatorTest, unregister_lids) assert_valid_lids({2, 4, 6}); assert_active_lids({4, 6}); hold_lids({1, 3, 5}); - trim_hold_lists(); + reclaim_memory(); EXPECT_EQ((std::vector<uint32_t>{1, 3, 5, 7, 8}), alloc_lids(5)); } diff --git a/searchcore/src/tests/proton/documentmetastore/lid_state_vector/lid_state_vector_test.cpp b/searchcore/src/tests/proton/documentmetastore/lid_state_vector/lid_state_vector_test.cpp index ab45cca0971..cbc11126b25 100644 --- a/searchcore/src/tests/proton/documentmetastore/lid_state_vector/lid_state_vector_test.cpp +++ b/searchcore/src/tests/proton/documentmetastore/lid_state_vector/lid_state_vector_test.cpp @@ -22,7 +22,7 @@ protected: ~LidStateVectorTest() { - _gen_hold.clearHoldLists(); + _gen_hold.reclaim_all(); } }; @@ -47,7 +47,7 @@ TEST_F(LidStateVectorTest, basic_free_list_is_working) EXPECT_EQ(0u, freeLids.count()); EXPECT_EQ(3u, list.size()); - list.trimHoldLists(20, freeLids); + list.reclaim_memory(20, freeLids); EXPECT_FALSE(freeLids.empty()); EXPECT_EQ(1u, freeLids.count()); @@ -57,7 +57,7 @@ TEST_F(LidStateVectorTest, basic_free_list_is_working) EXPECT_EQ(0u, freeLids.count()); EXPECT_EQ(2u, list.size()); - list.trimHoldLists(31, freeLids); + list.reclaim_memory(31, freeLids); EXPECT_FALSE(freeLids.empty()); EXPECT_EQ(2u, freeLids.count()); diff --git a/searchcore/src/tests/proton/feed_and_search/CMakeLists.txt b/searchcore/src/tests/proton/feed_and_search/CMakeLists.txt index a565d791988..24588d21d99 100644 --- a/searchcore/src/tests/proton/feed_and_search/CMakeLists.txt +++ b/searchcore/src/tests/proton/feed_and_search/CMakeLists.txt @@ -3,5 +3,6 @@ vespa_add_executable(searchcore_feed_and_search_test_app TEST SOURCES feed_and_search.cpp DEPENDS + searchlib_test ) vespa_add_test(NAME searchcore_feed_and_search_test_app COMMAND searchcore_feed_and_search_test_app) diff --git a/searchcore/src/tests/proton/feed_and_search/feed_and_search.cpp b/searchcore/src/tests/proton/feed_and_search/feed_and_search.cpp index ac540ad2e2d..3ae7a55aabd 100644 --- a/searchcore/src/tests/proton/feed_and_search/feed_and_search.cpp +++ b/searchcore/src/tests/proton/feed_and_search/feed_and_search.cpp @@ -3,6 +3,8 @@ #include <vespa/document/datatype/datatype.h> #include <vespa/document/fieldvalue/document.h> #include <vespa/document/fieldvalue/fieldvalue.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/searchlib/common/documentsummary.h> #include <vespa/vespalib/util/sequencedtaskexecutor.h> #include <vespa/searchlib/common/flush_token.h> @@ -10,9 +12,10 @@ #include <vespa/searchlib/diskindex/fusion.h> #include <vespa/searchlib/diskindex/indexbuilder.h> #include <vespa/searchlib/fef/fef.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> #include <vespa/searchlib/memoryindex/memory_index.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/test/index/mock_field_length_inspector.h> #include <vespa/searchlib/query/base.h> #include <vespa/searchlib/query/tree/simplequery.h> @@ -31,6 +34,7 @@ LOG_SETUP("feed_and_search_test"); using document::DataType; using document::Document; using document::FieldValue; +using document::StringFieldValue; using search::DocumentIdT; using search::FlushToken; using search::TuneFileIndexing; @@ -44,7 +48,6 @@ using search::fef::MatchData; using search::fef::MatchDataLayout; using search::fef::TermFieldHandle; using search::fef::TermFieldMatchData; -using search::index::DocBuilder; using search::index::DummyFileHeaderContext; using search::index::Schema; using search::index::test::MockFieldLengthInspector; @@ -56,6 +59,8 @@ using search::queryeval::FieldSpec; using search::queryeval::FieldSpecList; using search::queryeval::SearchIterator; using search::queryeval::Searchable; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using std::ostringstream; using vespalib::string; @@ -117,10 +122,9 @@ Document::UP buildDocument(DocBuilder & doc_builder, int id, const string &word) { ostringstream ost; ost << "id:ns:searchdocument::" << id; - doc_builder.startDocument(ost.str()); - doc_builder.startIndexField(field_name) - .addStr(noise).addStr(word).endField(); - return doc_builder.endDocument(); + auto doc = doc_builder.make_document(ost.str()); + doc->setValue(field_name, StringFieldBuilder(doc_builder).word(noise).space().word(word).build()); + return doc; } // Performs a search using a Searchable. @@ -165,7 +169,7 @@ void Test::requireThatMemoryIndexCanBeDumpedAndSearched() { auto indexFieldInverter = vespalib::SequencedTaskExecutor::create(invert_executor, 2); auto indexFieldWriter = vespalib::SequencedTaskExecutor::create(write_executor, 2); MemoryIndex memory_index(schema, MockFieldLengthInspector(), *indexFieldInverter, *indexFieldWriter); - DocBuilder doc_builder(schema); + DocBuilder doc_builder([](auto& header) { header.addField(field_name, DataType::T_STRING); }); Document::UP doc = buildDocument(doc_builder, doc_id1, word1); memory_index.insertDocument(doc_id1, *doc, {}); diff --git a/searchcore/src/tests/proton/index/fusionrunner_test.cpp b/searchcore/src/tests/proton/index/fusionrunner_test.cpp index 850f8a8f0d1..0475d37b32c 100644 --- a/searchcore/src/tests/proton/index/fusionrunner_test.cpp +++ b/searchcore/src/tests/proton/index/fusionrunner_test.cpp @@ -1,15 +1,19 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/searchcorespi/index/fusionrunner.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/searchcore/proton/index/indexmanager.h> #include <vespa/searchcore/proton/test/transport_helper.h> -#include <vespa/searchcorespi/index/fusionrunner.h> #include <vespa/vespalib/util/isequencedtaskexecutor.h> #include <vespa/searchlib/common/flush_token.h> #include <vespa/searchlib/diskindex/diskindex.h> #include <vespa/searchlib/diskindex/indexbuilder.h> #include <vespa/searchlib/fef/matchdatalayout.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/memoryindex/memory_index.h> #include <vespa/searchlib/query/tree/simplequery.h> #include <vespa/searchlib/test/index/mock_field_length_inspector.h> @@ -25,6 +29,7 @@ using document::Document; using document::FieldValue; +using document::StringFieldValue; using proton::ExecutorThreadingService; using proton::index::IndexManager; using search::FixedSourceSelector; @@ -38,7 +43,6 @@ using search::fef::MatchData; using search::fef::MatchDataLayout; using search::fef::TermFieldHandle; using search::fef::TermFieldMatchData; -using search::index::DocBuilder; using search::index::DummyFileHeaderContext; using search::index::Schema; using search::index::schema::DataType; @@ -51,6 +55,8 @@ using search::queryeval::FieldSpec; using search::queryeval::FieldSpecList; using search::queryeval::ISourceSelector; using search::queryeval::SearchIterator; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using searchcorespi::index::FusionRunner; using searchcorespi::index::FusionSpec; using std::set; @@ -152,9 +158,9 @@ void Test::tearDown() { Document::UP buildDocument(DocBuilder & doc_builder, int id, const string &word) { vespalib::asciistream ost; ost << "id:ns:searchdocument::" << id; - doc_builder.startDocument(ost.str()); - doc_builder.startIndexField(field_name).addStr(word).endField(); - return doc_builder.endDocument(); + auto doc = doc_builder.make_document(ost.str()); + doc->setValue(field_name, StringFieldBuilder(doc_builder).word(word).build()); + return doc; } void addDocument(DocBuilder & doc_builder, MemoryIndex &index, ISourceSelector &selector, @@ -181,7 +187,7 @@ void Test::createIndex(const string &dir, uint32_t id, bool fusion) { _selector->setDefaultSource(id - _selector->getBaseId()); Schema schema = getSchema(); - DocBuilder doc_builder(schema); + DocBuilder doc_builder([](auto& header) { header.addField(field_name, document::DataType::T_STRING); }); MemoryIndex memory_index(schema, MockFieldLengthInspector(), _service.write().indexFieldInverter(), _service.write().indexFieldWriter()); diff --git a/searchcore/src/tests/proton/index/index_writer/index_writer_test.cpp b/searchcore/src/tests/proton/index/index_writer/index_writer_test.cpp index 75e6b01b46f..7a135e46937 100644 --- a/searchcore/src/tests/proton/index/index_writer/index_writer_test.cpp +++ b/searchcore/src/tests/proton/index/index_writer/index_writer_test.cpp @@ -1,10 +1,12 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#include <vespa/vespalib/testkit/testapp.h> - #include <vespa/searchcore/proton/index/index_writer.h> +#include <vespa/document/fieldvalue/document.h> #include <vespa/searchcore/proton/test/mock_index_manager.h> -#include <vespa/searchlib/index/docbuilder.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/vespalib/util/stringfmt.h> + #include <vespa/log/log.h> LOG_SETUP("index_writer_test"); @@ -12,6 +14,7 @@ using namespace proton; using namespace search; using namespace search::index; using namespace searchcorespi; +using search::test::DocBuilder; using vespalib::IDestructorCallback; using document::Document; @@ -27,7 +30,7 @@ toString(const std::vector<SerialNum> &vec) return oss.str(); } -struct MyIndexManager : public test::MockIndexManager +struct MyIndexManager : public proton::test::MockIndexManager { typedef std::map<uint32_t, std::vector<SerialNum> > LidMap; LidMap puts; @@ -80,21 +83,18 @@ struct Fixture IIndexManager::SP iim; MyIndexManager &mim; IndexWriter iw; - Schema schema; - DocBuilder builder; + DocBuilder builder; Document::UP dummyDoc; Fixture() : iim(new MyIndexManager()), mim(static_cast<MyIndexManager &>(*iim)), iw(iim), - schema(), - builder(schema), + builder(), dummyDoc(createDoc(1234)) // This content of this is not used { } Document::UP createDoc(uint32_t lid) { - builder.startDocument(vespalib::make_string("id:ns:searchdocument::%u", lid)); - return builder.endDocument(); + return builder.make_document(vespalib::make_string("id:ns:searchdocument::%u", lid)); } void put(SerialNum serialNum, const search::DocumentIdT lid) { iw.put(serialNum, *dummyDoc, lid, {}); diff --git a/searchcore/src/tests/proton/index/indexmanager_test.cpp b/searchcore/src/tests/proton/index/indexmanager_test.cpp index b427daa4ad1..68f1d4d0d0e 100644 --- a/searchcore/src/tests/proton/index/indexmanager_test.cpp +++ b/searchcore/src/tests/proton/index/indexmanager_test.cpp @@ -1,6 +1,10 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include <vespa/searchcore/proton/index/indexmanager.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> +#include <vespa/document/fieldvalue/document.h> #include <vespa/searchcore/proton/test/transport_helper.h> #include <vespa/searchcorespi/index/index_manager_stats.h> #include <vespa/searchcorespi/index/indexcollection.h> @@ -9,8 +13,9 @@ #include <vespa/vespalib/util/sequencedtaskexecutor.h> #include <vespa/searchlib/common/flush_token.h> #include <vespa/searchlib/common/serialnum.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/memoryindex/compact_words_store.h> #include <vespa/searchlib/memoryindex/document_inverter.h> #include <vespa/searchlib/memoryindex/document_inverter_context.h> @@ -34,6 +39,7 @@ LOG_SETUP("indexmanager_test"); using document::Document; using document::FieldValue; +using document::StringFieldValue; using proton::index::IndexConfig; using proton::index::IndexManager; using vespalib::SequencedTaskExecutor; @@ -42,7 +48,6 @@ using search::TuneFileAttributes; using search::TuneFileIndexManager; using search::TuneFileIndexing; using vespalib::datastore::EntryRef; -using search::index::DocBuilder; using search::index::DummyFileHeaderContext; using search::index::FieldLengthInfo; using search::index::Schema; @@ -51,6 +56,8 @@ using search::index::test::MockFieldLengthInspector; using search::memoryindex::CompactWordsStore; using search::memoryindex::FieldIndexCollection; using search::queryeval::Source; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using std::set; using std::string; using vespalib::makeLambdaTask; @@ -92,9 +99,9 @@ Document::UP buildDocument(DocBuilder &doc_builder, int id, const string &word) { vespalib::asciistream ost; ost << "id:ns:searchdocument::" << id; - doc_builder.startDocument(ost.str()); - doc_builder.startIndexField(field_name).addStr(word).endField(); - return doc_builder.endDocument(); + auto doc = doc_builder.make_document(ost.str()); + doc->setValue(field_name, StringFieldBuilder(doc_builder).word(word).build()); + return doc; } void push_documents_and_wait(search::memoryindex::DocumentInverter &inverter) { @@ -119,7 +126,7 @@ struct IndexManagerTest : public ::testing::Test { _service(1), _index_manager(), _schema(getSchema()), - _builder(_schema) + _builder([](auto& header) { header.addField(field_name, document::DataType::T_STRING); }) { removeTestData(); std::filesystem::create_directory(std::filesystem::path(index_dir)); diff --git a/searchcore/src/tests/proton/reference/gid_to_lid_mapper/gid_to_lid_mapper_test.cpp b/searchcore/src/tests/proton/reference/gid_to_lid_mapper/gid_to_lid_mapper_test.cpp index 2f4c26094c7..5152d09fae5 100644 --- a/searchcore/src/tests/proton/reference/gid_to_lid_mapper/gid_to_lid_mapper_test.cpp +++ b/searchcore/src/tests/proton/reference/gid_to_lid_mapper/gid_to_lid_mapper_test.cpp @@ -126,11 +126,11 @@ struct Fixture return std::make_shared<GidToLidMapperFactory>(_dmsContext); } - void assertGenerations(generation_t currentGeneration, generation_t firstUsedGeneration) + void assertGenerations(generation_t currentGeneration, generation_t oldest_used_generation) { const GenerationHandler &handler = _dms->getGenerationHandler(); EXPECT_EQUAL(currentGeneration, handler.getCurrentGeneration()); - EXPECT_EQUAL(firstUsedGeneration, handler.getFirstUsedGeneration()); + EXPECT_EQUAL(oldest_used_generation, handler.get_oldest_used_generation()); } template <typename Function> diff --git a/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/CMakeLists.txt b/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/CMakeLists.txt index f776734757d..e980ae817f1 100644 --- a/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/CMakeLists.txt +++ b/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/CMakeLists.txt @@ -4,5 +4,6 @@ vespa_add_executable(searchcore_document_reprocessing_handler_test_app TEST document_reprocessing_handler_test.cpp DEPENDS searchcore_reprocessing + searchlib_test ) vespa_add_test(NAME searchcore_document_reprocessing_handler_test_app COMMAND searchcore_document_reprocessing_handler_test_app) diff --git a/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/document_reprocessing_handler_test.cpp b/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/document_reprocessing_handler_test.cpp index da645f9a94b..0755a172945 100644 --- a/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/document_reprocessing_handler_test.cpp +++ b/searchcore/src/tests/proton/reprocessing/document_reprocessing_handler/document_reprocessing_handler_test.cpp @@ -3,12 +3,12 @@ LOG_SETUP("document_reprocessing_handler_test"); #include <vespa/searchcore/proton/reprocessing/document_reprocessing_handler.h> -#include <vespa/searchlib/index/docbuilder.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/vespalib/testkit/testapp.h> using namespace document; using namespace proton; -using namespace search::index; +using search::test::DocBuilder; template <typename ReprocessingType> struct MyProcessor : public ReprocessingType @@ -36,13 +36,13 @@ struct FixtureBase FixtureBase(uint32_t docIdLimit); ~FixtureBase(); std::shared_ptr<Document> createDoc() { - return _docBuilder.startDocument(DOC_ID).endDocument(); + return _docBuilder.make_document(DOC_ID); } }; FixtureBase::FixtureBase(uint32_t docIdLimit) : _handler(docIdLimit), - _docBuilder(Schema()) + _docBuilder() { } FixtureBase::~FixtureBase() {} diff --git a/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.cpp b/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.cpp index fb99ca51e3a..3d6d522dd44 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.cpp +++ b/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.cpp @@ -30,7 +30,7 @@ public: DocumentMetaStore::SP documentMetaStore, InitializedAttributesResult &result) : _initializer(std::move(initializer)), - _documentMetaStore(documentMetaStore), + _documentMetaStore(std::move(documentMetaStore)), _result(result) {} @@ -74,8 +74,8 @@ AttributeManagerInitializerTask::AttributeManagerInitializerTask(std::promise<vo InitializedAttributesResult &attributesResult) : _promise(std::move(promise)), _configSerialNum(configSerialNum), - _documentMetaStore(documentMetaStore), - _attrMgr(attrMgr), + _documentMetaStore(std::move(documentMetaStore)), + _attrMgr(std::move(attrMgr)), _attributesResult(attributesResult) { } @@ -104,7 +104,7 @@ public: InitializerTask::SP documentMetaStoreInitTask, DocumentMetaStore::SP documentMetaStore, InitializedAttributesResult &attributesResult); - ~AttributeInitializerTasksBuilder(); + ~AttributeInitializerTasksBuilder() override; void add(AttributeInitializer::UP initializer) override; }; @@ -113,8 +113,8 @@ AttributeInitializerTasksBuilder::AttributeInitializerTasksBuilder(InitializerTa DocumentMetaStore::SP documentMetaStore, InitializedAttributesResult &attributesResult) : _attrMgrInitTask(attrMgrInitTask), - _documentMetaStoreInitTask(documentMetaStoreInitTask), - _documentMetaStore(documentMetaStore), + _documentMetaStoreInitTask(std::move(documentMetaStoreInitTask)), + _documentMetaStore(std::move(documentMetaStore)), _attributesResult(attributesResult) { } @@ -143,7 +143,7 @@ AttributeManagerInitializer::createAttributeSpec() const AttributeManagerInitializer::AttributeManagerInitializer(SerialNum configSerialNum, initializer::InitializerTask::SP documentMetaStoreInitTask, DocumentMetaStore::SP documentMetaStore, - AttributeManager::SP baseAttrMgr, + const AttributeManager & baseAttrMgr, const AttributesConfig &attrCfg, const AllocStrategy& alloc_strategy, bool fastAccessAttributesOnly, @@ -157,12 +157,12 @@ AttributeManagerInitializer::AttributeManagerInitializer(SerialNum configSerialN _fastAccessAttributesOnly(fastAccessAttributesOnly), _master(master), _attributesResult(), - _attrMgrResult(attrMgrResult) + _attrMgrResult(std::move(attrMgrResult)) { addDependency(documentMetaStoreInitTask); - AttributeInitializerTasksBuilder tasksBuilder(*this, documentMetaStoreInitTask, documentMetaStore, _attributesResult); + AttributeInitializerTasksBuilder tasksBuilder(*this, std::move(documentMetaStoreInitTask), std::move(documentMetaStore), _attributesResult); std::unique_ptr<AttributeCollectionSpec> attrSpec = createAttributeSpec(); - _attrMgr = std::make_shared<AttributeManager>(*baseAttrMgr, std::move(*attrSpec), tasksBuilder); + _attrMgr = std::make_shared<AttributeManager>(baseAttrMgr, std::move(*attrSpec), tasksBuilder); } AttributeManagerInitializer::~AttributeManagerInitializer() = default; diff --git a/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.h b/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.h index 99b6b026f8f..79203696ea1 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.h +++ b/searchcore/src/vespa/searchcore/proton/attribute/attribute_manager_initializer.h @@ -10,7 +10,7 @@ #include <vespa/searchlib/common/serialnum.h> #include <vespa/config-attributes.h> -namespace searchcorespi { namespace index { struct IThreadService; } } +namespace searchcorespi::index { struct IThreadService; } namespace proton { @@ -36,7 +36,7 @@ public: AttributeManagerInitializer(search::SerialNum configSerialNum, initializer::InitializerTask::SP documentMetaStoreInitTask, DocumentMetaStore::SP documentMetaStore, - AttributeManager::SP baseAttrMgr, + const AttributeManager & baseAttrMgr, const vespa::config::search::AttributesConfig &attrCfg, const AllocStrategy& alloc_strategy, bool fastAccessAttributesOnly, diff --git a/searchcore/src/vespa/searchcore/proton/attribute/attribute_vector_explorer.cpp b/searchcore/src/vespa/searchcore/proton/attribute/attribute_vector_explorer.cpp index eeb8cafb859..c153f873480 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/attribute_vector_explorer.cpp +++ b/searchcore/src/vespa/searchcore/proton/attribute/attribute_vector_explorer.cpp @@ -47,7 +47,7 @@ convertStatusToSlime(const Status &status, Cursor &object) void convertGenerationToSlime(const AttributeVector &attr, Cursor &object) { - object.setLong("firstUsed", attr.getFirstUsedGeneration()); + object.setLong("oldest_used", attr.get_oldest_used_generation()); object.setLong("current", attr.getCurrentGeneration()); } diff --git a/searchcore/src/vespa/searchcore/proton/attribute/attribute_writer.cpp b/searchcore/src/vespa/searchcore/proton/attribute/attribute_writer.cpp index 69ef42ef9a8..418615058ce 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/attribute_writer.cpp +++ b/searchcore/src/vespa/searchcore/proton/attribute/attribute_writer.cpp @@ -239,7 +239,7 @@ applyReplayDone(uint32_t docIdLimit, AttributeVector &attr) void applyHeartBeat(SerialNum serialNum, AttributeVector &attr) { - attr.removeAllOldGenerations(); + attr.reclaim_unused_memory(); if (attr.getStatus().getLastSyncToken() <= serialNum) { attr.commit(search::CommitParam(serialNum)); } diff --git a/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.cpp b/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.cpp index be8bcd8e66a..939ae196de8 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.cpp +++ b/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.cpp @@ -79,8 +79,8 @@ allocShrinker(const AttributeVector::SP &attr, vespalib::ISequencedTaskExecutor } -AttributeManager::AttributeWrap::AttributeWrap(const AttributeVectorSP & a, bool isExtra_) - : _attr(a), +AttributeManager::AttributeWrap::AttributeWrap(AttributeVectorSP a, bool isExtra_) + : _attr(std::move(a)), _isExtra(isExtra_) { } @@ -94,15 +94,15 @@ AttributeManager::AttributeWrap::AttributeWrap() AttributeManager::AttributeWrap::~AttributeWrap() = default; AttributeManager::AttributeWrap -AttributeManager::AttributeWrap::extraAttribute(const AttributeVectorSP &a) +AttributeManager::AttributeWrap::extraAttribute(AttributeVectorSP a) { - return AttributeWrap(a, true); + return {std::move(a), true}; } AttributeManager::AttributeWrap -AttributeManager::AttributeWrap::normalAttribute(const AttributeVectorSP &a) +AttributeManager::AttributeWrap::normalAttribute(AttributeVectorSP a) { - return AttributeWrap(a, false); + return {std::move(a), false}; } AttributeManager::FlushableWrap::FlushableWrap() @@ -136,18 +136,20 @@ AttributeManager::internalAddAttribute(AttributeSpec && spec, } void -AttributeManager::addAttribute(const AttributeWrap &attribute, const ShrinkerSP &shrinker) -{ - LOG(debug, "Adding attribute vector '%s'", attribute.getAttribute()->getName().c_str()); - _attributes[attribute.getAttribute()->getName()] = attribute; - assert(attribute.getAttribute()->getInterlock() == _interlock); - if ( ! attribute.isExtra() ) { +AttributeManager::addAttribute(AttributeWrap attributeWrap, const ShrinkerSP &shrinker) +{ + AttributeVector::SP attribute = attributeWrap.getAttribute(); + bool isExtra = attributeWrap.isExtra(); + const vespalib::string &name = attribute->getName(); + LOG(debug, "Adding attribute vector '%s'", name.c_str()); + _attributes[name] = std::move(attributeWrap); + assert(attribute->getInterlock() == _interlock); + if ( ! isExtra ) { // Flushing of extra attributes is handled elsewhere - auto attr = attribute.getAttribute(); - const vespalib::string &name = attr->getName(); - auto flusher = std::make_shared<FlushableAttribute>(attr, _diskLayout->createAttributeDir(name), _tuneFileAttributes, _fileHeaderContext, _attributeFieldWriter, _hwInfo); - _flushables[attribute.getAttribute()->getName()] = FlushableWrap(flusher, shrinker); - _writableAttributes.push_back(attribute.getAttribute().get()); + AttributeVector * attributeP = attribute.get(); + auto flusher = std::make_shared<FlushableAttribute>(std::move(attribute), _diskLayout->createAttributeDir(name), _tuneFileAttributes, _fileHeaderContext, _attributeFieldWriter, _hwInfo); + _flushables[name] = FlushableWrap(flusher, shrinker); + _writableAttributes.push_back(attributeP); } } @@ -234,7 +236,7 @@ AttributeManager::transferExtraAttributes(const AttributeManager &currMgr) { for (const auto &kv : currMgr._attributes) { if (kv.second.isExtra()) { - addAttribute(kv.second, 0); + addAttribute(kv.second, nullptr); } } } @@ -271,7 +273,7 @@ AttributeManager::AttributeManager(const vespalib::string &baseDir, std::shared_ptr<search::attribute::Interlock> interlock, vespalib::ISequencedTaskExecutor &attributeFieldWriter, vespalib::Executor& shared_executor, - const IAttributeFactory::SP &factory, + IAttributeFactory::SP factory, const HwInfo &hwInfo) : proton::IAttributeManager(), _attributes(), @@ -281,7 +283,7 @@ AttributeManager::AttributeManager(const vespalib::string &baseDir, _documentSubDbName(documentSubDbName), _tuneFileAttributes(tuneFileAttributes), _fileHeaderContext(fileHeaderContext), - _factory(factory), + _factory(std::move(factory)), _interlock(std::move(interlock)), _attributeFieldWriter(attributeFieldWriter), _shared_executor(shared_executor), @@ -329,7 +331,7 @@ AttributeManager::addInitializedAttributes(const std::vector<AttributeInitialize auto attr = result.getAttribute(); attr->setInterlock(_interlock); auto shrinker = allocShrinker(attr, _attributeFieldWriter, *_diskLayout); - addAttribute(AttributeWrap::normalAttribute(attr), shrinker); + addAttribute(AttributeWrap::normalAttribute(std::move(attr)), shrinker); } } @@ -414,7 +416,7 @@ AttributeManager::getAttributeReadGuard(const string &name, bool stableEnumGuard if (attribute) { return attribute->makeReadGuard(stableEnumGuard); } else { - return std::unique_ptr<search::attribute::AttributeReadGuard>(); + return {}; } } @@ -424,7 +426,7 @@ AttributeManager::getAttributeList(std::vector<AttributeGuard> &list) const list.reserve(_attributes.size()); for (const auto &kv : _attributes) { if (!kv.second.isExtra()) { - list.push_back(AttributeGuard(kv.second.getAttribute())); + list.emplace_back(kv.second.getAttribute()); } } } @@ -541,7 +543,7 @@ AttributeManager::getAttributeListAll(std::vector<AttributeGuard> &list) const { list.reserve(_attributes.size()); for (const auto &kv : _attributes) { - list.push_back(AttributeGuard(kv.second.getAttribute())); + list.emplace_back(kv.second.getAttribute()); } } @@ -629,7 +631,7 @@ AttributeManager::getExclusiveReadAccessor(const vespalib::string &name) const if (attribute) { return std::make_unique<ExclusiveAttributeReadAccessor>(attribute, _attributeFieldWriter); } - return ExclusiveAttributeReadAccessor::UP(); + return {}; } void diff --git a/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.h b/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.h index 2d5b75e71f4..b74e7e72a0e 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.h +++ b/searchcore/src/vespa/searchcore/proton/attribute/attributemanager.h @@ -45,12 +45,16 @@ private: private: AttributeVectorSP _attr; bool _isExtra; - AttributeWrap(const AttributeVectorSP & a, bool isExtra_); + AttributeWrap(AttributeVectorSP a, bool isExtra_); public: AttributeWrap(); + AttributeWrap(const AttributeWrap &) = default; + AttributeWrap & operator=(const AttributeWrap &) = delete; + AttributeWrap(AttributeWrap &&) noexcept = default; + AttributeWrap & operator=(AttributeWrap &&) noexcept = default; ~AttributeWrap(); - static AttributeWrap extraAttribute(const AttributeVectorSP &a); - static AttributeWrap normalAttribute(const AttributeVectorSP &a); + static AttributeWrap extraAttribute(AttributeVectorSP a); + static AttributeWrap normalAttribute(AttributeVectorSP a); bool isExtra() const { return _isExtra; } const AttributeVectorSP & getAttribute() const { return _attr; } }; @@ -85,20 +89,12 @@ private: std::unique_ptr<ImportedAttributesRepo> _importedAttributes; AttributeVectorSP internalAddAttribute(AttributeSpec && spec, uint64_t serialNum, const IAttributeFactory &factory); - - void addAttribute(const AttributeWrap &attribute, const ShrinkerSP &shrinker); - + void addAttribute(AttributeWrap attribute, const ShrinkerSP &shrinker); AttributeVectorSP findAttribute(const vespalib::string &name) const; - const FlushableWrap *findFlushable(const vespalib::string &name) const; - Spec::AttributeList transferExistingAttributes(const AttributeManager &currMgr, Spec::AttributeList && newAttributes); - - void addNewAttributes(const Spec &newSpec, Spec::AttributeList && toBeAdded, - IAttributeInitializerRegistry &initializerRegistry); - + void addNewAttributes(const Spec &newSpec, Spec::AttributeList && toBeAdded, IAttributeInitializerRegistry &initializerRegistry); void transferExtraAttributes(const AttributeManager &currMgr); - public: using SP = std::shared_ptr<AttributeManager>; @@ -118,7 +114,7 @@ public: std::shared_ptr<search::attribute::Interlock> interlock, vespalib::ISequencedTaskExecutor &attributeFieldWriter, vespalib::Executor& shared_executor, - const IAttributeFactory::SP &factory, + IAttributeFactory::SP factory, const HwInfo &hwInfo); AttributeManager(const AttributeManager &currMgr, Spec && newSpec, diff --git a/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.cpp b/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.cpp index 93663637e75..f1b7eac3712 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.cpp +++ b/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.cpp @@ -146,7 +146,7 @@ FlushableAttribute::Flusher::run() } } -FlushableAttribute::FlushableAttribute(const AttributeVectorSP attr, +FlushableAttribute::FlushableAttribute(AttributeVectorSP attr, const std::shared_ptr<AttributeDirectory> &attrDir, const TuneFileAttributes & tuneFileAttributes, @@ -207,7 +207,7 @@ IFlushTarget::Task::UP FlushableAttribute::internalInitFlush(SerialNum currentSerial) { // Called by attribute field writer thread while document db executor waits - _attr->removeAllOldGenerations(); + _attr->reclaim_unused_memory(); SerialNum syncToken = std::max(currentSerial, _attr->getStatus().getLastSyncToken()); auto writer = _attrDir->tryGetWriter(); if (!writer) { diff --git a/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.h b/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.h index e1054a19b6c..39d79372f25 100644 --- a/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.h +++ b/searchcore/src/vespa/searchcore/proton/attribute/flushableattribute.h @@ -51,7 +51,7 @@ public: * * fileHeaderContext must be kept alive by caller. **/ - FlushableAttribute(const AttributeVectorSP attr, + FlushableAttribute(AttributeVectorSP attr, const std::shared_ptr<AttributeDirectory> &attrDir, const search::TuneFileAttributes &tuneFileAttributes, const search::common::FileHeaderContext & diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/document_meta_store_adapter.h b/searchcore/src/vespa/searchcore/proton/documentmetastore/document_meta_store_adapter.h index 843a9eaaeaa..a4f744df6f9 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/document_meta_store_adapter.h +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/document_meta_store_adapter.h @@ -24,7 +24,7 @@ public: DocId getCommittedDocIdLimit() const override { return doGetCommittedDocIdLimit(); } - void removeAllOldGenerations() override { + void reclaim_unused_memory() override { doRemoveAllOldGenerations(); } uint64_t getCurrentGeneration() const override { diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp index b1b5f45a8fa..77452b60f21 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.cpp @@ -72,17 +72,8 @@ private: uint32_t _version; public: - Reader(std::unique_ptr<FastOS_FileInterface> datFile) - : _datFile(std::move(datFile)), - _lidReader(&_datFile.file()), - _gidReader(&_datFile.file()), - _bucketUsedBitsReader(&_datFile.file()), - _timestampReader(&_datFile.file()), - _docIdLimit(0) - { - _docIdLimit = _datFile.header().getTag(DOCID_LIMIT).asInteger(); - _version = _datFile.header().getTag(VERSION).asInteger(); - } + explicit Reader(std::unique_ptr<FastOS_FileInterface> datFile); + ~Reader(); uint32_t getDocIdLimit() const { return _docIdLimit; } @@ -126,6 +117,19 @@ public: } }; +Reader::Reader(std::unique_ptr<FastOS_FileInterface> datFile) + : _datFile(std::move(datFile)), + _lidReader(&_datFile.file()), + _gidReader(&_datFile.file()), + _bucketUsedBitsReader(&_datFile.file()), + _timestampReader(&_datFile.file()), + _docIdLimit(0) +{ + _docIdLimit = _datFile.header().getTag(DOCID_LIMIT).asInteger(); + _version = _datFile.header().getTag(VERSION).asInteger(); +} +Reader::~Reader() = default; + } namespace { @@ -134,12 +138,12 @@ class ShrinkBlockHeld : public GenerationHeldBase DocumentMetaStore &_dms; public: - ShrinkBlockHeld(DocumentMetaStore &dms) + explicit ShrinkBlockHeld(DocumentMetaStore &dms) : GenerationHeldBase(0), _dms(dms) { } - ~ShrinkBlockHeld() { + ~ShrinkBlockHeld() override { _dms.unblockShrinkLidSpace(); } }; @@ -224,7 +228,7 @@ DocumentMetaStore::onUpdateStat() { auto &compaction_strategy = getConfig().getCompactionStrategy(); vespalib::MemoryUsage usage = _metaDataStore.getMemoryUsage(); - usage.incAllocatedBytesOnHold(getGenerationHolder().getHeldBytes()); + usage.incAllocatedBytesOnHold(getGenerationHolder().get_held_bytes()); size_t bvSize = _lidAlloc.getUsedLidsSize(); usage.incAllocatedBytes(bvSize); usage.incUsedBytes(bvSize); @@ -241,20 +245,20 @@ DocumentMetaStore::onUpdateStat() } void -DocumentMetaStore::onGenerationChange(generation_t generation) +DocumentMetaStore::before_inc_generation(generation_t current_gen) { _gidToLidMap.getAllocator().freeze(); - _gidToLidMap.getAllocator().transferHoldLists(generation - 1); - getGenerationHolder().transferHoldLists(generation - 1); + _gidToLidMap.getAllocator().assign_generation(current_gen); + getGenerationHolder().assign_generation(current_gen); updateStat(false); } void -DocumentMetaStore::removeOldGenerations(generation_t firstUsed) +DocumentMetaStore::reclaim_memory(generation_t oldest_used_gen) { - _gidToLidMap.getAllocator().trimHoldLists(firstUsed); - _lidAlloc.trimHoldLists(firstUsed); - getGenerationHolder().trimHoldLists(firstUsed); + _gidToLidMap.getAllocator().reclaim_memory(oldest_used_gen); + _lidAlloc.reclaim_memory(oldest_used_gen); + getGenerationHolder().reclaim(oldest_used_gen); } std::unique_ptr<search::AttributeSaver> @@ -318,7 +322,7 @@ DocumentMetaStore::onLoad(vespalib::Executor *) _gidToLidMap.assign(treeBuilder); _gidToLidMap.getAllocator().freeze(); // create initial frozen tree generation_t generation = getGenerationHandler().getCurrentGeneration(); - _gidToLidMap.getAllocator().transferHoldLists(generation); + _gidToLidMap.getAllocator().assign_generation(generation); setNumDocs(_metaDataStore.size()); setCommittedDocIdLimit(_metaDataStore.size()); @@ -433,7 +437,7 @@ DocumentMetaStore::DocumentMetaStore(BucketDBOwnerSP bucketDB, setCommittedDocIdLimit(1u); // lid 0 is reserved _gidToLidMap.getAllocator().freeze(); // create initial frozen tree generation_t generation = getGenerationHandler().getCurrentGeneration(); - _gidToLidMap.getAllocator().transferHoldLists(generation); + _gidToLidMap.getAllocator().assign_generation(generation); updateStat(true); } @@ -442,7 +446,7 @@ DocumentMetaStore::~DocumentMetaStore() // TODO: Properly notify about modified buckets when using shared bucket db // between document types unload(); - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); assert(get_shrink_lid_space_blockers() == 0); } @@ -748,12 +752,12 @@ DocumentMetaStore::getMetaData(const GlobalId &gid) const { DocId lid = 0; if (!getLid(gid, lid) || !validLid(lid)) { - return search::DocumentMetaData(); + return {}; } const RawDocumentMetaData &raw = getRawMetaData(lid); Timestamp timestamp(raw.getTimestamp()); std::atomic_thread_fence(std::memory_order_acquire); - return search::DocumentMetaData(lid, timestamp, raw.getBucketId(), raw.getGid(), _subDbType == SubDbType::REMOVED); + return {lid, timestamp, raw.getBucketId(), raw.getGid(), _subDbType == SubDbType::REMOVED}; } void @@ -779,14 +783,7 @@ DocumentMetaStore::getMetaData(const BucketId &bucketId, LidUsageStats DocumentMetaStore::getLidUsageStats() const { - uint32_t docIdLimit = getCommittedDocIdLimit(); - uint32_t numDocs = getNumUsedLids(); - uint32_t lowestFreeLid = _lidAlloc.getLowestFreeLid(); - uint32_t highestUsedLid = _lidAlloc.getHighestUsedLid(); - return LidUsageStats(docIdLimit, - numDocs, - lowestFreeLid, - highestUsedLid); + return {getCommittedDocIdLimit(), getNumUsedLids(), _lidAlloc.getLowestFreeLid(), _lidAlloc.getHighestUsedLid()}; } Blueprint::UP @@ -1009,7 +1006,7 @@ DocumentMetaStore::holdUnblockShrinkLidSpace() { assert(get_shrink_lid_space_blockers() > 0); auto hold = std::make_unique<ShrinkBlockHeld>(*this); - getGenerationHolder().hold(std::move(hold)); + getGenerationHolder().insert(std::move(hold)); incGeneration(); } @@ -1064,7 +1061,7 @@ DocumentMetaStore::getBucketOf(const vespalib::GenerationHandler::Guard &, uint3 if (__builtin_expect(validLidFast(lid, getCommittedDocIdLimit()), true)) { return getRawMetaData(lid).getBucketId(); } - return BucketId(); + return {}; } vespalib::GenerationHandler::Guard @@ -1094,6 +1091,26 @@ DocumentMetaStore::foreach(const search::IGidToLidMapperVisitor &visitor) const { visitor.visit(getRawMetaData(key.get_lid()).getGid(), key.get_lid()); }); } +long +DocumentMetaStore::onSerializeForAscendingSort(DocId lid, void * serTo, long available, const search::common::BlobConverter *) const { + if ( ! validLid(lid)) return 0; + if (available < document::GlobalId::LENGTH) return -1; + memcpy(serTo, getRawMetaData(lid).getGid().get(), document::GlobalId::LENGTH); + return document::GlobalId::LENGTH; +} + +long +DocumentMetaStore::onSerializeForDescendingSort(DocId lid, void * serTo, long available, const search::common::BlobConverter *) const { + if ( ! validLid(lid)) return 0; + if (available < document::GlobalId::LENGTH) return -1; + const auto * src(static_cast<const uint8_t *>(getRawMetaData(lid).getGid().get())); + auto * dst = static_cast<uint8_t *>(serTo); + for (size_t i(0); i < document::GlobalId::LENGTH; ++i) { + dst[i] = 0xff - src[i]; + } + return document::GlobalId::LENGTH; +} + } // namespace proton namespace vespalib::btree { diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.h b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.h index c4010a07709..ba944e3493d 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.h +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastore.h @@ -51,7 +51,7 @@ public: // the ones with the same signature in proton::IDocumentMetaStore. using DocumentMetaStoreAttribute::commit; using DocumentMetaStoreAttribute::getCommittedDocIdLimit; - using DocumentMetaStoreAttribute::removeAllOldGenerations; + using DocumentMetaStoreAttribute::reclaim_unused_memory; using DocumentMetaStoreAttribute::getCurrentGeneration; private: @@ -93,8 +93,8 @@ private: void onUpdateStat() override; // Implements AttributeVector - void onGenerationChange(generation_t generation) override; - void removeOldGenerations(generation_t firstUsed) override; + void before_inc_generation(generation_t current_gen) override; + void reclaim_memory(generation_t oldest_used_gen) override; std::unique_ptr<search::AttributeSaver> onInitSave(vespalib::stringref fileName) override; bool onLoad(vespalib::Executor *executor) override; @@ -122,7 +122,7 @@ private: return getCommittedDocIdLimit(); } void doRemoveAllOldGenerations() override { - removeAllOldGenerations(); + reclaim_unused_memory(); } uint64_t doGetCurrentGeneration() const override { return getCurrentGeneration(); @@ -272,6 +272,8 @@ public: uint32_t getVersion() const override; void setTrackDocumentSizes(bool trackDocumentSizes) { _trackDocumentSizes = trackDocumentSizes; } void foreach(const search::IGidToLidMapperVisitor &visitor) const override; + long onSerializeForAscendingSort(DocId, void *, long, const search::common::BlobConverter *) const override; + long onSerializeForDescendingSort(DocId, void *, long, const search::common::BlobConverter *) const override; }; } diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.cpp b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.cpp index 57c3159645b..5deeaf924ae 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.cpp +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.cpp @@ -7,14 +7,14 @@ namespace proton { namespace { -const vespalib::string _G_documentMetaStoreName("[documentmetastore]"); +const vespalib::string documentMetaStoreName("[documentmetastore]"); } const vespalib::string & DocumentMetaStoreAttribute::getFixedName() { - return _G_documentMetaStoreName; + return documentMetaStoreName; } DocumentMetaStoreAttribute::DocumentMetaStoreAttribute(const vespalib::string &name) diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.h b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.h index 1a5f9c0077e..36408a01c17 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.h +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.h @@ -14,12 +14,11 @@ namespace proton { class DocumentMetaStoreAttribute : public search::NotImplementedAttribute { public: - DocumentMetaStoreAttribute(const vespalib::string &name=getFixedName()); + explicit DocumentMetaStoreAttribute(const vespalib::string &name); ~DocumentMetaStoreAttribute() override; static const vespalib::string &getFixedName(); - // Implements IAttributeVector size_t getFixedWidth() const override { return document::GlobalId::LENGTH; } diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreflushtarget.cpp b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreflushtarget.cpp index 7786ead49b2..609ee585a6c 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreflushtarget.cpp +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/documentmetastoreflushtarget.cpp @@ -203,7 +203,7 @@ IFlushTarget::Task::UP DocumentMetaStoreFlushTarget::initFlush(SerialNum currentSerial, std::shared_ptr<search::IFlushToken>) { // Called by document db executor - _dms->removeAllOldGenerations(); + _dms->reclaim_unused_memory(); SerialNum syncToken = std::max(currentSerial, _dms->getStatus().getLastSyncToken()); auto writer = _dmsDir->tryGetWriter(); if (!writer) { diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/i_document_meta_store.h b/searchcore/src/vespa/searchcore/proton/documentmetastore/i_document_meta_store.h index a03b6325797..942d3e21da5 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/i_document_meta_store.h +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/i_document_meta_store.h @@ -68,7 +68,7 @@ struct IDocumentMetaStore : public search::IDocumentMetaStore, // Functions that are also defined search::AttributeVector virtual void commit(const CommitParam & param) = 0; - virtual void removeAllOldGenerations() = 0; + virtual void reclaim_unused_memory() = 0; virtual bool canShrinkLidSpace() const = 0; virtual SerialNum getLastSerialNum() const = 0; diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_allocator.h b/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_allocator.h index 6118701b0dc..95a8cf85279 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_allocator.h +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_allocator.h @@ -41,8 +41,8 @@ public: void unregisterLid(DocId lid); void unregister_lids(const std::vector<DocId>& lids); size_t getUsedLidsSize() const { return _usedLids.byteSize(); } - void trimHoldLists(generation_t firstUsed) { - _holdLids.trimHoldLists(firstUsed, _freeLids); + void reclaim_memory(generation_t oldest_used_gen) { + _holdLids.reclaim_memory(oldest_used_gen, _freeLids); } void moveLidBegin(DocId fromLid, DocId toLid); void moveLidEnd(DocId fromLid, DocId toLid); diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.cpp b/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.cpp index 7157a40c5d5..ef0a244fc37 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.cpp +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.cpp @@ -23,9 +23,9 @@ LidHoldList::clear() { } void -LidHoldList::trimHoldLists(generation_t firstUsed, LidStateVector &freeLids) +LidHoldList::reclaim_memory(generation_t oldest_used_gen, LidStateVector &freeLids) { - while (!_holdList.empty() && _holdList.front().second < firstUsed) { + while (!_holdList.empty() && _holdList.front().second < oldest_used_gen) { uint32_t lid = _holdList.front().first; freeLids.setBit(lid); _holdList.pop_front(); diff --git a/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.h b/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.h index fc32fcb7510..565d8bf25e1 100644 --- a/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.h +++ b/searchcore/src/vespa/searchcore/proton/documentmetastore/lid_hold_list.h @@ -43,9 +43,9 @@ public: void clear(); /** - * Frees up elements with generation < first used generation for reuse. + * Frees up elements with generation < oldest used generation for reuse. **/ - void trimHoldLists(generation_t firstUsed, LidStateVector &freeLids); + void reclaim_memory(generation_t oldest_used_gen, LidStateVector &freeLids); }; diff --git a/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp b/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp index f332ca5ec26..9b85ddbdde8 100644 --- a/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp +++ b/searchcore/src/vespa/searchcore/proton/matching/result_processor.cpp @@ -3,6 +3,7 @@ #include "result_processor.h" #include "partial_result.h" #include "sessionmanager.h" +#include <vespa/searchcore/proton/documentmetastore/documentmetastoreattribute.h> #include <vespa/searchcore/grouping/groupingmanager.h> #include <vespa/searchcore/grouping/groupingcontext.h> #include <vespa/searchlib/uca/ucaconverter.h> @@ -28,7 +29,7 @@ ResultProcessor::Result::~Result() = default; ResultProcessor::Sort::Sort(uint32_t partitionId, const vespalib::Doom & doom, IAttributeContext &ac, const vespalib::string &ss) : sorter(FastS_DefaultResultSorter::instance()), _ucaFactory(std::make_unique<search::uca::UcaConverterFactory>()), - sortSpec(partitionId, doom, *_ucaFactory) + sortSpec(DocumentMetaStoreAttribute::getFixedName(), partitionId, doom, *_ucaFactory) { if (!ss.empty() && sortSpec.Init(ss.c_str(), ac)) { sorter = &sortSpec; @@ -46,9 +47,9 @@ ResultProcessor::Context::~Context() = default; void ResultProcessor::GroupingSource::merge(Source &s) { - GroupingSource &rhs = static_cast<GroupingSource&>(s); - assert((ctx == 0) == (rhs.ctx == 0)); - if (ctx != 0) { + auto &rhs = dynamic_cast<GroupingSource&>(s); + assert((ctx == nullptr) == (rhs.ctx == nullptr)); + if (ctx != nullptr) { search::grouping::GroupingManager man(*ctx); man.merge(*rhs.ctx); } @@ -112,7 +113,7 @@ ResultProcessor::extract_docid_ordering(const PartialResult &result) const } std::sort(list.begin(), list.end(), [](const auto &a, const auto &b){ return (a.first < b.first); }); return list; -}; +} ResultProcessor::Result::UP ResultProcessor::makeReply(PartialResultUP full_result) diff --git a/searchcore/src/vespa/searchcore/proton/server/fast_access_doc_subdb.cpp b/searchcore/src/vespa/searchcore/proton/server/fast_access_doc_subdb.cpp index 3ec1e23693e..ebe20f24d92 100644 --- a/searchcore/src/vespa/searchcore/proton/server/fast_access_doc_subdb.cpp +++ b/searchcore/src/vespa/searchcore/proton/server/fast_access_doc_subdb.cpp @@ -79,7 +79,7 @@ FastAccessDocSubDB::createAttributeManagerInitializer(const DocumentDBConfig &co return std::make_shared<AttributeManagerInitializer>(configSerialNum, documentMetaStoreInitTask, documentMetaStore, - baseAttrMgr, + *baseAttrMgr, (_hasAttributes ? configSnapshot.getAttributesConfig() : AttributesConfig()), alloc_strategy, _fastAccessAttributesOnly, diff --git a/searchcore/src/vespa/searchcore/proton/server/storeonlyfeedview.cpp b/searchcore/src/vespa/searchcore/proton/server/storeonlyfeedview.cpp index a9850b5c2b7..533b270c20a 100644 --- a/searchcore/src/vespa/searchcore/proton/server/storeonlyfeedview.cpp +++ b/searchcore/src/vespa/searchcore/proton/server/storeonlyfeedview.cpp @@ -763,7 +763,7 @@ void StoreOnlyFeedView::heartBeat(SerialNum serialNum, DoneCallback onDone) { assert(_writeService.master().isCurrentThread()); - _metaStore.removeAllOldGenerations(); + _metaStore.reclaim_unused_memory(); _metaStore.commit(CommitParam(serialNum)); heartBeatSummary(serialNum, onDone); heartBeatIndexedFields(serialNum, onDone); diff --git a/searchcore/src/vespa/searchcore/proton/test/CMakeLists.txt b/searchcore/src/vespa/searchcore/proton/test/CMakeLists.txt index 25570c4ad0c..ad86ce64d8e 100644 --- a/searchcore/src/vespa/searchcore/proton/test/CMakeLists.txt +++ b/searchcore/src/vespa/searchcore/proton/test/CMakeLists.txt @@ -20,4 +20,5 @@ vespa_add_library(searchcore_test STATIC searchcore_server searchcore_fconfig searchcorespi + searchlib_test ) diff --git a/searchcore/src/vespa/searchcore/proton/test/document_meta_store_observer.h b/searchcore/src/vespa/searchcore/proton/test/document_meta_store_observer.h index 60682ae90e5..4092f548016 100644 --- a/searchcore/src/vespa/searchcore/proton/test/document_meta_store_observer.h +++ b/searchcore/src/vespa/searchcore/proton/test/document_meta_store_observer.h @@ -162,8 +162,8 @@ struct DocumentMetaStoreObserver : public IDocumentMetaStore DocId getCommittedDocIdLimit() const override { return _store.getCommittedDocIdLimit(); } - void removeAllOldGenerations() override { - _store.removeAllOldGenerations(); + void reclaim_unused_memory() override { + _store.reclaim_unused_memory(); } bool canShrinkLidSpace() const override { return _store.canShrinkLidSpace(); diff --git a/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.cpp b/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.cpp index 2cdf1c45485..f9f98705144 100644 --- a/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.cpp +++ b/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.cpp @@ -5,8 +5,7 @@ namespace proton::test { UserDocumentsBuilder::UserDocumentsBuilder() - : _schema(), - _builder(_schema), + : _builder(), _docs() { } @@ -17,7 +16,7 @@ UserDocumentsBuilder & UserDocumentsBuilder::createDoc(uint32_t userId, search::DocumentIdT lid) { vespalib::string docId = vespalib::make_string("id:test:searchdocument:n=%u:%u", userId, lid); - document::Document::SP doc(_builder.startDocument(docId).endDocument().release()); + document::Document::SP doc(_builder.make_document(docId)); _docs.addDoc(userId, Document(doc, lid, storage::spi::Timestamp(lid))); return *this; } diff --git a/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.h b/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.h index f05b6da11de..1c7a82ef5ba 100644 --- a/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.h +++ b/searchcore/src/vespa/searchcore/proton/test/userdocumentsbuilder.h @@ -2,7 +2,7 @@ #pragma once #include "userdocuments.h" -#include <vespa/searchlib/index/docbuilder.h> +#include <vespa/searchlib/test/doc_builder.h> #include <vespa/vespalib/util/stringfmt.h> namespace proton::test { @@ -13,14 +13,13 @@ namespace proton::test { class UserDocumentsBuilder { private: - search::index::Schema _schema; - search::index::DocBuilder _builder; + search::test::DocBuilder _builder; UserDocuments _docs; public: UserDocumentsBuilder(); ~UserDocumentsBuilder(); - const std::shared_ptr<const document::DocumentTypeRepo> &getRepo() const { - return _builder.getDocumentTypeRepo(); + std::shared_ptr<const document::DocumentTypeRepo> getRepo() const { + return _builder.get_repo_sp(); } UserDocumentsBuilder &createDoc(uint32_t userId, search::DocumentIdT lid); UserDocumentsBuilder &createDocs(uint32_t userId, search::DocumentIdT begin, diff --git a/searchlib/CMakeLists.txt b/searchlib/CMakeLists.txt index a7d831aa623..e8f43a6037d 100644 --- a/searchlib/CMakeLists.txt +++ b/searchlib/CMakeLists.txt @@ -168,8 +168,6 @@ vespa_define_module( src/tests/grouping src/tests/groupingengine src/tests/hitcollector - src/tests/index/docbuilder - src/tests/index/doctypebuilder src/tests/index/field_length_calculator src/tests/indexmetainfo src/tests/ld-library-path @@ -225,6 +223,8 @@ vespa_define_module( src/tests/tensor/hnsw_saver src/tests/tensor/tensor_buffer_operations src/tests/tensor/tensor_buffer_store + src/tests/tensor/tensor_buffer_type_mapper + src/tests/test/string_field_builder src/tests/transactionlog src/tests/transactionlogstress src/tests/true diff --git a/searchlib/src/tests/attribute/attributemanager/attributemanager_test.cpp b/searchlib/src/tests/attribute/attributemanager/attributemanager_test.cpp index 7f0a88c9f86..3fa74b78d2a 100644 --- a/searchlib/src/tests/attribute/attributemanager/attributemanager_test.cpp +++ b/searchlib/src/tests/attribute/attributemanager/attributemanager_test.cpp @@ -38,8 +38,7 @@ public: generation_t getGen() const { return getCurrentGeneration(); } uint32_t getRefCount(generation_t gen) const { return getGenerationRefCount(gen); } void incGen() { incGeneration(); } - void updateFirstUsedGen() { updateFirstUsedGeneration(); } - generation_t getFirstUsedGen() const { return getFirstUsedGeneration(); } + generation_t oldest_used_gen() const { return get_oldest_used_generation(); } }; @@ -49,35 +48,35 @@ TEST("Test attribute guards") TestAttribute * v = static_cast<TestAttribute *> (vec.get()); EXPECT_EQUAL(v->getGen(), unsigned(0)); EXPECT_EQUAL(v->getRefCount(0), unsigned(0)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(0)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(0)); { AttributeGuard g0(vec); EXPECT_EQUAL(v->getGen(), unsigned(0)); EXPECT_EQUAL(v->getRefCount(0), unsigned(1)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(0)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(0)); { AttributeGuard g1(vec); EXPECT_EQUAL(v->getGen(), unsigned(0)); EXPECT_EQUAL(v->getRefCount(0), unsigned(2)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(0)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(0)); } EXPECT_EQUAL(v->getRefCount(0), unsigned(1)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(0)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(0)); } EXPECT_EQUAL(v->getRefCount(0), unsigned(0)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(0)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(0)); v->incGen(); EXPECT_EQUAL(v->getGen(), unsigned(1)); EXPECT_EQUAL(v->getRefCount(0), unsigned(0)); EXPECT_EQUAL(v->getRefCount(1), unsigned(0)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(1)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(1)); { AttributeGuard g0(vec); EXPECT_EQUAL(v->getGen(), unsigned(1)); EXPECT_EQUAL(v->getRefCount(0), unsigned(0)); EXPECT_EQUAL(v->getRefCount(1), unsigned(1)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(1)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(1)); { v->incGen(); AttributeGuard g1(vec); @@ -85,19 +84,19 @@ TEST("Test attribute guards") EXPECT_EQUAL(v->getRefCount(0), unsigned(0)); EXPECT_EQUAL(v->getRefCount(1), unsigned(1)); EXPECT_EQUAL(v->getRefCount(2), unsigned(1)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(1)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(1)); } EXPECT_EQUAL(v->getRefCount(0), unsigned(0)); EXPECT_EQUAL(v->getRefCount(1), unsigned(1)); EXPECT_EQUAL(v->getRefCount(2), unsigned(0)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(1)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(1)); } EXPECT_EQUAL(v->getRefCount(0), unsigned(0)); EXPECT_EQUAL(v->getRefCount(1), unsigned(0)); EXPECT_EQUAL(v->getRefCount(2), unsigned(0)); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(1)); - v->updateFirstUsedGeneration(); - EXPECT_EQUAL(v->getFirstUsedGen(), unsigned(2)); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(1)); + v->update_oldest_used_generation(); + EXPECT_EQUAL(v->oldest_used_gen(), unsigned(2)); EXPECT_EQUAL(v->getGen(), unsigned(2)); } diff --git a/searchlib/src/tests/attribute/document_weight_or_filter_search/document_weight_or_filter_search_test.cpp b/searchlib/src/tests/attribute/document_weight_or_filter_search/document_weight_or_filter_search_test.cpp index e27065f1c25..b9c70d76934 100644 --- a/searchlib/src/tests/attribute/document_weight_or_filter_search/document_weight_or_filter_search_test.cpp +++ b/searchlib/src/tests/attribute/document_weight_or_filter_search/document_weight_or_filter_search_test.cpp @@ -135,7 +135,7 @@ DocumentWeightOrFilterSearchTest::~DocumentWeightOrFilterSearchTest() _postings.clear(tree); } _postings.clearBuilder(); - _postings.clearHoldLists(); + _postings.reclaim_all_memory(); inc_generation(); } @@ -143,10 +143,9 @@ void DocumentWeightOrFilterSearchTest::inc_generation() { _postings.freeze(); - _postings.transferHoldLists(_gens.getCurrentGeneration()); + _postings.assign_generation(_gens.getCurrentGeneration()); _gens.incGeneration(); - _gens.updateFirstUsedGeneration(); - _postings.trimHoldLists(_gens.getFirstUsedGeneration()); + _postings.reclaim_memory(_gens.get_oldest_used_generation()); } TEST_F(DocumentWeightOrFilterSearchTest, daat_or) diff --git a/searchlib/src/tests/attribute/enum_comparator/enum_comparator_test.cpp b/searchlib/src/tests/attribute/enum_comparator/enum_comparator_test.cpp index 1d76473754f..9d717202551 100644 --- a/searchlib/src/tests/attribute/enum_comparator/enum_comparator_test.cpp +++ b/searchlib/src/tests/attribute/enum_comparator/enum_comparator_test.cpp @@ -147,9 +147,9 @@ TEST("requireThatComparatorWithTreeIsWorking") EXPECT_EQUAL(101, exp); t.clear(m); m.freeze(); - m.transferHoldLists(g.getCurrentGeneration()); + m.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - m.trimHoldLists(g.getFirstUsedGeneration()); + m.reclaim_memory(g.get_oldest_used_generation()); } TEST("requireThatFoldedLessIsWorking") diff --git a/searchlib/src/tests/attribute/enumstore/enumstore_test.cpp b/searchlib/src/tests/attribute/enumstore/enumstore_test.cpp index 02ff01043b0..0542a253cc5 100644 --- a/searchlib/src/tests/attribute/enumstore/enumstore_test.cpp +++ b/searchlib/src/tests/attribute/enumstore/enumstore_test.cpp @@ -345,8 +345,8 @@ TEST(EnumStoreTest, test_hold_lists_and_generation) // check readers again checkReaders(ses, readers); - ses.transfer_hold_lists(sesGen); - ses.trim_hold_lists(sesGen + 1); + ses.assign_generation(sesGen); + ses.reclaim_memory(sesGen + 1); } void @@ -357,8 +357,8 @@ dec_ref_count(NumericEnumStore& store, NumericEnumStore::Index idx) updater.commit(); generation_t gen = 5; - store.transfer_hold_lists(gen); - store.trim_hold_lists(gen + 1); + store.assign_generation(gen); + store.reclaim_memory(gen + 1); } TEST(EnumStoreTest, address_space_usage_is_reported) @@ -882,9 +882,9 @@ namespace { void inc_generation(generation_t &gen, NumericEnumStore &store) { store.freeze_dictionary(); - store.transfer_hold_lists(gen); + store.assign_generation(gen); ++gen; - store.trim_hold_lists(gen); + store.reclaim_memory(gen); } } diff --git a/searchlib/src/tests/attribute/imported_attribute_vector/imported_attribute_vector_test.cpp b/searchlib/src/tests/attribute/imported_attribute_vector/imported_attribute_vector_test.cpp index b9f3c23213e..0d2ce048111 100644 --- a/searchlib/src/tests/attribute/imported_attribute_vector/imported_attribute_vector_test.cpp +++ b/searchlib/src/tests/attribute/imported_attribute_vector/imported_attribute_vector_test.cpp @@ -73,14 +73,14 @@ TEST_F("makeReadGuard(false) acquires guards on both target and reference attrib EXPECT_EQUAL(2u, f.target_attr->getCurrentGeneration()); EXPECT_EQUAL(2u, f.reference_attr->getCurrentGeneration()); // Should still be holding guard for first generation of writes for both attributes - EXPECT_EQUAL(1u, f.target_attr->getFirstUsedGeneration()); - EXPECT_EQUAL(1u, f.reference_attr->getFirstUsedGeneration()); + EXPECT_EQUAL(1u, f.target_attr->get_oldest_used_generation()); + EXPECT_EQUAL(1u, f.reference_attr->get_oldest_used_generation()); } // Force a generation handler update add_n_docs_with_undefined_values(*f.reference_attr, 1); add_n_docs_with_undefined_values(*f.target_attr, 1); - EXPECT_EQUAL(3u, f.target_attr->getFirstUsedGeneration()); - EXPECT_EQUAL(3u, f.reference_attr->getFirstUsedGeneration()); + EXPECT_EQUAL(3u, f.target_attr->get_oldest_used_generation()); + EXPECT_EQUAL(3u, f.reference_attr->get_oldest_used_generation()); } TEST_F("makeReadGuard(true) acquires enum guard on target and regular guard on reference attribute", Fixture) { @@ -95,15 +95,15 @@ TEST_F("makeReadGuard(true) acquires enum guard on target and regular guard on r EXPECT_EQUAL(5u, f.target_attr->getCurrentGeneration()); EXPECT_EQUAL(2u, f.reference_attr->getCurrentGeneration()); - EXPECT_EQUAL(3u, f.target_attr->getFirstUsedGeneration()); - EXPECT_EQUAL(1u, f.reference_attr->getFirstUsedGeneration()); + EXPECT_EQUAL(3u, f.target_attr->get_oldest_used_generation()); + EXPECT_EQUAL(1u, f.reference_attr->get_oldest_used_generation()); EXPECT_TRUE(has_active_enum_guards(*f.target_attr)); } // Force a generation handler update add_n_docs_with_undefined_values(*f.reference_attr, 1); add_n_docs_with_undefined_values(*f.target_attr, 1); - EXPECT_EQUAL(7u, f.target_attr->getFirstUsedGeneration()); - EXPECT_EQUAL(3u, f.reference_attr->getFirstUsedGeneration()); + EXPECT_EQUAL(7u, f.target_attr->get_oldest_used_generation()); + EXPECT_EQUAL(3u, f.reference_attr->get_oldest_used_generation()); EXPECT_FALSE(has_active_enum_guards(*f.target_attr)); } diff --git a/searchlib/src/tests/attribute/multi_value_mapping/multi_value_mapping_test.cpp b/searchlib/src/tests/attribute/multi_value_mapping/multi_value_mapping_test.cpp index 735ebcff6cf..8b8f4d2c4d4 100644 --- a/searchlib/src/tests/attribute/multi_value_mapping/multi_value_mapping_test.cpp +++ b/searchlib/src/tests/attribute/multi_value_mapping/multi_value_mapping_test.cpp @@ -41,11 +41,11 @@ class MyAttribute : public search::NotImplementedAttribute _mvMapping.shrink(committedDocIdLimit); setNumDocs(committedDocIdLimit); } - virtual void removeOldGenerations(generation_t firstUsed) override { - _mvMapping.trimHoldLists(firstUsed); + virtual void reclaim_memory(generation_t oldest_used_gen) override { + _mvMapping.reclaim_memory(oldest_used_gen); } - virtual void onGenerationChange(generation_t generation) override { - _mvMapping.transferHoldLists(generation - 1); + virtual void before_inc_generation(generation_t current_gen) override { + _mvMapping.assign_generation(current_gen); } public: @@ -115,8 +115,8 @@ public: ConstArrayRef act = get(docId); EXPECT_EQ(exp, std::vector<EntryT>(act.cbegin(), act.cend())); } - void transferHoldLists(generation_t generation) { _mvMapping->transferHoldLists(generation); } - void trimHoldLists(generation_t firstUsed) { _mvMapping->trimHoldLists(firstUsed); } + void assign_generation(generation_t current_gen) { _mvMapping->assign_generation(current_gen); } + void reclaim_memory(generation_t oldest_used_gen) { _mvMapping->reclaim_memory(oldest_used_gen); } void addDocs(uint32_t numDocs) { for (uint32_t i = 0; i < numDocs; ++i) { uint32_t doc = 0; @@ -245,12 +245,12 @@ TEST_F(IntMappingTest, test_that_old_value_is_not_overwritten_while_held) auto old3 = get(3); assertArray({5}, old3); set(3, {7}); - transferHoldLists(10); + assign_generation(10); assertArray({5}, old3); assertGet(3, {7}); - trimHoldLists(10); + reclaim_memory(10); assertArray({5}, old3); - trimHoldLists(11); + reclaim_memory(11); assertArray({0}, old3); } diff --git a/searchlib/src/tests/attribute/posting_store/posting_store_test.cpp b/searchlib/src/tests/attribute/posting_store/posting_store_test.cpp index 36babec6a89..75e7faf0227 100644 --- a/searchlib/src/tests/attribute/posting_store/posting_store_test.cpp +++ b/searchlib/src/tests/attribute/posting_store/posting_store_test.cpp @@ -64,11 +64,11 @@ protected: { _value_store.freeze_dictionary(); _store.freeze(); - _value_store.transfer_hold_lists(_gen_handler.getCurrentGeneration()); - _store.transferHoldLists(_gen_handler.getCurrentGeneration()); + _value_store.assign_generation(_gen_handler.getCurrentGeneration()); + _store.assign_generation(_gen_handler.getCurrentGeneration()); _gen_handler.incGeneration(); - _value_store.trim_hold_lists(_gen_handler.getFirstUsedGeneration()); - _store.trimHoldLists(_gen_handler.getFirstUsedGeneration()); + _value_store.reclaim_memory(_gen_handler.get_oldest_used_generation()); + _store.reclaim_memory(_gen_handler.get_oldest_used_generation()); } EntryRef add_sequence(int start_key, int end_key) diff --git a/searchlib/src/tests/attribute/postinglist/postinglist.cpp b/searchlib/src/tests/attribute/postinglist/postinglist.cpp index 54efb3261c8..1eed3a015e1 100644 --- a/searchlib/src/tests/attribute/postinglist/postinglist.cpp +++ b/searchlib/src/tests/attribute/postinglist/postinglist.cpp @@ -201,7 +201,7 @@ private: PostingListNodeAllocator &postingsAlloc); void - removeOldGenerations(Tree &tree, + reclaim_memory(Tree &tree, ValueHandle &valueHandle, PostingList &postings, PostingListNodeAllocator &postingsAlloc); @@ -259,12 +259,12 @@ AttributePostingListTest::freeTree(bool verbose) static_cast<uint64_t>(_intNodeAlloc->getMemoryUsage().allocatedBytesOnHold())); _intNodeAlloc->freeze(); _intPostings->freeze(); - _intNodeAlloc->transferHoldLists(_handler.getCurrentGeneration()); + _intNodeAlloc->assign_generation(_handler.getCurrentGeneration()); _intPostings->clearBuilder(); - _intPostings->transferHoldLists(_handler.getCurrentGeneration()); + _intPostings->assign_generation(_handler.getCurrentGeneration()); _handler.incGeneration(); - _intNodeAlloc->trimHoldLists(_handler.getFirstUsedGeneration()); - _intPostings->trimHoldLists(_handler.getFirstUsedGeneration()); + _intNodeAlloc->reclaim_memory(_handler.get_oldest_used_generation()); + _intPostings->reclaim_memory(_handler.get_oldest_used_generation()); LOG(info, "freeTree after unhold: %" PRIu64 " (%" PRIu64 " held)", static_cast<uint64_t>(_intNodeAlloc->getMemoryUsage().allocatedBytes()), @@ -613,9 +613,9 @@ AttributePostingListTest::doCompactEnumStore(Tree &tree, valueHandle.holdBuffer(*it); } generation_t generation = _handler.getCurrentGeneration(); - valueHandle.transferHoldLists(generation); + valueHandle.assign_generation(generation); _handler.incGeneration(); - valueHandle.trimHoldLists(_handler.getFirstUsedGeneration()); + valueHandle.reclaim_memory(_handler.get_oldest_used_generation()); LOG(info, "doCompactEnumStore done"); @@ -658,22 +658,22 @@ bumpGeneration(Tree &tree, (void) tree; (void) valueHandle; postingsAlloc.freeze(); - postingsAlloc.transferHoldLists(_handler.getCurrentGeneration()); - postings.transferHoldLists(_handler.getCurrentGeneration()); + postingsAlloc.assign_generation(_handler.getCurrentGeneration()); + postings.assign_generation(_handler.getCurrentGeneration()); _handler.incGeneration(); } void AttributePostingListTest:: -removeOldGenerations(Tree &tree, +reclaim_memory(Tree &tree, ValueHandle &valueHandle, PostingList &postings, PostingListNodeAllocator &postingsAlloc) { (void) tree; (void) valueHandle; - postingsAlloc.trimHoldLists(_handler.getFirstUsedGeneration()); - postings.trimHoldLists(_handler.getFirstUsedGeneration()); + postingsAlloc.reclaim_memory(_handler.get_oldest_used_generation()); + postings.reclaim_memory(_handler.get_oldest_used_generation()); } int @@ -689,7 +689,7 @@ AttributePostingListTest::Main() lookupRandomValues(*_intTree, *_intNodeAlloc, *_intKeyStore, *_intPostings, _stlTree, _randomValues); _intNodeAlloc->freeze(); - _intNodeAlloc->transferHoldLists(_handler.getCurrentGeneration()); + _intNodeAlloc->assign_generation(_handler.getCurrentGeneration()); doCompactEnumStore(*_intTree, *_intNodeAlloc, *_intKeyStore); removeRandomValues(*_intTree, *_intNodeAlloc, *_intKeyStore, *_intPostings, _stlTree, _randomValues); diff --git a/searchlib/src/tests/attribute/tensorattribute/tensorattribute_test.cpp b/searchlib/src/tests/attribute/tensorattribute/tensorattribute_test.cpp index 3dda2eb6d95..9127c4b59fc 100644 --- a/searchlib/src/tests/attribute/tensorattribute/tensorattribute_test.cpp +++ b/searchlib/src/tests/attribute/tensorattribute/tensorattribute_test.cpp @@ -221,11 +221,11 @@ public: auto vector = _vectors.get_vector(docid).typify<double>(); _removes.emplace_back(docid, DoubleVector(vector.begin(), vector.end())); } - void transfer_hold_lists(generation_t current_gen) override { + void assign_generation(generation_t current_gen) override { _transfer_gen = current_gen; } - void trim_hold_lists(generation_t first_used_gen) override { - _trim_gen = first_used_gen; + void reclaim_memory(generation_t oldest_used_gen) override { + _trim_gen = oldest_used_gen; } bool consider_compact(const CompactionStrategy&) override { return false; @@ -350,28 +350,11 @@ struct Fixture { vespalib::ThreadStackExecutor _executor; bool _denseTensors; FixtureTraits _traits; + vespalib::string _mmap_allocator_base_dir; - Fixture(const vespalib::string &typeSpec, - FixtureTraits traits = FixtureTraits()) - : _dir_handler(test_dir), - _cfg(BasicType::TENSOR, CollectionType::SINGLE), - _name(attr_name), - _typeSpec(typeSpec), - _index_factory(), - _tensorAttr(), - _attr(), - _executor(1, 0x10000), - _denseTensors(false), - _traits(traits) - { - if (traits.enable_hnsw_index) { - _cfg.set_distance_metric(DistanceMetric::Euclidean); - _cfg.set_hnsw_index_params(HnswIndexParams(4, 20, DistanceMetric::Euclidean)); - } - setup(); - } + Fixture(const vespalib::string &typeSpec, FixtureTraits traits = FixtureTraits()); - ~Fixture() {} + ~Fixture(); void setup() { _cfg.setTensorType(ValueType::from_spec(_typeSpec)); @@ -545,8 +528,35 @@ struct Fixture { void testEmptyTensor(); void testOnHoldAccounting(); void test_populate_address_space_usage(); + void test_mmap_file_allocator(); }; +Fixture::Fixture(const vespalib::string &typeSpec, FixtureTraits traits) + : _dir_handler(test_dir), + _cfg(BasicType::TENSOR, CollectionType::SINGLE), + _name(attr_name), + _typeSpec(typeSpec), + _index_factory(), + _tensorAttr(), + _attr(), + _executor(1, 0x10000), + _denseTensors(false), + _traits(traits), + _mmap_allocator_base_dir("mmap-file-allocator-factory-dir") +{ + if (traits.enable_hnsw_index) { + _cfg.set_distance_metric(DistanceMetric::Euclidean); + _cfg.set_hnsw_index_params(HnswIndexParams(4, 20, DistanceMetric::Euclidean)); + } + vespalib::alloc::MmapFileAllocatorFactory::instance().setup(_mmap_allocator_base_dir); + setup(); +} + +Fixture::~Fixture() +{ + vespalib::alloc::MmapFileAllocatorFactory::instance().setup(""); + std::filesystem::remove_all(std::filesystem::path(_mmap_allocator_base_dir)); +} void Fixture::set_example_tensors() @@ -750,6 +760,23 @@ Fixture::test_populate_address_space_usage() } } +void +Fixture::test_mmap_file_allocator() +{ + std::filesystem::path allocator_dir(_mmap_allocator_base_dir + "/0.my_attr"); + if (!_traits.use_mmap_file_allocator) { + EXPECT_FALSE(std::filesystem::is_directory(allocator_dir)); + } else { + EXPECT_TRUE(std::filesystem::is_directory(allocator_dir)); + int entry_cnt = 0; + for (auto& entry : std::filesystem::directory_iterator(allocator_dir)) { + EXPECT_LESS(0u, entry.file_size()); + ++entry_cnt; + } + EXPECT_LESS(0, entry_cnt); + } +} + template <class MakeFixture> void testAll(MakeFixture &&f) { @@ -761,6 +788,7 @@ void testAll(MakeFixture &&f) TEST_DO(f()->testEmptyTensor()); TEST_DO(f()->testOnHoldAccounting()); TEST_DO(f()->test_populate_address_space_usage()); + TEST_DO(f()->test_mmap_file_allocator()); } TEST("Test sparse tensors with generic tensor attribute") @@ -768,6 +796,11 @@ TEST("Test sparse tensors with generic tensor attribute") testAll([]() { return std::make_shared<Fixture>(sparseSpec); }); } +TEST("Test sparse tensors with generic tensor attribute, paged") +{ + testAll([]() { return std::make_shared<Fixture>(sparseSpec, FixtureTraits().mmap_file_allocator()); }); +} + TEST("Test sparse tensors with direct tensor attribute") { testAll([]() { return std::make_shared<Fixture>(sparseSpec, FixtureTraits().direct()); }); @@ -778,11 +811,21 @@ TEST("Test dense tensors with generic tensor attribute") testAll([]() { return std::make_shared<Fixture>(denseSpec); }); } +TEST("Test dense tensors with generic tensor attribute, paged") +{ + testAll([]() { return std::make_shared<Fixture>(denseSpec, FixtureTraits().mmap_file_allocator()); }); +} + TEST("Test dense tensors with dense tensor attribute") { testAll([]() { return std::make_shared<Fixture>(denseSpec, FixtureTraits().dense()); }); } +TEST("Test dense tensors with dense tensor attribute, paged") +{ + testAll([]() { return std::make_shared<Fixture>(denseSpec, FixtureTraits().dense().mmap_file_allocator()); }); +} + TEST_F("Hnsw index is NOT instantiated in dense tensor attribute by default", Fixture(vec_2d_spec, FixtureTraits().dense())) { @@ -1176,17 +1219,4 @@ TEST_F("NN blueprint do NOT want global filter when NOT having index (implicit b EXPECT_FALSE(bp->getState().want_global_filter()); } -TEST("Dense tensor attribute with paged flag uses mmap file allocator") -{ - vespalib::string basedir("mmap-file-allocator-factory-dir"); - vespalib::alloc::MmapFileAllocatorFactory::instance().setup(basedir); - { - Fixture f(vec_2d_spec, FixtureTraits().dense().mmap_file_allocator()); - vespalib::string allocator_dir(basedir + "/0.my_attr"); - EXPECT_TRUE(std::filesystem::is_directory(std::filesystem::path(allocator_dir))); - } - vespalib::alloc::MmapFileAllocatorFactory::instance().setup(""); - std::filesystem::remove_all(std::filesystem::path(basedir)); -} - TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/searchlib/src/tests/common/bitvector/bitvector_test.cpp b/searchlib/src/tests/common/bitvector/bitvector_test.cpp index 79af28d20be..7a26202682b 100644 --- a/searchlib/src/tests/common/bitvector/bitvector_test.cpp +++ b/searchlib/src/tests/common/bitvector/bitvector_test.cpp @@ -654,8 +654,8 @@ TEST("requireThatGrowWorks") EXPECT_EQUAL(4095u, v.writer().capacity()); EXPECT_EQUAL(3u, v.writer().countTrueBits()); - g.transferHoldLists(1); - g.trimHoldLists(2); + g.assign_generation(1); + g.reclaim(2); } TEST("require that growable bit vectors keeps memory allocator") @@ -676,8 +676,8 @@ TEST("require that growable bit vectors keeps memory allocator") EXPECT_EQUAL(AllocStats(4, 1), stats); v.writer().resize(1); // DO NOT TRY THIS AT HOME EXPECT_EQUAL(AllocStats(5, 2), stats); - g.transferHoldLists(1); - g.trimHoldLists(2); + g.assign_generation(1); + g.reclaim(2); } TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/searchlib/src/tests/diskindex/fusion/CMakeLists.txt b/searchlib/src/tests/diskindex/fusion/CMakeLists.txt index 367f71783f9..6b9cc1b495a 100644 --- a/searchlib/src/tests/diskindex/fusion/CMakeLists.txt +++ b/searchlib/src/tests/diskindex/fusion/CMakeLists.txt @@ -3,7 +3,7 @@ vespa_add_executable(searchlib_fusion_test_app TEST SOURCES fusion_test.cpp DEPENDS - searchlib + searchlib_test GTest::GTest AFTER searchlib_vespa-index-inspect_app diff --git a/searchlib/src/tests/diskindex/fusion/fusion_test.cpp b/searchlib/src/tests/diskindex/fusion/fusion_test.cpp index 6e60d14b8ff..54d34c849a0 100644 --- a/searchlib/src/tests/diskindex/fusion/fusion_test.cpp +++ b/searchlib/src/tests/diskindex/fusion/fusion_test.cpp @@ -1,14 +1,20 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/searchlib/diskindex/fusion.h> +#include <vespa/document/fieldvalue/arrayfieldvalue.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/fieldvalue/weightedsetfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/searchlib/common/flush_token.h> #include <vespa/searchlib/diskindex/diskindex.h> -#include <vespa/searchlib/diskindex/fusion.h> #include <vespa/searchlib/diskindex/indexbuilder.h> #include <vespa/searchlib/diskindex/zcposoccrandread.h> #include <vespa/searchlib/fef/fieldpositionsiterator.h> #include <vespa/searchlib/fef/termfieldmatchdata.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/index/schemautil.h> #include <vespa/searchlib/memoryindex/document_inverter.h> #include <vespa/searchlib/memoryindex/document_inverter_context.h> @@ -31,7 +37,10 @@ LOG_SETUP("fusion_test"); namespace search { +using document::ArrayFieldValue; using document::Document; +using document::StringFieldValue; +using document::WeightedSetFieldValue; using fef::FieldPositionsIterator; using fef::TermFieldMatchData; using fef::TermFieldMatchDataArray; @@ -43,6 +52,8 @@ using search::common::FileHeaderContext; using search::index::schema::CollectionType; using search::index::schema::DataType; using search::index::test::MockFieldLengthInspector; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using vespalib::SequencedTaskExecutor; using namespace index; @@ -112,24 +123,18 @@ toString(FieldPositionsIterator posItr, bool hasElements = false, bool hasWeight std::unique_ptr<Document> make_doc10(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::10"); - b.startIndexField("f0"). - addStr("a").addStr("b").addStr("c").addStr("d"). - addStr("e").addStr("f").addStr("z"). - endField(); - b.startIndexField("f1"). - addStr("w").addStr("x"). - addStr("y").addStr("z"). - endField(); - b.startIndexField("f2"). - startElement(4).addStr("ax").addStr("ay").addStr("z").endElement(). - startElement(5).addStr("ax").endElement(). - endField(); - b.startIndexField("f3"). - startElement(4).addStr("wx").addStr("z").endElement(). - endField(); - - return b.endDocument(); + auto doc = b.make_document("id:ns:searchdocument::10"); + StringFieldBuilder sfb(b); + doc->setValue("f0", sfb.tokenize("a b c d e f z").build()); + doc->setValue("f1", sfb.tokenize("w x y z").build()); + auto string_array = b.make_array("f2"); + string_array.add(sfb.tokenize("ax ay z").build()); + string_array.add(sfb.tokenize("ax").build()); + doc->setValue("f2", string_array); + auto string_wset = b.make_wset("f3"); + string_wset.add(sfb.tokenize("wx z").build(), 4); + doc->setValue("f3", string_wset); + return doc; } Schema::IndexField @@ -151,6 +156,18 @@ make_schema(bool interleaved_features) return schema; } +DocBuilder::AddFieldsType +make_add_fields() +{ + return [](auto& header) { using namespace document::config_builder; + using DataType = document::DataType; + header.addField("f0", DataType::T_STRING) + .addField("f1", DataType::T_STRING) + .addField("f2", Array(DataType::T_STRING)) + .addField("f3", Wset(DataType::T_STRING)); + }; +} + void assert_interleaved_features(DiskIndex &d, const vespalib::string &field, const vespalib::string &term, uint32_t doc_id, uint32_t exp_num_occs, uint32_t exp_field_length) { @@ -327,7 +344,8 @@ FusionTest::requireThatFusionIsWorking(const vespalib::string &prefix, bool dire addField("f2").addField("f3"). addField("f4")); FieldIndexCollection fic(schema, MockFieldLengthInspector()); - DocBuilder b(schema); + DocBuilder b(make_add_fields()); + StringFieldBuilder sfb(b); auto invertThreads = SequencedTaskExecutor::create(invert_executor, 2); auto pushThreads = SequencedTaskExecutor::create(push_executor, 2); DocumentInverterContext inv_context(schema, *invertThreads, *pushThreads, fic); @@ -338,19 +356,21 @@ FusionTest::requireThatFusionIsWorking(const vespalib::string &prefix, bool dire inv.invertDocument(10, *doc, {}); myPushDocument(inv); - b.startDocument("id:ns:searchdocument::11"). - startIndexField("f3"). - startElement(-27).addStr("zz").endElement(). - endField(); - doc = b.endDocument(); + doc = b.make_document("id:ns:searchdocument::11"); + { + auto string_wset = b.make_wset("f3"); + string_wset.add(sfb.word("zz").build(), -27); + doc->setValue("f3", string_wset); + } inv.invertDocument(11, *doc, {}); myPushDocument(inv); - b.startDocument("id:ns:searchdocument::12"). - startIndexField("f3"). - startElement(0).addStr("zz0").endElement(). - endField(); - doc = b.endDocument(); + doc = b.make_document("id:ns:searchdocument::12"); + { + auto string_wset = b.make_wset("f3"); + string_wset.add(sfb.word("zz0").build(), 0); + doc->setValue("f3", string_wset); + } inv.invertDocument(12, *doc, {}); myPushDocument(inv); @@ -468,7 +488,7 @@ FusionTest::make_simple_index(const vespalib::string &dump_dir, const IFieldLeng FieldIndexCollection fic(_schema, field_length_inspector); uint32_t numDocs = 20; uint32_t numWords = 1000; - DocBuilder b(_schema); + DocBuilder b(make_add_fields()); auto invertThreads = SequencedTaskExecutor::create(invert_executor, 2); auto pushThreads = SequencedTaskExecutor::create(push_executor, 2); DocumentInverterContext inv_context(_schema, *invertThreads, *pushThreads, fic); diff --git a/searchlib/src/tests/index/docbuilder/.gitignore b/searchlib/src/tests/index/docbuilder/.gitignore deleted file mode 100644 index 999644fce87..00000000000 --- a/searchlib/src/tests/index/docbuilder/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*_test -.depend -Makefile -docbuilder_test -searchlib_docbuilder_test_app diff --git a/searchlib/src/tests/index/docbuilder/CMakeLists.txt b/searchlib/src/tests/index/docbuilder/CMakeLists.txt deleted file mode 100644 index 7a969f602ea..00000000000 --- a/searchlib/src/tests/index/docbuilder/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -vespa_add_executable(searchlib_docbuilder_test_app TEST - SOURCES - docbuilder_test.cpp - DEPENDS - searchlib -) -vespa_add_test(NAME searchlib_docbuilder_test_app COMMAND searchlib_docbuilder_test_app) diff --git a/searchlib/src/tests/index/docbuilder/docbuilder_test.cpp b/searchlib/src/tests/index/docbuilder/docbuilder_test.cpp deleted file mode 100644 index f76b61dcb78..00000000000 --- a/searchlib/src/tests/index/docbuilder/docbuilder_test.cpp +++ /dev/null @@ -1,437 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include <vespa/log/log.h> -LOG_SETUP("docbuilder_test"); -#include <boost/algorithm/string/classification.hpp> -#include <boost/algorithm/string/split.hpp> -#include <vespa/searchlib/index/docbuilder.h> -#include <vespa/vespalib/encoding/base64.h> -#include <vespa/vespalib/testkit/testapp.h> -#include <vespa/document/repo/fixedtyperepo.h> -#include <iostream> - -using namespace document; -using search::index::schema::CollectionType; - -namespace search::index { - -namespace -{ -std::string empty; -} - -namespace linguistics -{ -const vespalib::string SPANTREE_NAME("linguistics"); -} - - -TEST("test docBuilder") -{ - Schema s; - s.addIndexField(Schema::IndexField("ia", schema::DataType::STRING)); - s.addIndexField(Schema::IndexField("ib", schema::DataType::STRING, CollectionType::ARRAY)); - s.addIndexField(Schema::IndexField("ic", schema::DataType::STRING, CollectionType::WEIGHTEDSET)); - s.addUriIndexFields(Schema::IndexField("iu", schema::DataType::STRING)); - s.addUriIndexFields(Schema::IndexField("iau", schema::DataType::STRING, CollectionType::ARRAY)); - s.addUriIndexFields(Schema::IndexField("iwu", schema::DataType::STRING, CollectionType::WEIGHTEDSET)); - s.addAttributeField(Schema::AttributeField("aa", schema::DataType::INT32)); - s.addAttributeField(Schema::AttributeField("ab", schema::DataType::FLOAT)); - s.addAttributeField(Schema::AttributeField("ac", schema::DataType::STRING)); - s.addAttributeField(Schema::AttributeField("ad", schema::DataType::INT32, CollectionType::ARRAY)); - s.addAttributeField(Schema::AttributeField("ae", schema::DataType::FLOAT, CollectionType::ARRAY)); - s.addAttributeField(Schema::AttributeField("af", schema::DataType::STRING, CollectionType::ARRAY)); - s.addAttributeField(Schema::AttributeField("ag", schema::DataType::INT32, CollectionType::WEIGHTEDSET)); - s.addAttributeField(Schema::AttributeField("ah", schema::DataType::FLOAT, CollectionType::WEIGHTEDSET)); - s.addAttributeField(Schema::AttributeField("ai", schema::DataType::STRING, CollectionType::WEIGHTEDSET)); - s.addAttributeField(Schema::AttributeField("asp1", schema::DataType::INT32)); - s.addAttributeField(Schema::AttributeField("asp2", schema::DataType::INT64)); - s.addAttributeField(Schema::AttributeField("aap1", schema::DataType::INT32, CollectionType::ARRAY)); - s.addAttributeField(Schema::AttributeField("aap2", schema::DataType::INT64, CollectionType::ARRAY)); - s.addAttributeField(Schema::AttributeField("awp1", schema::DataType::INT32, CollectionType::WEIGHTEDSET)); - s.addAttributeField(Schema::AttributeField("awp2", schema::DataType::INT64, CollectionType::WEIGHTEDSET)); - - DocBuilder b(s); - Document::UP doc; - std::vector<std::string> lines; - std::vector<std::string>::const_iterator itr; - std::string xml; - - { // empty - doc = b.startDocument("id:ns:searchdocument::0").endDocument(); - xml = doc->toXml(""); - boost::split(lines, xml, boost::is_any_of("\n")); - itr = lines.begin(); - EXPECT_EQUAL("<document documenttype=\"searchdocument\" documentid=\"id:ns:searchdocument::0\"/>", *itr++); - EXPECT_EQUAL("", *itr++); - EXPECT_TRUE(itr == lines.end()); - } - { // all fields set - std::vector<char> binaryBlob; - binaryBlob.push_back('\0'); - binaryBlob.push_back('\2'); - binaryBlob.push_back('\1'); - std::string raw1s("Single Raw Element"); - std::string raw1a0("Array Raw Element 0"); - std::string raw1a1("Array Raw Element 1"); - std::string raw1w0("Weighted Set Raw Element 0"); - std::string raw1w1("Weighted Set Raw Element 1"); - raw1s += std::string(&binaryBlob[0], - &binaryBlob[0] + binaryBlob.size()); - raw1a0 += std::string(&binaryBlob[0], - &binaryBlob[0] + binaryBlob.size()); - raw1a1 += std::string(&binaryBlob[0], - &binaryBlob[0] + binaryBlob.size()); - raw1w0 += std::string(&binaryBlob[0], - &binaryBlob[0] + binaryBlob.size()); - raw1w1 += std::string(&binaryBlob[0], - &binaryBlob[0] + binaryBlob.size()); - b.startDocument("id:ns:searchdocument::1"); - b.startIndexField("ia").addStr("foo").addStr("bar").addStr("baz").addTermAnnotation("altbaz").endField(); - b.startIndexField("ib").startElement().addStr("foo").endElement(). - startElement(1).addStr("bar").addStr("baz").endElement().endField(); - b. startIndexField("ic"). - startElement(20).addStr("bar").addStr("baz").endElement(). - startElement().addStr("foo").endElement(). - endField(); - b.startIndexField("iu"). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:81/fluke?ab=2#4"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("81"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("4"). - endSubField(). - endField(); - b.startIndexField("iau"). - startElement(1). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:82/fluke?ab=2#8"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("82"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("8"). - endSubField(). - endElement(). - startElement(1). - startSubField("all"). - addUrlTokenizedString("http://www.flickr.com:82/fluke?ab=2#9"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.flickr.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("82"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("9"). - endSubField(). - endElement(). - endField(); - b.startIndexField("iwu"). - startElement(4). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:83/fluke?ab=2#12"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("83"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("12"). - endSubField(). - endElement(). - startElement(7). - startSubField("all"). - addUrlTokenizedString("http://www.flickr.com:85/fluke?ab=2#13"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.flickr.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("85"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("13"). - endSubField(). - endElement(). - endField(); - b.startAttributeField("aa").addInt(2147483647).endField(); - b.startAttributeField("ab").addFloat(1234.56).endField(); - b.startAttributeField("ac").addStr("foo baz").endField(); - b.startAttributeField("ad").startElement().addInt(10).endElement().endField(); - b.startAttributeField("ae").startElement().addFloat(10.5).endElement().endField(); - b.startAttributeField("af").startElement().addStr("foo").endElement().endField(); - b.startAttributeField("ag").startElement(2).addInt(20).endElement().endField(); - b.startAttributeField("ah").startElement(3).addFloat(20.5).endElement().endField(); - b.startAttributeField("ai").startElement(4).addStr("bar").endElement().endField(); - b.startAttributeField("asp1").addInt(1001).endField(); - b.startAttributeField("asp2").addPosition(1002, 1003).endField(); - b.startAttributeField("aap1"). - startElement().addInt(1004).endElement(). - startElement().addInt(1005).endElement(). - endField(); - b.startAttributeField("aap2"). - startElement().addPosition(1006, 1007).endElement(). - startElement().addPosition(1008, 1009).endElement(). - endField(); - b.startAttributeField("awp1"). - startElement(41).addInt(1010).endElement(). - startElement(42).addInt(1011).endElement(). - endField(); - b.startAttributeField("awp2"). - startElement(43).addPosition(1012, 1013).endElement(). - startElement(44).addPosition(1014, 1015).endElement(). - endField(); - doc = b.endDocument(); - xml = doc->toXml(""); - boost::split(lines, xml, boost::is_any_of("\n")); - itr = lines.begin(); - EXPECT_EQUAL("<document documenttype=\"searchdocument\" documentid=\"id:ns:searchdocument::1\">", *itr++); - EXPECT_EQUAL("<iu>", *itr++); - EXPECT_EQUAL("<all>http://www.example.com:81/fluke?ab=2#4</all>", *itr++); - EXPECT_EQUAL("<host>www.example.com</host>", *itr++); - EXPECT_EQUAL("<scheme>http</scheme>", *itr++); - EXPECT_EQUAL("<path>/fluke</path>", *itr++); - EXPECT_EQUAL("<port>81</port>", *itr++); - EXPECT_EQUAL("<query>ab=2</query>", *itr++); - EXPECT_EQUAL("<fragment>4</fragment>", *itr++); - EXPECT_EQUAL("</iu>", *itr++); - EXPECT_EQUAL("<aa>2147483647</aa>", *itr++); - EXPECT_EQUAL("<aap2>", *itr++); - EXPECT_EQUAL("<item>1047806</item>", *itr++); - EXPECT_EQUAL("<item>1048322</item>", *itr++); - EXPECT_EQUAL("</aap2>", *itr++); - EXPECT_EQUAL("<ia>foo bar baz</ia>", *itr++); - EXPECT_EQUAL("<ae>", *itr++); - EXPECT_EQUAL("<item>10.5</item>", *itr++); - EXPECT_EQUAL("</ae>", *itr++); - EXPECT_EQUAL("<ib>", *itr++); - EXPECT_EQUAL("<item>foo</item>", *itr++); - EXPECT_EQUAL("<item>bar baz</item>", *itr++); - EXPECT_EQUAL("</ib>", *itr++); - EXPECT_EQUAL("<ah>", *itr++); - EXPECT_EQUAL("<item weight=\"3\">20.5</item>", *itr++); - EXPECT_EQUAL("</ah>", *itr++); - EXPECT_EQUAL("<ic>", *itr++); - EXPECT_EQUAL("<item weight=\"20\">bar baz</item>", *itr++); - EXPECT_EQUAL("<item weight=\"1\">foo</item>", *itr++); - EXPECT_EQUAL("</ic>", *itr++); - EXPECT_EQUAL("<ac>foo baz</ac>", *itr++); - EXPECT_EQUAL("<awp2>", *itr++); - EXPECT_EQUAL("<item weight=\"43\">1048370</item>", *itr++); - EXPECT_EQUAL("<item weight=\"44\">1048382</item>", *itr++); - EXPECT_EQUAL("</awp2>", *itr++); - EXPECT_EQUAL("<iau>", *itr++); - EXPECT_EQUAL("<item>", *itr++); - EXPECT_EQUAL("<all>http://www.example.com:82/fluke?ab=2#8</all>", *itr++); - EXPECT_EQUAL("<host>www.example.com</host>", *itr++); - EXPECT_EQUAL("<scheme>http</scheme>", *itr++); - EXPECT_EQUAL("<path>/fluke</path>", *itr++); - EXPECT_EQUAL("<port>82</port>", *itr++); - EXPECT_EQUAL("<query>ab=2</query>", *itr++); - EXPECT_EQUAL("<fragment>8</fragment>", *itr++); - EXPECT_EQUAL("</item>", *itr++); - EXPECT_EQUAL("<item>", *itr++); - EXPECT_EQUAL("<all>http://www.flickr.com:82/fluke?ab=2#9</all>", *itr++); - EXPECT_EQUAL("<host>www.flickr.com</host>", *itr++); - EXPECT_EQUAL("<scheme>http</scheme>", *itr++); - EXPECT_EQUAL("<path>/fluke</path>", *itr++); - EXPECT_EQUAL("<port>82</port>", *itr++); - EXPECT_EQUAL("<query>ab=2</query>", *itr++); - EXPECT_EQUAL("<fragment>9</fragment>", *itr++); - EXPECT_EQUAL("</item>", *itr++); - EXPECT_EQUAL("</iau>", *itr++); - EXPECT_EQUAL("<asp2>1047758</asp2>", *itr++); - EXPECT_EQUAL("<ai>", *itr++); - EXPECT_EQUAL("<item weight=\"4\">bar</item>", *itr++); - EXPECT_EQUAL("</ai>", *itr++); - EXPECT_EQUAL("<asp1>1001</asp1>", *itr++); - EXPECT_EQUAL("<ad>", *itr++); - EXPECT_EQUAL("<item>10</item>", *itr++); - EXPECT_EQUAL("</ad>", *itr++); - EXPECT_EQUAL("<iwu>", *itr++); - EXPECT_EQUAL("<item weight=\"4\">", *itr++); - EXPECT_EQUAL("<all>http://www.example.com:83/fluke?ab=2#12</all>", *itr++); - EXPECT_EQUAL("<host>www.example.com</host>", *itr++); - EXPECT_EQUAL("<scheme>http</scheme>", *itr++); - EXPECT_EQUAL("<path>/fluke</path>", *itr++); - EXPECT_EQUAL("<port>83</port>", *itr++); - EXPECT_EQUAL("<query>ab=2</query>", *itr++); - EXPECT_EQUAL("<fragment>12</fragment>", *itr++); - EXPECT_EQUAL("</item>", *itr++); - EXPECT_EQUAL("<item weight=\"7\">", *itr++); - EXPECT_EQUAL("<all>http://www.flickr.com:85/fluke?ab=2#13</all>", *itr++); - EXPECT_EQUAL("<host>www.flickr.com</host>", *itr++); - EXPECT_EQUAL("<scheme>http</scheme>", *itr++); - EXPECT_EQUAL("<path>/fluke</path>", *itr++); - EXPECT_EQUAL("<port>85</port>", *itr++); - EXPECT_EQUAL("<query>ab=2</query>", *itr++); - EXPECT_EQUAL("<fragment>13</fragment>", *itr++); - EXPECT_EQUAL("</item>", *itr++); - EXPECT_EQUAL("</iwu>", *itr++); - EXPECT_EQUAL("<ab>1234.56</ab>", *itr++); - EXPECT_EQUAL("<ag>", *itr++); - EXPECT_EQUAL("<item weight=\"2\">20</item>", *itr++); - EXPECT_EQUAL("</ag>", *itr++); - EXPECT_EQUAL("<awp1>", *itr++); - EXPECT_EQUAL("<item weight=\"41\">1010</item>", *itr++); - EXPECT_EQUAL("<item weight=\"42\">1011</item>", *itr++); - EXPECT_EQUAL("</awp1>", *itr++); - EXPECT_EQUAL("<aap1>", *itr++); - EXPECT_EQUAL("<item>1004</item>", *itr++); - EXPECT_EQUAL("<item>1005</item>", *itr++); - EXPECT_EQUAL("</aap1>", *itr++); - EXPECT_EQUAL("<af>", *itr++); - EXPECT_EQUAL("<item>foo</item>", *itr++); - EXPECT_EQUAL("</af>", *itr++); - EXPECT_EQUAL("</document>", *itr++); - EXPECT_TRUE(itr == lines.end()); -#if 0 - std::cout << "onedoc xml start -----" << std::endl << - xml << std::endl << - "-------" << std::endl; - std::cout << "onedoc toString start ----" << std::endl << - doc->toString(true) << std::endl << - "-------" << std::endl; -#endif - } - { // create one more to see that everything is cleared - b.startDocument("id:ns:searchdocument::2"); - b.startIndexField("ia").addStr("yes").endField(); - b.startAttributeField("aa").addInt(20).endField(); - doc = b.endDocument(); - xml = doc->toXml(""); - boost::split(lines, xml, boost::is_any_of("\n")); - itr = lines.begin(); - EXPECT_EQUAL("<document documenttype=\"searchdocument\" documentid=\"id:ns:searchdocument::2\">", *itr++); - EXPECT_EQUAL("<aa>20</aa>", *itr++); - EXPECT_EQUAL("<ia>yes</ia>", *itr++); - EXPECT_EQUAL("</document>", *itr++); - EXPECT_TRUE(itr == lines.end()); - } - { // create field with cjk chars - b.startDocument("id:ns:searchdocument::3"); - b.startIndexField("ia"). - addStr("我就是那个"). - setAutoSpace(false). - addStr("大灰狼"). - setAutoSpace(true). - endField(); - doc = b.endDocument(); - xml = doc->toXml(""); - boost::split(lines, xml, boost::is_any_of("\n")); - itr = lines.begin(); - EXPECT_EQUAL("<document documenttype=\"searchdocument\" documentid=\"id:ns:searchdocument::3\">", *itr++); - EXPECT_EQUAL("<ia>我就是那个大灰狼</ia>", *itr++); - EXPECT_EQUAL("</document>", *itr++); - EXPECT_TRUE(itr == lines.end()); - const FieldValue::UP iaval = doc->getValue("ia"); - ASSERT_TRUE(iaval.get() != NULL); - const StringFieldValue *iasval = dynamic_cast<const StringFieldValue *> - (iaval.get()); - ASSERT_TRUE(iasval != NULL); - StringFieldValue::SpanTrees trees = iasval->getSpanTrees(); - const SpanTree *tree = StringFieldValue::findTree(trees, linguistics::SPANTREE_NAME); - ASSERT_TRUE(tree != NULL); - std::vector<Span> spans; - std::vector<Span> expSpans; - for (SpanTree::const_iterator i = tree->begin(), ie = tree->end(); - i != ie; ++i) { - Annotation &ann = const_cast<Annotation &>(*i); - const Span *span = dynamic_cast<const Span *>(ann.getSpanNode()); - if (span == NULL) - continue; - spans.push_back(*span); - } - expSpans.push_back(Span(0, 15)); - expSpans.push_back(Span(0, 15)); - expSpans.push_back(Span(15, 9)); - expSpans.push_back(Span(15, 9)); - ASSERT_TRUE(expSpans == spans); -#if 0 - std::cout << "onedoc xml start -----" << std::endl << - xml << std::endl << - "-------" << std::endl; - std::cout << "onedoc toString start ----" << std::endl << - doc->toString(true) << std::endl << - "-------" << std::endl; -#endif - } -} - -TEST("test if index names are valid uri parts") { - EXPECT_FALSE(UriField::mightBePartofUri("all")); - EXPECT_FALSE(UriField::mightBePartofUri("fragment")); - EXPECT_FALSE(UriField::mightBePartofUri(".all")); - EXPECT_FALSE(UriField::mightBePartofUri("all.b")); - EXPECT_TRUE(UriField::mightBePartofUri("b.all")); - EXPECT_TRUE(UriField::mightBePartofUri("b.scheme")); - EXPECT_TRUE(UriField::mightBePartofUri("b.host")); - EXPECT_TRUE(UriField::mightBePartofUri("b.port")); - EXPECT_TRUE(UriField::mightBePartofUri("b.hostname")); - EXPECT_TRUE(UriField::mightBePartofUri("b.path")); - EXPECT_TRUE(UriField::mightBePartofUri("b.query")); - EXPECT_TRUE(UriField::mightBePartofUri("b.fragment")); -} - -} - -TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/searchlib/src/tests/index/doctypebuilder/.gitignore b/searchlib/src/tests/index/doctypebuilder/.gitignore deleted file mode 100644 index f15be1efcfe..00000000000 --- a/searchlib/src/tests/index/doctypebuilder/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*_test -.depend -Makefile -doctypebuilder_test -searchlib_doctypebuilder_test_app diff --git a/searchlib/src/tests/index/doctypebuilder/CMakeLists.txt b/searchlib/src/tests/index/doctypebuilder/CMakeLists.txt deleted file mode 100644 index 348ecde5a7c..00000000000 --- a/searchlib/src/tests/index/doctypebuilder/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -vespa_add_executable(searchlib_doctypebuilder_test_app TEST - SOURCES - doctypebuilder_test.cpp - DEPENDS - searchlib -) -vespa_add_test(NAME searchlib_doctypebuilder_test_app COMMAND searchlib_doctypebuilder_test_app) diff --git a/searchlib/src/tests/index/doctypebuilder/doctypebuilder_test.cpp b/searchlib/src/tests/index/doctypebuilder/doctypebuilder_test.cpp deleted file mode 100644 index 95854fa11b2..00000000000 --- a/searchlib/src/tests/index/doctypebuilder/doctypebuilder_test.cpp +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#include <vespa/document/repo/documenttyperepo.h> -#include <vespa/searchlib/index/doctypebuilder.h> -#include <vespa/document/datatype/documenttype.h> -#include <vespa/vespalib/testkit/testapp.h> - -using namespace document; - -namespace search { -namespace index { - -using schema::CollectionType; -using schema::DataType; - -TEST("testSearchDocType") { - Schema s; - s.addIndexField(Schema::IndexField("ia", DataType::STRING)); - s.addIndexField(Schema::IndexField("ib", DataType::STRING, CollectionType::ARRAY)); - s.addIndexField(Schema::IndexField("ic", DataType::STRING, CollectionType::WEIGHTEDSET)); - s.addUriIndexFields(Schema::IndexField("iu", DataType::STRING)); - s.addUriIndexFields(Schema::IndexField("iau", DataType::STRING, CollectionType::ARRAY)); - s.addUriIndexFields(Schema::IndexField("iwu", DataType::STRING, CollectionType::WEIGHTEDSET)); - s.addAttributeField(Schema::AttributeField("aa", DataType::INT32)); - s.addAttributeField(Schema::AttributeField("spos", DataType::INT64)); - s.addAttributeField(Schema::AttributeField("apos", DataType::INT64, CollectionType::ARRAY)); - s.addAttributeField(Schema::AttributeField("wpos", DataType::INT64, CollectionType::WEIGHTEDSET)); - - DocTypeBuilder docTypeBuilder(s); - document::config::DocumenttypesConfig config = docTypeBuilder.makeConfig(); - DocumentTypeRepo repo(config); - const DocumentType *docType = repo.getDocumentType("searchdocument"); - ASSERT_TRUE(docType); - EXPECT_EQUAL(10u, docType->getFieldCount()); - - EXPECT_EQUAL("String", docType->getField("ia").getDataType().getName()); - EXPECT_EQUAL("Array<String>", - docType->getField("ib").getDataType().getName()); - EXPECT_EQUAL("WeightedSet<String>", - docType->getField("ic").getDataType().getName()); - EXPECT_EQUAL("url", docType->getField("iu").getDataType().getName()); - EXPECT_EQUAL("Array<url>", - docType->getField("iau").getDataType().getName()); - EXPECT_EQUAL("WeightedSet<url>", - docType->getField("iwu").getDataType().getName()); - - EXPECT_EQUAL("Int", docType->getField("aa").getDataType().getName()); - EXPECT_EQUAL("Long", docType->getField("spos").getDataType().getName()); - EXPECT_EQUAL("Array<Long>", - docType->getField("apos").getDataType().getName()); - EXPECT_EQUAL("WeightedSet<Long>", - docType->getField("wpos").getDataType().getName()); -} - -TEST("require that multiple fields can have the same type") { - Schema s; - s.addIndexField(Schema::IndexField("array1", DataType::STRING, CollectionType::ARRAY)); - s.addIndexField(Schema::IndexField("array2", DataType::STRING, CollectionType::ARRAY)); - DocTypeBuilder docTypeBuilder(s); - document::config::DocumenttypesConfig config = docTypeBuilder.makeConfig(); - DocumentTypeRepo repo(config); - const DocumentType *docType = repo.getDocumentType("searchdocument"); - ASSERT_TRUE(docType); - EXPECT_EQUAL(2u, docType->getFieldCount()); - - EXPECT_EQUAL("Array<String>", - docType->getField("array1").getDataType().getName()); - EXPECT_EQUAL("Array<String>", - docType->getField("array2").getDataType().getName()); -} - -} // namespace index -} // namespace search - -TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/searchlib/src/tests/memoryindex/document_inverter/document_inverter_test.cpp b/searchlib/src/tests/memoryindex/document_inverter/document_inverter_test.cpp index 3f8a04d9460..85b8fc64304 100644 --- a/searchlib/src/tests/memoryindex/document_inverter/document_inverter_test.cpp +++ b/searchlib/src/tests/memoryindex/document_inverter/document_inverter_test.cpp @@ -1,8 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#include <vespa/searchlib/index/docbuilder.h> -#include <vespa/searchlib/index/field_length_calculator.h> #include <vespa/searchlib/memoryindex/document_inverter.h> +#include <vespa/document/datatype/datatype.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> +#include <vespa/searchlib/index/field_length_calculator.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/memoryindex/document_inverter_context.h> #include <vespa/searchlib/memoryindex/field_index_remover.h> #include <vespa/searchlib/memoryindex/field_inverter.h> @@ -19,74 +24,79 @@ namespace search::memoryindex { using document::Document; -using index::DocBuilder; using index::FieldLengthCalculator; using index::Schema; using index::schema::CollectionType; using index::schema::DataType; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using vespalib::SequencedTaskExecutor; using vespalib::ISequencedTaskExecutor; namespace { +DocBuilder::AddFieldsType +make_add_fields() +{ + return [](auto& header) { using namespace document::config_builder; + using DataType = document::DataType; + header.addField("f0", DataType::T_STRING) + .addField("f1", DataType::T_STRING) + .addField("f2", Array(DataType::T_STRING)) + .addField("f3", Wset(DataType::T_STRING)); + }; +} + Document::UP makeDoc10(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::10"); - b.startIndexField("f0"). - addStr("a").addStr("b").addStr("c").addStr("d"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::10"); + doc->setValue("f0", sfb.tokenize("a b c d").build()); + return doc; } Document::UP makeDoc11(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::11"); - b.startIndexField("f0"). - addStr("a").addStr("b").addStr("e").addStr("f"). - endField(); - b.startIndexField("f1"). - addStr("a").addStr("g"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::11"); + doc->setValue("f0", sfb.tokenize("a b e f").build()); + doc->setValue("f1", sfb.tokenize("a g").build()); + return doc; } Document::UP makeDoc12(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::12"); - b.startIndexField("f0"). - addStr("h").addStr("doc12"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::12"); + doc->setValue("f0", sfb.tokenize("h doc12").build()); + return doc; } Document::UP makeDoc13(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::13"); - b.startIndexField("f0"). - addStr("i").addStr("doc13"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::13"); + doc->setValue("f0", sfb.tokenize("i doc13").build()); + return doc; } Document::UP makeDoc14(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::14"); - b.startIndexField("f0"). - addStr("j").addStr("doc14"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::14"); + doc->setValue("f0", sfb.tokenize("j doc14").build()); + return doc; } Document::UP makeDoc15(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::15"); - return b.endDocument(); + return b.make_document("id:ns:searchdocument::15"); } } @@ -118,7 +128,7 @@ struct DocumentInverterTest : public ::testing::Test { DocumentInverterTest() : _schema(makeSchema()), - _b(_schema), + _b(make_add_fields()), _invertThreads(SequencedTaskExecutor::create(invert_executor, 1)), _pushThreads(SequencedTaskExecutor::create(push_executor, 1)), _word_store(), diff --git a/searchlib/src/tests/memoryindex/field_index/field_index_test.cpp b/searchlib/src/tests/memoryindex/field_index/field_index_test.cpp index 23d96f7b81c..1e6cb61d3f4 100644 --- a/searchlib/src/tests/memoryindex/field_index/field_index_test.cpp +++ b/searchlib/src/tests/memoryindex/field_index/field_index_test.cpp @@ -1,11 +1,18 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/document/datatype/datatype.h> +#include <vespa/document/datatype/urldatatype.h> +#include <vespa/document/fieldvalue/arrayfieldvalue.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/fieldvalue/structfieldvalue.h> +#include <vespa/document/fieldvalue/weightedsetfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/searchlib/diskindex/fusion.h> #include <vespa/searchlib/diskindex/indexbuilder.h> #include <vespa/searchlib/diskindex/zcposoccrandread.h> #include <vespa/searchlib/fef/fieldpositionsiterator.h> #include <vespa/searchlib/fef/termfieldmatchdata.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/index/docidandfeatures.h> #include <vespa/searchlib/index/dummyfileheadercontext.h> #include <vespa/searchlib/memoryindex/document_inverter.h> @@ -15,6 +22,8 @@ #include <vespa/searchlib/memoryindex/ordered_field_index_inserter.h> #include <vespa/searchlib/memoryindex/posting_iterator.h> #include <vespa/searchlib/queryeval/iterators.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/test/index/mock_field_length_inspector.h> #include <vespa/searchlib/test/memoryindex/wrap_inserter.h> #include <vespa/vespalib/btree/btreenodeallocator.hpp> @@ -37,12 +46,18 @@ namespace search { using namespace fef; using namespace index; +using document::ArrayFieldValue; using document::Document; +using document::StructFieldValue; +using document::UrlDataType; +using document::WeightedSetFieldValue; using queryeval::RankedSearchIteratorBase; using queryeval::SearchIterator; using search::index::schema::CollectionType; using search::index::schema::DataType; using search::index::test::MockFieldLengthInspector; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using vespalib::GenerationHandler; using vespalib::ISequencedTaskExecutor; using vespalib::SequencedTaskExecutor; @@ -451,14 +466,13 @@ featureStoreRef(const FieldIndexCollection &fieldIndexes, uint32_t fieldId) return fieldIndexes.getFieldIndex(fieldId)->getFeatureStore(); } -DataStoreBase::MemStats +MemoryStats getFeatureStoreMemStats(const FieldIndexCollection &fieldIndexes) { - DataStoreBase::MemStats res; + MemoryStats res; uint32_t numFields = fieldIndexes.getNumFields(); for (uint32_t fieldId = 0; fieldId < numFields; ++fieldId) { - DataStoreBase::MemStats stats = - fieldIndexes.getFieldIndex(fieldId)->getFeatureStore().getMemStats(); + auto stats = fieldIndexes.getFieldIndex(fieldId)->getFeatureStore().getMemStats(); res += stats; } return res; @@ -506,6 +520,12 @@ make_single_field_schema() return result; } +DocBuilder::AddFieldsType +make_single_add_fields() +{ + return [](auto& header) { header.addField("f0", document::DataType::T_STRING); }; +} + template <typename FieldIndexType> struct FieldIndexTest : public ::testing::Test { Schema schema; @@ -707,6 +727,18 @@ make_multi_field_schema() return result; } +DocBuilder::AddFieldsType +make_multi_field_add_fields() +{ + return [](auto& header) { using namespace document::config_builder; + using DataType = document::DataType; + header.addField("f0", DataType::T_STRING) + .addField("f1", DataType::T_STRING) + .addField("f2", Array(DataType::T_STRING)) + .addField("f3", Wset(DataType::T_STRING)); + }; +} + struct FieldIndexCollectionTest : public ::testing::Test { Schema schema; FieldIndexCollection fic; @@ -914,10 +946,10 @@ public: DocumentInverterContext _inv_context; DocumentInverter _inv; - InverterTest(const Schema& schema) + InverterTest(const Schema& schema, DocBuilder::AddFieldsType add_fields) : _schema(schema), _fic(_schema, MockFieldLengthInspector()), - _b(_schema), + _b(add_fields), _invertThreads(SequencedTaskExecutor::create(invert_executor, 2)), _pushThreads(SequencedTaskExecutor::create(push_executor, 2)), _inv_context(_schema, *_invertThreads, *_pushThreads, _fic), @@ -939,97 +971,69 @@ public: class BasicInverterTest : public InverterTest { public: - BasicInverterTest() : InverterTest(make_multi_field_schema()) {} + BasicInverterTest() : InverterTest(make_multi_field_schema(), make_multi_field_add_fields()) {} }; TEST_F(BasicInverterTest, require_that_inversion_is_working) { Document::UP doc; + StringFieldBuilder sfb(_b); - _b.startDocument("id:ns:searchdocument::10"); - _b.startIndexField("f0"). - addStr("a").addStr("b").addStr("c").addStr("d"). - endField(); - doc = _b.endDocument(); + doc = _b.make_document("id:ns:searchdocument::10"); + doc->setValue("f0", sfb.tokenize("a b c d").build()); _inv.invertDocument(10, *doc, {}); myPushDocument(_inv); - _b.startDocument("id:ns:searchdocument::20"); - _b.startIndexField("f0"). - addStr("a").addStr("a").addStr("b").addStr("c").addStr("d"). - endField(); - doc = _b.endDocument(); + doc = _b.make_document("id:ns:searchdocument::20"); + doc->setValue("f0", sfb.tokenize("a a b c d").build()); _inv.invertDocument(20, *doc, {}); myPushDocument(_inv); - _b.startDocument("id:ns:searchdocument::30"); - _b.startIndexField("f0"). - addStr("a").addStr("b").addStr("c").addStr("d"). - addStr("e").addStr("f"). - endField(); - _b.startIndexField("f1"). - addStr("\nw2").addStr("w").addStr("x"). - addStr("\nw3").addStr("y").addStr("z"). - endField(); - _b.startIndexField("f2"). - startElement(4). - addStr("w").addStr("x"). - endElement(). - startElement(5). - addStr("y").addStr("z"). - endElement(). - endField(); - _b.startIndexField("f3"). - startElement(6). - addStr("w").addStr("x"). - endElement(). - startElement(7). - addStr("y").addStr("z"). - endElement(). - endField(); - doc = _b.endDocument(); + doc = _b.make_document("id:ns:searchdocument::30"); + doc->setValue("f0", sfb.tokenize("a b c d e f").build()); + doc->setValue("f1", sfb.word("\nw2").tokenize(" w x "). + word("\nw3").tokenize(" y z").build()); + { + auto string_array = _b.make_array("f2"); + string_array.add(sfb.tokenize("w x").build()); + string_array.add(sfb.tokenize("y z").build()); + doc->setValue("f2", string_array); + } + { + auto string_wset = _b.make_wset("f3"); + string_wset.add(sfb.tokenize("w x").build(), 6); + string_wset.add(sfb.tokenize("y z").build(), 7); + doc->setValue("f3", string_wset); + } _inv.invertDocument(30, *doc, {}); myPushDocument(_inv); - _b.startDocument("id:ns:searchdocument::40"); - _b.startIndexField("f0"). - addStr("a").addStr("a").addStr("b").addStr("c").addStr("a"). - addStr("e").addStr("f"). - endField(); - doc = _b.endDocument(); + doc = _b.make_document("id:ns:searchdocument::40"); + doc->setValue("f0", sfb.tokenize("a a b c a e f").build()); _inv.invertDocument(40, *doc, {}); myPushDocument(_inv); - _b.startDocument("id:ns:searchdocument::999"); - _b.startIndexField("f0"). - addStr("this").addStr("is").addStr("_a_").addStr("test"). - addStr("for").addStr("insertion").addStr("speed").addStr("with"). - addStr("more").addStr("than").addStr("just").addStr("__a__"). - addStr("few").addStr("words").addStr("present").addStr("in"). - addStr("some").addStr("of").addStr("the").addStr("fields"). - endField(); - _b.startIndexField("f1"). - addStr("the").addStr("other").addStr("field").addStr("also"). - addStr("has").addStr("some").addStr("content"). - endField(); - _b.startIndexField("f2"). - startElement(1). - addStr("strange").addStr("things").addStr("here"). - addStr("has").addStr("some").addStr("content"). - endElement(). - endField(); - _b.startIndexField("f3"). - startElement(3). - addStr("not").addStr("a").addStr("weighty").addStr("argument"). - endElement(). - endField(); - doc = _b.endDocument(); + doc = _b.make_document("id:ns:searchdocument::999"); + doc->setValue("f0", sfb.tokenize("this is ").word("_a_"). + tokenize(" test for insertion speed with more than just "). + word("__a__").tokenize(" few words present in some of the fields").build()); + doc->setValue("f1", sfb.tokenize("the other field also has some content").build()); + { + auto string_array = _b.make_array("f2"); + string_array.add(sfb.tokenize("strange things here has some content").build()); + doc->setValue("f2", string_array); + } + { + auto string_wset = _b.make_wset("f3"); + string_wset.add(sfb.tokenize("not a weighty argument").build(), 3); + doc->setValue("f3", string_wset); + } for (uint32_t docId = 10000; docId < 20000; ++docId) { _inv.invertDocument(docId, *doc, {}); myPushDocument(_inv); } - DataStoreBase::MemStats beforeStats = getFeatureStoreMemStats(_fic); + auto beforeStats = getFeatureStoreMemStats(_fic); LOG(info, "Before feature compaction: allocElems=%zu, usedElems=%zu" ", deadElems=%zu, holdElems=%zu" @@ -1049,7 +1053,7 @@ TEST_F(BasicInverterTest, require_that_inversion_is_working) (fieldIndex->takeGenerationGuard())); } myCommit(_fic, *_pushThreads); - DataStoreBase::MemStats duringStats = getFeatureStoreMemStats(_fic); + auto duringStats = getFeatureStoreMemStats(_fic); LOG(info, "During feature compaction: allocElems=%zu, usedElems=%zu" ", deadElems=%zu, holdElems=%zu" @@ -1064,7 +1068,7 @@ TEST_F(BasicInverterTest, require_that_inversion_is_working) duringStats._holdBuffers); guards.clear(); myCommit(_fic, *_pushThreads); - DataStoreBase::MemStats afterStats = getFeatureStoreMemStats(_fic); + auto afterStats = getFeatureStoreMemStats(_fic); LOG(info, "After feature compaction: allocElems=%zu, usedElems=%zu" ", deadElems=%zu, holdElems=%zu" @@ -1133,19 +1137,17 @@ TEST_F(BasicInverterTest, require_that_inversion_is_working) TEST_F(BasicInverterTest, require_that_inverter_handles_remove_via_document_remover) { - Document::UP doc; + StringFieldBuilder sfb(_b); - _b.startDocument("id:ns:searchdocument::1"); - _b.startIndexField("f0").addStr("a").addStr("b").endField(); - _b.startIndexField("f1").addStr("a").addStr("c").endField(); - Document::UP doc1 = _b.endDocument(); - _inv.invertDocument(1, *doc1.get(), {}); + auto doc1 = _b.make_document("id:ns:searchdocument::1"); + doc1->setValue("f0", sfb.tokenize("a b").build()); + doc1->setValue("f1", sfb.tokenize("a c").build()); + _inv.invertDocument(1, *doc1, {}); myPushDocument(_inv); - _b.startDocument("id:ns:searchdocument::2"); - _b.startIndexField("f0").addStr("b").addStr("c").endField(); - Document::UP doc2 = _b.endDocument(); - _inv.invertDocument(2, *doc2.get(), {}); + auto doc2 = _b.make_document("id:ns:searchdocument::2"); + doc2->setValue("f0", sfb.tokenize("b c").build()); + _inv.invertDocument(2, *doc2, {}); myPushDocument(_inv); EXPECT_TRUE(assertPostingList("[1]", find("a", 0))); @@ -1173,136 +1175,71 @@ make_uri_schema() return result; } +DocBuilder::AddFieldsType +make_uri_add_fields() +{ + return [](auto& header) { using namespace document::config_builder; + header.addField("iu", UrlDataType::getInstance().getId()) + .addField("iau", Array(UrlDataType::getInstance().getId())) + .addField("iwu", Wset(UrlDataType::getInstance().getId())); + }; +} + class UriInverterTest : public InverterTest { public: - UriInverterTest() : InverterTest(make_uri_schema()) {} + UriInverterTest() : InverterTest(make_uri_schema(), make_uri_add_fields()) {} }; TEST_F(UriInverterTest, require_that_uri_indexing_is_working) { Document::UP doc; - - _b.startDocument("id:ns:searchdocument::10"); - _b.startIndexField("iu"). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:81/fluke?ab=2#4"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("81"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("4"). - endSubField(). - endField(); - _b.startIndexField("iau"). - startElement(1). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:82/fluke?ab=2#8"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("82"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("8"). - endSubField(). - endElement(). - startElement(1). - startSubField("all"). - addUrlTokenizedString("http://www.flickr.com:82/fluke?ab=2#9"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.flickr.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("82"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("9"). - endSubField(). - endElement(). - endField(); - _b.startIndexField("iwu"). - startElement(4). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:83/fluke?ab=2#12"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("83"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("12"). - endSubField(). - endElement(). - startElement(7). - startSubField("all"). - addUrlTokenizedString("http://www.flickr.com:85/fluke?ab=2#13"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.flickr.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("85"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("13"). - endSubField(). - endElement(). - endField(); - doc = _b.endDocument(); + StringFieldBuilder sfb(_b); + sfb.url_mode(true); + auto url_value = _b.make_url(); + + doc = _b.make_document("id:ns:searchdocument::10"); + url_value.setValue("all", sfb.tokenize("http://www.example.com:81/fluke?ab=2#4").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.example.com").build()); + url_value.setValue("port", sfb.tokenize("81").build()); + url_value.setValue("path", sfb.tokenize("/fluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("4").build()); + doc->setValue("iu", url_value); + auto url_array = _b.make_array("iau"); + url_value.setValue("all", sfb.tokenize("http://www.example.com:82/fluke?ab=2#8").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.example.com").build()); + url_value.setValue("port", sfb.tokenize("82").build()); + url_value.setValue("path", sfb.tokenize("/fluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("8").build()); + url_array.add(url_value); + url_value.setValue("all", sfb.tokenize("http://www.flickr.com:82/fluke?ab=2#9").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.flickr.com").build()); + url_value.setValue("path", sfb.tokenize("/fluke").build()); + url_value.setValue("fragment", sfb.tokenize("9").build()); + url_array.add(url_value); + doc->setValue("iau", url_array); + auto url_wset = _b.make_wset("iwu"); + url_value.setValue("all", sfb.tokenize("http://www.example.com:83/fluke?ab=2#12").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.example.com").build()); + url_value.setValue("port", sfb.tokenize("83").build()); + url_value.setValue("path", sfb.tokenize("/fluke").alt_word("altfluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("12").build()); + url_wset.add(url_value, 4); + url_value.setValue("all", sfb.tokenize("http://www.flickr.com:85/fluke?ab=2#13").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.flickr.com").build()); + url_value.setValue("port", sfb.tokenize("85").build()); + url_value.setValue("path", sfb.tokenize("/fluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("13").build()); + url_wset.add(url_value, 7); + doc->setValue("iwu", url_wset); _inv.invertDocument(10, *doc, {}); myPushDocument(_inv); @@ -1361,21 +1298,16 @@ TEST_F(UriInverterTest, require_that_uri_indexing_is_working) class CjkInverterTest : public InverterTest { public: - CjkInverterTest() : InverterTest(make_single_field_schema()) {} + CjkInverterTest() : InverterTest(make_single_field_schema(), make_single_add_fields()) {} }; TEST_F(CjkInverterTest, require_that_cjk_indexing_is_working) { Document::UP doc; + StringFieldBuilder sfb(_b); - _b.startDocument("id:ns:searchdocument::10"); - _b.startIndexField("f0"). - addStr("我就是那个"). - setAutoSpace(false). - addStr("大灰狼"). - setAutoSpace(true). - endField(); - doc = _b.endDocument(); + doc = _b.make_document("id:ns:searchdocument::10"); + doc->setValue("f0", sfb.word("我就是那个").word("大灰狼").build()); _inv.invertDocument(10, *doc, {}); myPushDocument(_inv); diff --git a/searchlib/src/tests/memoryindex/field_inverter/field_inverter_test.cpp b/searchlib/src/tests/memoryindex/field_inverter/field_inverter_test.cpp index ed049a82c42..db97846dc30 100644 --- a/searchlib/src/tests/memoryindex/field_inverter/field_inverter_test.cpp +++ b/searchlib/src/tests/memoryindex/field_inverter/field_inverter_test.cpp @@ -1,8 +1,14 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#include <vespa/document/repo/fixedtyperepo.h> -#include <vespa/searchlib/index/docbuilder.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/arrayfieldvalue.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/fieldvalue/weightedsetfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> +#include <vespa/searchcommon/common/schema.h> #include <vespa/searchlib/index/field_length_calculator.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/memoryindex/field_index_remover.h> #include <vespa/searchlib/memoryindex/field_inverter.h> #include <vespa/searchlib/memoryindex/word_store.h> @@ -13,11 +19,14 @@ namespace search { +using document::ArrayFieldValue; using document::Document; -using index::DocBuilder; +using document::WeightedSetFieldValue; using index::Schema; using index::schema::CollectionType; using index::schema::DataType; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using namespace index; @@ -28,81 +37,79 @@ namespace { Document::UP makeDoc10(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::10"); - b.startIndexField("f0"). - addStr("a").addStr("b").addStr("c").addStr("d"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::10"); + doc->setValue("f0", sfb.tokenize("a b c d").build()); + return doc; } Document::UP makeDoc11(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::11"); - b.startIndexField("f0"). - addStr("a").addStr("b").addStr("e").addStr("f"). - endField(); - b.startIndexField("f1"). - addStr("a").addStr("g"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::11"); + doc->setValue("f0", sfb.tokenize("a b e f").build()); + doc->setValue("f1", sfb.tokenize("a g").build()); + return doc; } Document::UP makeDoc12(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::12"); - b.startIndexField("f0"). - addStr("h").addStr("doc12"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::12"); + doc->setValue("f0", sfb.tokenize("h doc12").build()); + return doc; } Document::UP makeDoc13(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::13"); - b.startIndexField("f0"). - addStr("i").addStr("doc13"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::13"); + doc->setValue("f0", sfb.tokenize("i doc13").build()); + return doc; } Document::UP makeDoc14(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::14"); - b.startIndexField("f0"). - addStr("j").addStr("doc14"). - endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::14"); + doc->setValue("f0", sfb.tokenize("j doc14").build()); + return doc; } Document::UP makeDoc15(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::15"); - return b.endDocument(); + return b.make_document("id:ns:searchdocument::15"); } Document::UP makeDoc16(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::16"); - b.startIndexField("f0").addStr("foo").addStr("bar").addStr("baz"). - addTermAnnotation("altbaz").addStr("y").addTermAnnotation("alty"). - addStr("z").endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::16"); + doc->setValue("f0", sfb.tokenize("foo bar baz").alt_word("altbaz").tokenize(" y").alt_word("alty").tokenize(" z").build()); + return doc; } Document::UP makeDoc17(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::17"); - b.startIndexField("f1").addStr("foo0").addStr("bar0").endField(); - b.startIndexField("f2").startElement(1).addStr("foo").addStr("bar").endElement().startElement(1).addStr("bar").endElement().endField(); - b.startIndexField("f3").startElement(3).addStr("foo2").addStr("bar2").endElement().startElement(4).addStr("bar2").endElement().endField(); - return b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::17"); + doc->setValue("f1", sfb.tokenize("foo0 bar0").build()); + auto string_array = b.make_array("f2"); + string_array.add(sfb.tokenize("foo bar").build()); + string_array.add(sfb.tokenize("bar").build()); + doc->setValue("f2", string_array); + auto string_wset = b.make_wset("f3"); + string_wset.add(sfb.tokenize("foo2 bar2").build(), 3); + string_wset.add(sfb.tokenize("bar2").build(), 4); + doc->setValue("f3", string_wset); + return doc; } vespalib::string corruptWord = "corruptWord"; @@ -110,9 +117,9 @@ vespalib::string corruptWord = "corruptWord"; Document::UP makeCorruptDocument(DocBuilder &b, size_t wordOffset) { - b.startDocument("id:ns:searchdocument::18"); - b.startIndexField("f0").addStr("before").addStr(corruptWord).addStr("after").addStr("z").endField(); - auto doc = b.endDocument(); + StringFieldBuilder sfb(b); + auto doc = b.make_document("id:ns:searchdocument::18"); + doc->setValue("f0", sfb.tokenize("before ").word(corruptWord).tokenize(" after z").build()); vespalib::nbostream stream; doc->serialize(stream); std::vector<char> raw; @@ -127,7 +134,7 @@ makeCorruptDocument(DocBuilder &b, size_t wordOffset) } vespalib::nbostream badstream; badstream.write(&raw[0], raw.size()); - return std::make_unique<Document>(*b.getDocumentTypeRepo(), badstream); + return std::make_unique<Document>(b.get_repo(), badstream); } } @@ -151,9 +158,21 @@ struct FieldInverterTest : public ::testing::Test { return schema; } + static DocBuilder::AddFieldsType + make_add_fields() + { + return [](auto& header) { using namespace document::config_builder; + using DataType = document::DataType; + header.addField("f0", DataType::T_STRING) + .addField("f1", DataType::T_STRING) + .addField("f2", Array(DataType::T_STRING)) + .addField("f3", Wset(DataType::T_STRING)); + }; + } + FieldInverterTest() : _schema(makeSchema()), - _b(_schema), + _b(make_add_fields()), _word_store(), _remover(_word_store), _inserter_backend(), diff --git a/searchlib/src/tests/memoryindex/memory_index/CMakeLists.txt b/searchlib/src/tests/memoryindex/memory_index/CMakeLists.txt index e5915cca6f3..0a771d98b90 100644 --- a/searchlib/src/tests/memoryindex/memory_index/CMakeLists.txt +++ b/searchlib/src/tests/memoryindex/memory_index/CMakeLists.txt @@ -3,7 +3,7 @@ vespa_add_executable(searchlib_memory_index_test_app TEST SOURCES memory_index_test.cpp DEPENDS - searchlib + searchlib_test GTest::GTest ) vespa_add_test(NAME searchlib_memory_index_test_app COMMAND searchlib_memory_index_test_app) diff --git a/searchlib/src/tests/memoryindex/memory_index/memory_index_test.cpp b/searchlib/src/tests/memoryindex/memory_index/memory_index_test.cpp index b3ea948dfa7..b5b8e0c7a7c 100644 --- a/searchlib/src/tests/memoryindex/memory_index/memory_index_test.cpp +++ b/searchlib/src/tests/memoryindex/memory_index/memory_index_test.cpp @@ -1,11 +1,15 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/searchlib/common/scheduletaskcallback.h> #include <vespa/searchlib/fef/matchdata.h> #include <vespa/searchlib/fef/matchdatalayout.h> #include <vespa/searchlib/fef/termfieldmatchdata.h> -#include <vespa/searchlib/index/docbuilder.h> #include <vespa/searchlib/index/i_field_length_inspector.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/memoryindex/memory_index.h> #include <vespa/searchlib/query/tree/simplequery.h> #include <vespa/searchlib/queryeval/booleanmatchiteratorwrapper.h> @@ -32,6 +36,8 @@ using vespalib::makeLambdaTask; using search::query::Node; using search::query::SimplePhrase; using search::query::SimpleStringTerm; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using vespalib::ISequencedTaskExecutor; using vespalib::SequencedTaskExecutor; using namespace search::fef; @@ -59,6 +65,12 @@ struct MySetup : public IFieldLengthInspector { } return FieldLengthInfo(); } + void add_fields(document::config_builder::Struct& header) const { + for (uint32_t i = 0; i < schema.getNumIndexFields(); ++i) { + auto& field = schema.getIndexField(i); + header.addField(field.getName(), document::DataType::T_STRING); + } + } }; @@ -71,30 +83,37 @@ struct Index { std::unique_ptr<ISequencedTaskExecutor> _pushThreads; MemoryIndex index; DocBuilder builder; + StringFieldBuilder sfb; + std::unique_ptr<Document> builder_doc; uint32_t docid; std::string currentField; + bool add_space; Index(const MySetup &setup); ~Index(); void closeField() { if (!currentField.empty()) { - builder.endField(); + builder_doc->setValue(currentField, sfb.build()); currentField.clear(); } } Index &doc(uint32_t id) { docid = id; - builder.startDocument(vespalib::make_string("id:ns:searchdocument::%u", id)); + builder_doc = builder.make_document(vespalib::make_string("id:ns:searchdocument::%u", id)); return *this; } Index &field(const std::string &name) { closeField(); - builder.startIndexField(name); currentField = name; + add_space = false; return *this; } Index &add(const std::string &token) { - builder.addStr(token); + if (add_space) { + sfb.space(); + } + add_space = true; + sfb.word(token); return *this; } void internalSyncCommit() { @@ -106,7 +125,7 @@ struct Index { } Document::UP commit() { closeField(); - Document::UP d = builder.endDocument(); + Document::UP d = std::move(builder_doc); index.insertDocument(docid, *d, {}); internalSyncCommit(); return d; @@ -133,9 +152,12 @@ Index::Index(const MySetup &setup) _invertThreads(SequencedTaskExecutor::create(invert_executor, 2)), _pushThreads(SequencedTaskExecutor::create(push_executor, 2)), index(schema, setup, *_invertThreads, *_pushThreads), - builder(schema), + builder([&setup](auto& header) { setup.add_fields(header); }), + sfb(builder), + builder_doc(), docid(1), - currentField() + currentField(), + add_space(false) { } Index::~Index() = default; diff --git a/searchlib/src/tests/memoryindex/url_field_inverter/url_field_inverter_test.cpp b/searchlib/src/tests/memoryindex/url_field_inverter/url_field_inverter_test.cpp index 969f483eef6..b3892d5d69a 100644 --- a/searchlib/src/tests/memoryindex/url_field_inverter/url_field_inverter_test.cpp +++ b/searchlib/src/tests/memoryindex/url_field_inverter/url_field_inverter_test.cpp @@ -1,12 +1,22 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/searchlib/memoryindex/url_field_inverter.h> +#include <vespa/document/datatype/urldatatype.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/arrayfieldvalue.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/document/fieldvalue/structfieldvalue.h> +#include <vespa/document/fieldvalue/weightedsetfieldvalue.h> +#include <vespa/document/repo/configbuilder.h> #include <vespa/document/repo/fixedtyperepo.h> -#include <vespa/searchlib/index/docbuilder.h> +#include <vespa/searchcommon/common/schema.h> #include <vespa/searchlib/index/field_length_calculator.h> +#include <vespa/searchlib/index/schema_index_fields.h> #include <vespa/searchlib/memoryindex/field_index_remover.h> #include <vespa/searchlib/memoryindex/field_inverter.h> -#include <vespa/searchlib/memoryindex/url_field_inverter.h> #include <vespa/searchlib/memoryindex/word_store.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/searchlib/test/string_field_builder.h> #include <vespa/searchlib/test/memoryindex/ordered_field_index_inserter.h> #include <vespa/searchlib/test/memoryindex/ordered_field_index_inserter_backend.h> #include <vespa/vespalib/gtest/gtest.h> @@ -14,8 +24,14 @@ namespace search { using document::Document; +using document::ArrayFieldValue; +using document::StructFieldValue; +using document::UrlDataType; +using document::WeightedSetFieldValue; using index::schema::CollectionType; using index::schema::DataType; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; using namespace index; @@ -28,151 +44,79 @@ const vespalib::string url = "url"; Document::UP makeDoc10Single(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::10"); - b.startIndexField("url"). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:81/fluke?ab=2#4"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("81"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - addTermAnnotation("altfluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("4"). - endSubField(). - endField(); - return b.endDocument(); + auto doc = b.make_document("id:ns:searchdocument::10"); + auto url_value = b.make_struct("url"); + StringFieldBuilder sfb(b); + sfb.url_mode(true); + url_value.setValue("all", sfb.tokenize("http://www.example.com:81/fluke?ab=2#4").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.example.com").build()); + url_value.setValue("port", sfb.tokenize("81").build()); + url_value.setValue("path", sfb.tokenize("/fluke").alt_word("altfluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("4").build()); + doc->setValue("url", url_value); + return doc; } Document::UP makeDoc10Array(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::10"); - b.startIndexField("url"). - startElement(1). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:82/fluke?ab=2#8"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("82"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - addTermAnnotation("altfluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("8"). - endSubField(). - endElement(). - startElement(1). - startSubField("all"). - addUrlTokenizedString("http://www.flickr.com:82/fluke?ab=2#9"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.flickr.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("82"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("9"). - endSubField(). - endElement(). - endField(); - return b.endDocument(); + auto doc = b.make_document("id:ns:searchdocument::10"); + StringFieldBuilder sfb(b); + sfb.url_mode(true); + auto url_array = b.make_array("url"); + auto url_value = b.make_url(); + url_value.setValue("all", sfb.tokenize("http://www.example.com:82/fluke?ab=2#8").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.example.com").build()); + url_value.setValue("port", sfb.tokenize("82").build()); + url_value.setValue("path", sfb.tokenize("/fluke").alt_word("altfluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("8").build()); + url_array.add(url_value); + url_value.setValue("all", sfb.tokenize("http://www.flickr.com:82/fluke?ab=2#9").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.flickr.com").build()); + url_value.setValue("path", sfb.tokenize("/fluke").build()); + url_value.setValue("fragment", sfb.tokenize("9").build()); + url_array.add(url_value); + doc->setValue("url", url_array); + return doc; } Document::UP makeDoc10WeightedSet(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::10"); - b.startIndexField("url"). - startElement(4). - startSubField("all"). - addUrlTokenizedString("http://www.example.com:83/fluke?ab=2#12"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.example.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("83"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - addTermAnnotation("altfluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("12"). - endSubField(). - endElement(). - startElement(7). - startSubField("all"). - addUrlTokenizedString("http://www.flickr.com:85/fluke?ab=2#13"). - endSubField(). - startSubField("scheme"). - addUrlTokenizedString("http"). - endSubField(). - startSubField("host"). - addUrlTokenizedString("www.flickr.com"). - endSubField(). - startSubField("port"). - addUrlTokenizedString("85"). - endSubField(). - startSubField("path"). - addUrlTokenizedString("/fluke"). - endSubField(). - startSubField("query"). - addUrlTokenizedString("ab=2"). - endSubField(). - startSubField("fragment"). - addUrlTokenizedString("13"). - endSubField(). - endElement(). - endField(); - return b.endDocument(); + auto doc = b.make_document("id:ns:searchdocument::10"); + StringFieldBuilder sfb(b); + sfb.url_mode(true); + auto url_wset = b.make_wset("url"); + auto url_value = b.make_url(); + url_value.setValue("all", sfb.tokenize("http://www.example.com:83/fluke?ab=2#12").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.example.com").build()); + url_value.setValue("port", sfb.tokenize("83").build()); + url_value.setValue("path", sfb.tokenize("/fluke").alt_word("altfluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("12").build()); + url_wset.add(url_value, 4); + url_value.setValue("all", sfb.tokenize("http://www.flickr.com:85/fluke?ab=2#13").build()); + url_value.setValue("scheme", sfb.tokenize("http").build()); + url_value.setValue("host", sfb.tokenize("www.flickr.com").build()); + url_value.setValue("port", sfb.tokenize("85").build()); + url_value.setValue("path", sfb.tokenize("/fluke").build()); + url_value.setValue("query", sfb.tokenize("ab=2").build()); + url_value.setValue("fragment", sfb.tokenize("13").build()); + url_wset.add(url_value, 7); + doc->setValue("url", url_wset); + return doc; } Document::UP makeDoc10Empty(DocBuilder &b) { - b.startDocument("id:ns:searchdocument::10"); - return b.endDocument(); + return b.make_document("id:ns:searchdocument::10"); } } @@ -195,9 +139,10 @@ struct UrlFieldInverterTest : public ::testing::Test { return schema; } - UrlFieldInverterTest(Schema::CollectionType collectionType) + UrlFieldInverterTest(Schema::CollectionType collectionType, + DocBuilder::AddFieldsType add_fields) : _schema(makeSchema(collectionType)), - _b(_schema), + _b(add_fields), _word_store(), _remover(_word_store), _inserter_backend(), @@ -250,16 +195,32 @@ struct UrlFieldInverterTest : public ::testing::Test { UrlFieldInverterTest::~UrlFieldInverterTest() = default; +DocBuilder::AddFieldsType +add_single_url = [](auto& header) { + header.addField("url", UrlDataType::getInstance().getId()); }; + +DocBuilder::AddFieldsType +add_array_url = [](auto& header) { + using namespace document::config_builder; + header.addField("url", Array(UrlDataType::getInstance().getId())); }; + +DocBuilder::AddFieldsType +add_wset_url = [](auto& header) { + using namespace document::config_builder; + header.addField("url", Wset(UrlDataType::getInstance().getId())); }; + + + struct SingleInverterTest : public UrlFieldInverterTest { - SingleInverterTest() : UrlFieldInverterTest(CollectionType::SINGLE) {} + SingleInverterTest() : UrlFieldInverterTest(CollectionType::SINGLE, add_single_url) {} }; struct ArrayInverterTest : public UrlFieldInverterTest { - ArrayInverterTest() : UrlFieldInverterTest(CollectionType::ARRAY) {} + ArrayInverterTest() : UrlFieldInverterTest(CollectionType::ARRAY, add_array_url) {} }; struct WeightedSetInverterTest : public UrlFieldInverterTest { - WeightedSetInverterTest() : UrlFieldInverterTest(CollectionType::WEIGHTEDSET) {} + WeightedSetInverterTest() : UrlFieldInverterTest(CollectionType::WEIGHTEDSET, add_wset_url) {} }; diff --git a/searchlib/src/tests/predicate/simple_index_test.cpp b/searchlib/src/tests/predicate/simple_index_test.cpp index dfa8c12deec..7bf52680782 100644 --- a/searchlib/src/tests/predicate/simple_index_test.cpp +++ b/searchlib/src/tests/predicate/simple_index_test.cpp @@ -74,7 +74,7 @@ struct Fixture { Fixture() : _generation_holder(), _limit_provider(), _index(_generation_holder, _limit_provider, config) {} ~Fixture() { - _generation_holder.clearHoldLists(); + _generation_holder.reclaim_all(); } SimpleIndex<MyData> &index() { return _index; diff --git a/searchlib/src/tests/sortspec/multilevelsort.cpp b/searchlib/src/tests/sortspec/multilevelsort.cpp index 82bdf99ab2c..87dc6608c3f 100644 --- a/searchlib/src/tests/sortspec/multilevelsort.cpp +++ b/searchlib/src/tests/sortspec/multilevelsort.cpp @@ -16,11 +16,6 @@ LOG_SETUP("multilevelsort_test"); using namespace search; -typedef FastS_SortSpec::VectorRef VectorRef; -typedef IntegerAttributeTemplate<int8_t> Int8; -typedef IntegerAttributeTemplate<int16_t> Int16; -typedef IntegerAttributeTemplate<int32_t> Int32; -typedef IntegerAttributeTemplate<int64_t> Int64; typedef FloatingPointAttributeTemplate<float> Float; typedef FloatingPointAttributeTemplate<double> Double; typedef std::map<std::string, AttributeVector::SP > VectorMap; @@ -53,16 +48,16 @@ public: }; private: template<typename T> - T getRandomValue() { + static T getRandomValue() { T min = std::numeric_limits<T>::min(); T max = std::numeric_limits<T>::max(); return static_cast<T>(double(min) + (double(max) - double(min)) * (double(rand()) / double(RAND_MAX))); } template<typename T> - void fill(IntegerAttribute *attr, uint32_t size, uint32_t unique = 0); + static void fill(IntegerAttribute *attr, uint32_t size, uint32_t unique = 0); template<typename T> - void fill(FloatingPointAttribute *attr, uint32_t size, uint32_t unique = 0); - void fill(StringAttribute *attr, uint32_t size, const std::vector<std::string> &values); + static void fill(FloatingPointAttribute *attr, uint32_t size, uint32_t unique = 0); + static void fill(StringAttribute *attr, uint32_t size, const std::vector<std::string> &values); template <typename V> int compareTemplate(AttributeVector *vector, uint32_t a, uint32_t b); int compare(AttributeVector *vector, AttrType type, uint32_t a, uint32_t b); @@ -181,7 +176,7 @@ MultilevelSortTest::compare(AttributeVector *vector, AttrType type, uint32_t a, } else if (type == DOUBLE) { return compareTemplate<double>(vector, a, b); } else if (type == STRING) { - StringAttribute *vString = static_cast<StringAttribute*>(vector); + StringAttribute *vString = dynamic_cast<StringAttribute*>(vector); const char *va = vString->get(a); const char *vb = vString->get(b); std::string sa(va); @@ -199,106 +194,99 @@ MultilevelSortTest::compare(AttributeVector *vector, AttrType type, uint32_t a, } void -MultilevelSortTest::sortAndCheck(const std::vector<Spec> &spec, uint32_t num, +MultilevelSortTest::sortAndCheck(const std::vector<Spec> &specs, uint32_t num, uint32_t unique, const std::vector<std::string> &strValues) { VectorMap vec; // generate attribute vectors - for (uint32_t i = 0; i < spec.size(); ++i) { - std::string name = spec[i]._name; - AttrType type = spec[i]._type; + for (const auto & spec : specs) { + std::string name = spec._name; + AttrType type = spec._type; if (type == INT8) { Config cfg(BasicType::INT8, CollectionType::SINGLE); vec[name] = AttributeFactory::createAttribute(name, cfg); - fill<int8_t>(static_cast<IntegerAttribute *>(vec[name].get()), num, unique); + fill<int8_t>(dynamic_cast<IntegerAttribute *>(vec[name].get()), num, unique); } else if (type == INT16) { Config cfg(BasicType::INT16, CollectionType::SINGLE); vec[name] = AttributeFactory::createAttribute(name, cfg); - fill<int16_t>(static_cast<IntegerAttribute *>(vec[name].get()), num, unique); + fill<int16_t>(dynamic_cast<IntegerAttribute *>(vec[name].get()), num, unique); } else if (type == INT32) { Config cfg(BasicType::INT32, CollectionType::SINGLE); vec[name] = AttributeFactory::createAttribute(name, cfg); - fill<int32_t>(static_cast<IntegerAttribute *>(vec[name].get()), num, unique); + fill<int32_t>(dynamic_cast<IntegerAttribute *>(vec[name].get()), num, unique); } else if (type == INT64) { Config cfg(BasicType::INT64, CollectionType::SINGLE); vec[name] = AttributeFactory::createAttribute(name, cfg); - fill<int64_t>(static_cast<IntegerAttribute *>(vec[name].get()), num, unique); + fill<int64_t>(dynamic_cast<IntegerAttribute *>(vec[name].get()), num, unique); } else if (type == FLOAT) { Config cfg(BasicType::FLOAT, CollectionType::SINGLE); vec[name] = AttributeFactory::createAttribute(name, cfg); - fill<float>(static_cast<FloatingPointAttribute *>(vec[name].get()), num, unique); + fill<float>(dynamic_cast<FloatingPointAttribute *>(vec[name].get()), num, unique); } else if (type == DOUBLE) { Config cfg(BasicType::DOUBLE, CollectionType::SINGLE); vec[name] = AttributeFactory::createAttribute(name, cfg); - fill<double>(static_cast<FloatingPointAttribute *>(vec[name].get()), num, unique); + fill<double>(dynamic_cast<FloatingPointAttribute *>(vec[name].get()), num, unique); } else if (type == STRING) { Config cfg(BasicType::STRING, CollectionType::SINGLE); vec[name] = AttributeFactory::createAttribute(name, cfg); - fill(static_cast<StringAttribute *>(vec[name].get()), num, strValues); + fill(dynamic_cast<StringAttribute *>(vec[name].get()), num, strValues); } if (vec[name]) vec[name]->commit(); } - RankedHit *hits = new RankedHit[num]; + std::vector<RankedHit> hits; + hits.reserve(num); for (uint32_t i = 0; i < num; ++i) { - hits[i]._docId = i; - hits[i]._rankValue = getRandomValue<uint32_t>(); + hits.emplace_back(i, getRandomValue<uint32_t>()); } vespalib::TestClock clock; vespalib::Doom doom(clock.clock(), vespalib::steady_time::max()); search::uca::UcaConverterFactory ucaFactory; - FastS_SortSpec sorter(7, doom, ucaFactory); + FastS_SortSpec sorter("no-metastore", 7, doom, ucaFactory); // init sorter with sort data - for(uint32_t i = 0; i < spec.size(); ++i) { + for (const auto & spec : specs) { AttributeGuard ag; - if (spec[i]._type == RANK) { - sorter._vectors.push_back - (VectorRef(spec[i]._asc ? FastS_SortSpec::ASC_RANK : - FastS_SortSpec::DESC_RANK, nullptr, nullptr)); - } else if (spec[i]._type == DOCID) { - sorter._vectors.push_back - (VectorRef(spec[i]._asc ? FastS_SortSpec::ASC_DOCID : - FastS_SortSpec::DESC_DOCID, nullptr, nullptr)); + if (spec._type == RANK) { + sorter._vectors.emplace_back(spec._asc ? FastS_SortSpec::ASC_RANK : FastS_SortSpec::DESC_RANK, nullptr, nullptr); + } else if (spec._type == DOCID) { + sorter._vectors.emplace_back(spec._asc ? FastS_SortSpec::ASC_DOCID : FastS_SortSpec::DESC_DOCID, nullptr, nullptr); } else { - const search::attribute::IAttributeVector * v = vec[spec[i]._name].get(); - sorter._vectors.push_back - (VectorRef(spec[i]._asc ? FastS_SortSpec::ASC_VECTOR : - FastS_SortSpec::DESC_VECTOR, v, nullptr)); + const search::attribute::IAttributeVector * v = vec[spec._name].get(); + sorter._vectors.emplace_back(spec._asc ? FastS_SortSpec::ASC_VECTOR : FastS_SortSpec::DESC_VECTOR, v, nullptr); } } vespalib::Timer timer; - sorter.sortResults(hits, num, num); + sorter.sortResults(&hits[0], num, num); LOG(info, "sort time = %" PRId64 " ms", vespalib::count_ms(timer.elapsed())); - uint32_t *offsets = new uint32_t[num + 1]; - char *buf = new char[sorter.getSortDataSize(0, num)]; - sorter.copySortData(0, num, offsets, buf); + std::vector<uint32_t> offsets(num + 1, 0); + auto buf = std::make_unique<char []>(sorter.getSortDataSize(0, num)); + sorter.copySortData(0, num, &offsets[0], buf.get()); // check results for (uint32_t i = 0; i < num - 1; ++i) { - for (uint32_t j = 0; j < spec.size(); ++j) { + for (const Spec & spec : specs) { int cmp = 0; - if (spec[j]._type == RANK) { + if (spec._type == RANK) { if (hits[i].getRank() < hits[i+1].getRank()) { cmp = -1; } else if (hits[i].getRank() > hits[i+1].getRank()) { cmp = 1; } - } else if (spec[j]._type == DOCID) { + } else if (spec._type == DOCID) { if (hits[i].getDocId() < hits[i+1].getDocId()) { cmp = -1; } else if (hits[i].getDocId() > hits[i+1].getDocId()) { cmp = 1; } } else { - AttributeVector *av = vec[spec[j]._name].get(); - cmp = compare(av, spec[j]._type, - hits[i].getDocId(), hits[i+1].getDocId()); + AttributeVector *av = vec[spec._name].get(); + cmp = compare(av, spec._type, hits[i].getDocId(), hits[i+1].getDocId()); } - if (spec[j]._asc) { + if (spec._asc) { EXPECT_TRUE(cmp <= 0); if (cmp < 0) { break; @@ -311,56 +299,51 @@ MultilevelSortTest::sortAndCheck(const std::vector<Spec> &spec, uint32_t num, } } // check binary sort data - uint32_t minLen = std::min(sorter._sortDataArray[i]._len, - sorter._sortDataArray[i+1]._len); + uint32_t minLen = std::min(sorter._sortDataArray[i]._len, sorter._sortDataArray[i+1]._len); int cmp = memcmp(&sorter._binarySortData[0] + sorter._sortDataArray[i]._idx, &sorter._binarySortData[0] + sorter._sortDataArray[i+1]._idx, minLen); EXPECT_TRUE(cmp <= 0); EXPECT_TRUE(sorter._sortDataArray[i]._len == (offsets[i+1] - offsets[i])); cmp = memcmp(&sorter._binarySortData[0] + sorter._sortDataArray[i]._idx, - buf + offsets[i], sorter._sortDataArray[i]._len); + buf.get() + offsets[i], sorter._sortDataArray[i]._len); EXPECT_TRUE(cmp == 0); } EXPECT_TRUE(sorter._sortDataArray[num-1]._len == (offsets[num] - offsets[num-1])); int cmp = memcmp(&sorter._binarySortData[0] + sorter._sortDataArray[num-1]._idx, - buf + offsets[num-1], sorter._sortDataArray[num-1]._len); + buf.get() + offsets[num-1], sorter._sortDataArray[num-1]._len); EXPECT_TRUE(cmp == 0); - - delete [] hits; - delete [] offsets; - delete [] buf; } void MultilevelSortTest::testSort() { { std::vector<Spec> spec; - spec.push_back(Spec("int8", INT8)); - spec.push_back(Spec("int16", INT16)); - spec.push_back(Spec("int32", INT32)); - spec.push_back(Spec("int64", INT64)); - spec.push_back(Spec("float", FLOAT)); - spec.push_back(Spec("double", DOUBLE)); - spec.push_back(Spec("string", STRING)); - spec.push_back(Spec("rank", RANK)); - spec.push_back(Spec("docid", DOCID)); + spec.emplace_back("int8", INT8); + spec.emplace_back("int16", INT16); + spec.emplace_back("int32", INT32); + spec.emplace_back("int64", INT64); + spec.emplace_back("float", FLOAT); + spec.emplace_back("double", DOUBLE); + spec.emplace_back("string", STRING); + spec.emplace_back("rank", RANK); + spec.emplace_back("docid", DOCID); std::vector<std::string> strValues; - strValues.push_back("applications"); - strValues.push_back("places"); - strValues.push_back("system"); - strValues.push_back("vespa search core"); + strValues.emplace_back("applications"); + strValues.emplace_back("places"); + strValues.emplace_back("system"); + strValues.emplace_back("vespa search core"); srand(12345); sortAndCheck(spec, 5000, 4, strValues); srand(time(nullptr)); sortAndCheck(spec, 5000, 4, strValues); - strValues.push_back("multilevelsort"); - strValues.push_back("trondheim"); - strValues.push_back("ubuntu"); - strValues.push_back("fastserver4"); + strValues.emplace_back("multilevelsort"); + strValues.emplace_back("trondheim"); + strValues.emplace_back("ubuntu"); + strValues.emplace_back("fastserver4"); srand(56789); sortAndCheck(spec, 5000, 8, strValues); @@ -403,7 +386,7 @@ TEST("test that [docid] translates to [lid][paritionid]") { vespalib::TestClock clock; vespalib::Doom doom(clock.clock(), vespalib::steady_time::max()); search::uca::UcaConverterFactory ucaFactory; - FastS_SortSpec asc(7, doom, ucaFactory); + FastS_SortSpec asc("no-metastore", 7, doom, ucaFactory); RankedHit hits[2] = {RankedHit(91, 0.0), RankedHit(3, 2.0)}; search::AttributeManager mgr; search::AttributeContext ac(mgr); @@ -420,7 +403,7 @@ TEST("test that [docid] translates to [lid][paritionid]") { EXPECT_EQUAL(6u, sr2.second); EXPECT_EQUAL(0, memcmp(SECOND_ASC, sr2.first, 6)); - FastS_SortSpec desc(7, doom, ucaFactory); + FastS_SortSpec desc("no-metastore", 7, doom, ucaFactory); desc.Init("-[docid]", ac); desc.initWithoutSorting(hits, 2); sr1 = desc.getSortRef(0); @@ -431,4 +414,45 @@ TEST("test that [docid] translates to [lid][paritionid]") { EXPECT_EQUAL(0, memcmp(SECOND_DESC, sr2.first, 6)); } +TEST("test that [docid] uses attribute when one exists") { + vespalib::TestClock clock; + vespalib::Doom doom(clock.clock(), vespalib::steady_time::max()); + search::uca::UcaConverterFactory ucaFactory; + FastS_SortSpec asc("metastore", 7, doom, ucaFactory); + RankedHit hits[2] = {RankedHit(91, 0.0), RankedHit(3, 2.0)}; + Config cfg(BasicType::INT64, CollectionType::SINGLE); + auto metastore = AttributeFactory::createAttribute("metastore", cfg); + ASSERT_TRUE(metastore->addDocs(100)); + auto * iattr = dynamic_cast<IntegerAttribute *>(metastore.get()); + for (uint32_t lid(0); lid < 100; lid++) { + iattr->update(lid, lid); + } + metastore->commit(); + search::AttributeManager mgr; + mgr.add(metastore); + search::AttributeContext ac(mgr); + EXPECT_TRUE(asc.Init("+[docid]", ac)); + asc.initWithoutSorting(hits, 2); + constexpr uint8_t FIRST_ASC[8] = {0x80,0,0,0,0,0,0,91}; + constexpr uint8_t SECOND_ASC[8] = {0x80,0,0,0,0,0,0,3}; + constexpr uint8_t FIRST_DESC[8] = {0x7f,0xff,0xff,0xff,0xff,0xff,0xff,0xff - 91}; + constexpr uint8_t SECOND_DESC[8] = {0x7f,0xff,0xff,0xff,0xff,0xff,0xff,0xff - 3}; + auto sr1 = asc.getSortRef(0); + EXPECT_EQUAL(8u, sr1.second); + EXPECT_EQUAL(0, memcmp(FIRST_ASC, sr1.first, 8)); + auto sr2 = asc.getSortRef(1); + EXPECT_EQUAL(8u, sr2.second); + EXPECT_EQUAL(0, memcmp(SECOND_ASC, sr2.first, 8)); + + FastS_SortSpec desc("metastore", 7, doom, ucaFactory); + desc.Init("-[docid]", ac); + desc.initWithoutSorting(hits, 2); + sr1 = desc.getSortRef(0); + EXPECT_EQUAL(8u, sr1.second); + EXPECT_EQUAL(0, memcmp(FIRST_DESC, sr1.first, 8)); + sr2 = desc.getSortRef(1); + EXPECT_EQUAL(8u, sr2.second); + EXPECT_EQUAL(0, memcmp(SECOND_DESC, sr2.first, 8)); +} + TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/searchlib/src/tests/tensor/dense_tensor_store/dense_tensor_store_test.cpp b/searchlib/src/tests/tensor/dense_tensor_store/dense_tensor_store_test.cpp index 149662cd266..26ef57aab65 100644 --- a/searchlib/src/tests/tensor/dense_tensor_store/dense_tensor_store_test.cpp +++ b/searchlib/src/tests/tensor/dense_tensor_store/dense_tensor_store_test.cpp @@ -32,15 +32,15 @@ struct Fixture {} void assertSetAndGetTensor(const TensorSpec &tensorSpec) { Value::UP expTensor = makeTensor(tensorSpec); - EntryRef ref = store.setTensor(*expTensor); - Value::UP actTensor = store.getTensor(ref); + EntryRef ref = store.store_tensor(*expTensor); + Value::UP actTensor = store.get_tensor(ref); EXPECT_EQUAL(*expTensor, *actTensor); assertTensorView(ref, *expTensor); } void assertEmptyTensor(const TensorSpec &tensorSpec) { Value::UP expTensor = makeTensor(tensorSpec); EntryRef ref; - Value::UP actTensor = store.getTensor(ref); + Value::UP actTensor = store.get_tensor(ref); EXPECT_TRUE(actTensor.get() == nullptr); assertTensorView(ref, *expTensor); } diff --git a/searchlib/src/tests/tensor/direct_tensor_store/direct_tensor_store_test.cpp b/searchlib/src/tests/tensor/direct_tensor_store/direct_tensor_store_test.cpp index 1574e7d38f1..cb9fa8522a8 100644 --- a/searchlib/src/tests/tensor/direct_tensor_store/direct_tensor_store_test.cpp +++ b/searchlib/src/tests/tensor/direct_tensor_store/direct_tensor_store_test.cpp @@ -58,11 +58,11 @@ public: DirectTensorStoreTest() : store() {} virtual ~DirectTensorStoreTest() { - store.clearHoldLists(); + store.reclaim_all_memory(); } void expect_tensor(const Value* exp, EntryRef ref) { - const auto* act = store.get_tensor(ref); + const auto* act = store.get_tensor_ptr(ref); ASSERT_TRUE(act); EXPECT_EQ(exp, act); } @@ -81,7 +81,7 @@ TEST_F(DirectTensorStoreTest, heap_allocated_memory_is_tracked) store.store_tensor(make_tensor(5)); auto mem_1 = store.getMemoryUsage(); auto ref = store.store_tensor(make_tensor(10)); - auto tensor_mem_usage = store.get_tensor(ref)->get_memory_usage(); + auto tensor_mem_usage = store.get_tensor_ptr(ref)->get_memory_usage(); auto mem_2 = store.getMemoryUsage(); EXPECT_GT(tensor_mem_usage.usedBytes(), 500); EXPECT_LT(tensor_mem_usage.usedBytes(), 50000); @@ -93,21 +93,21 @@ TEST_F(DirectTensorStoreTest, heap_allocated_memory_is_tracked) TEST_F(DirectTensorStoreTest, invalid_ref_returns_nullptr) { - const auto* t = store.get_tensor(EntryRef()); + const auto* t = store.get_tensor_ptr(EntryRef()); EXPECT_FALSE(t); } TEST_F(DirectTensorStoreTest, hold_adds_entry_to_hold_list) { auto ref = store.store_tensor(make_tensor(5)); - auto tensor_mem_usage = store.get_tensor(ref)->get_memory_usage(); + auto tensor_mem_usage = store.get_tensor_ptr(ref)->get_memory_usage(); auto mem_1 = store.getMemoryUsage(); store.holdTensor(ref); auto mem_2 = store.getMemoryUsage(); EXPECT_GT(mem_2.allocatedBytesOnHold(), mem_1.allocatedBytesOnHold() + tensor_mem_usage.allocatedBytes()); } -TEST_F(DirectTensorStoreTest, move_allocates_new_entry_and_puts_old_entry_on_hold) +TEST_F(DirectTensorStoreTest, move_on_compact_allocates_new_entry_and_leaves_old_entry_alone) { auto t = make_tensor(5); auto* exp = t.get(); @@ -115,12 +115,13 @@ TEST_F(DirectTensorStoreTest, move_allocates_new_entry_and_puts_old_entry_on_hol auto ref_1 = store.store_tensor(std::move(t)); auto mem_1 = store.getMemoryUsage(); - auto ref_2 = store.move(ref_1); + auto ref_2 = store.move_on_compact(ref_1); auto mem_2 = store.getMemoryUsage(); EXPECT_NE(ref_1, ref_2); expect_tensor(exp, ref_1); expect_tensor(exp, ref_2); - EXPECT_GT(mem_2.allocatedBytesOnHold(), mem_1.allocatedBytesOnHold() + tensor_mem_usage.allocatedBytes()); + EXPECT_EQ(0, mem_2.allocatedBytesOnHold()); + EXPECT_GT(mem_2.usedBytes(), mem_1.usedBytes() + tensor_mem_usage.allocatedBytes()); } GTEST_MAIN_RUN_ALL_TESTS() diff --git a/searchlib/src/tests/tensor/hnsw_index/hnsw_index_test.cpp b/searchlib/src/tests/tensor/hnsw_index/hnsw_index_test.cpp index 7877b488065..9fccad1d2d4 100644 --- a/searchlib/src/tests/tensor/hnsw_index/hnsw_index_test.cpp +++ b/searchlib/src/tests/tensor/hnsw_index/hnsw_index_test.cpp @@ -99,10 +99,9 @@ public: commit(); } void commit() { - index->transfer_hold_lists(gen_handler.getCurrentGeneration()); + index->assign_generation(gen_handler.getCurrentGeneration()); gen_handler.incGeneration(); - gen_handler.updateFirstUsedGeneration(); - index->trim_hold_lists(gen_handler.getFirstUsedGeneration()); + index->reclaim_memory(gen_handler.get_oldest_used_generation()); } void set_filter(std::vector<uint32_t> docids) { uint32_t sz = 10; diff --git a/searchlib/src/tests/tensor/hnsw_index/stress_hnsw_mt.cpp b/searchlib/src/tests/tensor/hnsw_index/stress_hnsw_mt.cpp index d559fa592ad..81b56909d57 100644 --- a/searchlib/src/tests/tensor/hnsw_index/stress_hnsw_mt.cpp +++ b/searchlib/src/tests/tensor/hnsw_index/stress_hnsw_mt.cpp @@ -267,10 +267,9 @@ public: ASSERT_EQ(r.get(), nullptr); } void commit(uint32_t docid) { - index->transfer_hold_lists(gen_handler.getCurrentGeneration()); + index->assign_generation(gen_handler.getCurrentGeneration()); gen_handler.incGeneration(); - gen_handler.updateFirstUsedGeneration(); - index->trim_hold_lists(gen_handler.getFirstUsedGeneration()); + index->reclaim_memory(gen_handler.get_oldest_used_generation()); std::lock_guard<std::mutex> guard(in_progress_lock); in_progress->clearBit(docid); // printf("commit: %u\n", docid); diff --git a/searchlib/src/tests/tensor/tensor_buffer_operations/tensor_buffer_operations_test.cpp b/searchlib/src/tests/tensor/tensor_buffer_operations/tensor_buffer_operations_test.cpp index 55598a1b11f..bda229f8074 100644 --- a/searchlib/src/tests/tensor/tensor_buffer_operations/tensor_buffer_operations_test.cpp +++ b/searchlib/src/tests/tensor/tensor_buffer_operations/tensor_buffer_operations_test.cpp @@ -137,10 +137,9 @@ TensorBufferOperationsTest::assert_store_copy_load(const TensorSpec& tensor_spec { auto buf = store_tensor(tensor_spec); auto buf2 = buf; - _ops.copied_labels(buf2); - EXPECT_EQ(buf, buf2); - _ops.reclaim_labels(buf); + _ops.copied_labels(buf); EXPECT_NE(buf, buf2); + _ops.reclaim_labels(buf); buf.clear(); auto loaded_spec = load_tensor_spec(buf2); _ops.reclaim_labels(buf2); diff --git a/searchlib/src/tests/tensor/tensor_buffer_store/tensor_buffer_store_test.cpp b/searchlib/src/tests/tensor/tensor_buffer_store/tensor_buffer_store_test.cpp index 101b84e01aa..3bbb6cd334e 100644 --- a/searchlib/src/tests/tensor/tensor_buffer_store/tensor_buffer_store_test.cpp +++ b/searchlib/src/tests/tensor/tensor_buffer_store/tensor_buffer_store_test.cpp @@ -29,7 +29,7 @@ protected: vespalib::nbostream encode_stored_tensor(EntryRef ref); void assert_store_load(const TensorSpec& tensor_spec); void assert_store_load_many(const TensorSpec& tensor_spec); - void assert_store_move_load(const TensorSpec& tensor_spec); + void assert_store_move_on_compact_load(const TensorSpec& tensor_spec); void assert_store_encode_store_encoded_load(const TensorSpec& tensor_spec); }; @@ -102,10 +102,10 @@ TensorBufferStoreTest::assert_store_load_many(const TensorSpec& tensor_spec) } void -TensorBufferStoreTest::assert_store_move_load(const TensorSpec& tensor_spec) +TensorBufferStoreTest::assert_store_move_on_compact_load(const TensorSpec& tensor_spec) { auto ref = store_tensor(tensor_spec); - auto ref2 = _store.move(ref); + auto ref2 = _store.move_on_compact(ref); EXPECT_NE(ref, ref2); auto loaded_spec = load_tensor_spec(ref2); _store.holdTensor(ref2); @@ -147,10 +147,10 @@ TEST_F(TensorBufferStoreTest, tensor_can_be_stored_and_loaded_many_times) } } -TEST_F(TensorBufferStoreTest, stored_tensor_can_be_copied) +TEST_F(TensorBufferStoreTest, stored_tensor_can_be_moved_on_compact) { for (auto& tensor_spec : tensor_specs) { - assert_store_move_load(tensor_spec); + assert_store_move_on_compact_load(tensor_spec); } } diff --git a/searchlib/src/tests/tensor/tensor_buffer_type_mapper/CMakeLists.txt b/searchlib/src/tests/tensor/tensor_buffer_type_mapper/CMakeLists.txt new file mode 100644 index 00000000000..e219b17ebd1 --- /dev/null +++ b/searchlib/src/tests/tensor/tensor_buffer_type_mapper/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(searchlib_tensor_buffer_type_mapper_test_app TEST + SOURCES + tensor_buffer_type_mapper_test.cpp + DEPENDS + searchlib + GTest::GTest +) +vespa_add_test(NAME searchlib_tensor_buffer_type_mapper_test_app COMMAND searchlib_tensor_buffer_type_mapper_test_app) diff --git a/searchlib/src/tests/tensor/tensor_buffer_type_mapper/tensor_buffer_type_mapper_test.cpp b/searchlib/src/tests/tensor/tensor_buffer_type_mapper/tensor_buffer_type_mapper_test.cpp new file mode 100644 index 00000000000..8e88c103516 --- /dev/null +++ b/searchlib/src/tests/tensor/tensor_buffer_type_mapper/tensor_buffer_type_mapper_test.cpp @@ -0,0 +1,121 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/searchlib/tensor/tensor_buffer_type_mapper.h> +#include <vespa/searchlib/tensor/tensor_buffer_operations.h> +#include <vespa/eval/eval/value_type.h> +#include <vespa/vespalib/gtest/gtest.h> + +using search::tensor::TensorBufferOperations; +using search::tensor::TensorBufferTypeMapper; +using vespalib::eval::ValueType; + +const vespalib::string tensor_type_sparse_spec("tensor(x{})"); +const vespalib::string tensor_type_2d_spec("tensor(x{},y{})"); +const vespalib::string tensor_type_2d_mixed_spec("tensor(x{},y[2])"); +const vespalib::string float_tensor_type_spec("tensor<float>(y{})"); +const vespalib::string tensor_type_dense_spec("tensor(x[2])"); + +struct TestParam +{ + vespalib::string _name; + std::vector<size_t> _array_sizes; + vespalib::string _tensor_type_spec; + TestParam(vespalib::string name, std::vector<size_t> array_sizes, const vespalib::string& tensor_type_spec) + : _name(std::move(name)), + _array_sizes(std::move(array_sizes)), + _tensor_type_spec(tensor_type_spec) + { + } + TestParam(const TestParam&); + ~TestParam(); +}; + +TestParam::TestParam(const TestParam&) = default; + +TestParam::~TestParam() = default; + +std::ostream& operator<<(std::ostream& os, const TestParam& param) +{ + os << param._name; + return os; +} + +class TensorBufferTypeMapperTest : public testing::TestWithParam<TestParam> +{ +protected: + ValueType _tensor_type; + TensorBufferOperations _ops; + TensorBufferTypeMapper _mapper; + TensorBufferTypeMapperTest(); + ~TensorBufferTypeMapperTest() override; + std::vector<size_t> get_array_sizes(); + void select_type_ids(); +}; + +TensorBufferTypeMapperTest::TensorBufferTypeMapperTest() + : testing::TestWithParam<TestParam>(), + _tensor_type(ValueType::from_spec(GetParam()._tensor_type_spec)), + _ops(_tensor_type), + _mapper(GetParam()._array_sizes.size(), &_ops) +{ +} + +TensorBufferTypeMapperTest::~TensorBufferTypeMapperTest() = default; + +std::vector<size_t> +TensorBufferTypeMapperTest::get_array_sizes() +{ + uint32_t max_small_subspaces_type_id = GetParam()._array_sizes.size(); + std::vector<size_t> array_sizes; + for (uint32_t type_id = 1; type_id <= max_small_subspaces_type_id; ++type_id) { + auto num_subspaces = type_id - 1; + array_sizes.emplace_back(_mapper.get_array_size(type_id)); + EXPECT_EQ(_ops.get_array_size(num_subspaces), array_sizes.back()); + } + return array_sizes; +} + +void +TensorBufferTypeMapperTest::select_type_ids() +{ + auto& array_sizes = GetParam()._array_sizes; + uint32_t type_id = 0; + for (auto array_size : array_sizes) { + ++type_id; + EXPECT_EQ(type_id, _mapper.get_type_id(array_size)); + EXPECT_EQ(type_id, _mapper.get_type_id(array_size - 1)); + if (array_size == array_sizes.back()) { + // Fallback to indirect storage, using type id 0 + EXPECT_EQ(0u, _mapper.get_type_id(array_size + 1)); + } else { + EXPECT_EQ(type_id + 1, _mapper.get_type_id(array_size + 1)); + } + } +} + +/* + * For "dense" case, array size for type id 1 is irrelevant, since + * type ids 0 and 1 are not used when storing dense tensors in + * TensorBufferStore. + */ + +VESPA_GTEST_INSTANTIATE_TEST_SUITE_P(TensorBufferTypeMapperMultiTest, + TensorBufferTypeMapperTest, + testing::Values(TestParam("1d", {8, 16, 32, 40, 64}, tensor_type_sparse_spec), + TestParam("1dfloat", {4, 12, 20, 28, 36}, float_tensor_type_spec), + TestParam("2d", {8, 24, 40, 56, 80}, tensor_type_2d_spec), + TestParam("2dmixed", {8, 24, 48, 64, 96}, tensor_type_2d_mixed_spec), + TestParam("dense", {8, 24}, tensor_type_dense_spec)), + testing::PrintToStringParamName()); + +TEST_P(TensorBufferTypeMapperTest, array_sizes_are_calculated) +{ + EXPECT_EQ(GetParam()._array_sizes, get_array_sizes()); +} + +TEST_P(TensorBufferTypeMapperTest, type_ids_are_selected) +{ + select_type_ids(); +} + +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/searchlib/src/tests/test/string_field_builder/CMakeLists.txt b/searchlib/src/tests/test/string_field_builder/CMakeLists.txt new file mode 100644 index 00000000000..6cd9c5e36f1 --- /dev/null +++ b/searchlib/src/tests/test/string_field_builder/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(searchlib_string_field_builder_test_app TEST + SOURCES + string_field_builder_test.cpp + DEPENDS + searchlib_test + GTest::GTest +) +vespa_add_test(NAME searchlib_string_field_builder_test_app COMMAND searchlib_string_field_builder_test_app) diff --git a/searchlib/src/tests/test/string_field_builder/string_field_builder_test.cpp b/searchlib/src/tests/test/string_field_builder/string_field_builder_test.cpp new file mode 100644 index 00000000000..9d886e6cde7 --- /dev/null +++ b/searchlib/src/tests/test/string_field_builder/string_field_builder_test.cpp @@ -0,0 +1,141 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/searchlib/test/string_field_builder.h> +#include <vespa/document/annotation/annotation.h> +#include <vespa/document/annotation/span.h> +#include <vespa/document/annotation/spanlist.h> +#include <vespa/document/annotation/spantree.h> +#include <vespa/document/datatype/annotationtype.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/searchlib/test/doc_builder.h> +#include <vespa/vespalib/gtest/gtest.h> +#include <cassert> +#include <iostream> + +using document::Annotation; +using document::AnnotationType; +using document::Span; +using document::SpanNode; +using document::SpanTree; +using document::StringFieldValue; +using search::test::DocBuilder; +using search::test::StringFieldBuilder; + +namespace +{ + +const vespalib::string SPANTREE_NAME("linguistics"); + +struct MyAnnotation { + int32_t start; + int32_t length; + std::optional<vespalib::string> label; + + MyAnnotation(int32_t start_in, int32_t length_in) noexcept + : start(start_in), + length(length_in), + label() + { + } + + MyAnnotation(int32_t start_in, int32_t length_in, vespalib::string label_in) noexcept + : start(start_in), + length(length_in), + label(label_in) + { + } + + bool operator==(const MyAnnotation& rhs) const noexcept; +}; + +bool +MyAnnotation::operator==(const MyAnnotation& rhs) const noexcept +{ + return start == rhs.start && + length == rhs.length && + label == rhs.label; +} + + +std::ostream& operator<<(std::ostream& os, const MyAnnotation& ann) { + os << "[" << ann.start << "," << ann.length << "]"; + if (ann.label.has_value()) { + os << "(\"" << ann.label.value() << "\")"; + } + return os; +} + +} + +class StringFieldBuilderTest : public testing::Test +{ +protected: + DocBuilder db; + StringFieldBuilder sfb; + StringFieldBuilderTest(); + ~StringFieldBuilderTest(); + std::vector<MyAnnotation> get_annotations(const StringFieldValue& val); + void assert_annotations(std::vector<MyAnnotation> exp, const vespalib::string& plain, const StringFieldValue& val); +}; + +StringFieldBuilderTest::StringFieldBuilderTest() + : testing::Test(), + db(), + sfb(db) +{ +} + +StringFieldBuilderTest::~StringFieldBuilderTest() = default; + +std::vector<MyAnnotation> +StringFieldBuilderTest::get_annotations(const StringFieldValue& val) +{ + std::vector<MyAnnotation> result; + StringFieldValue::SpanTrees trees = val.getSpanTrees(); + const auto* tree = StringFieldValue::findTree(trees, SPANTREE_NAME); + if (tree != nullptr) { + for (auto& ann : *tree) { + assert(ann.getType() == *AnnotationType::TERM); + auto span = dynamic_cast<const Span *>(ann.getSpanNode()); + if (span == nullptr) { + continue; + } + auto ann_fv = ann.getFieldValue(); + if (ann_fv == nullptr) { + result.emplace_back(span->from(), span->length()); + } else { + result.emplace_back(span->from(), span->length(), dynamic_cast<const StringFieldValue &>(*ann_fv).getValue()); + } + } + } + return result; +} + +void +StringFieldBuilderTest::assert_annotations(std::vector<MyAnnotation> exp, const vespalib::string& plain, const StringFieldValue& val) +{ + EXPECT_EQ(exp, get_annotations(val)); + EXPECT_EQ(plain, val.getValue()); +} + +TEST_F(StringFieldBuilderTest, no_annotations) +{ + assert_annotations({}, "foo", StringFieldValue("foo")); +} + +TEST_F(StringFieldBuilderTest, single_word) +{ + assert_annotations({{0, 4}}, "word", sfb.word("word").build()); +} + +TEST_F(StringFieldBuilderTest, tokenize) +{ + assert_annotations({{0, 4}, {5, 2}, {8, 1}, {10, 4}}, "this is a test", sfb.tokenize("this is a test").build()); +} + +TEST_F(StringFieldBuilderTest, alt_word) +{ + assert_annotations({{0, 3}, {4, 3}, {4, 3, "baz"}}, "foo bar", sfb.word("foo").space().word("bar").alt_word("baz").build()); +} + +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/searchlib/src/vespa/searchcommon/common/growstrategy.h b/searchlib/src/vespa/searchcommon/common/growstrategy.h index bc04047aa3c..8766989ded0 100644 --- a/searchlib/src/vespa/searchcommon/common/growstrategy.h +++ b/searchlib/src/vespa/searchcommon/common/growstrategy.h @@ -24,7 +24,7 @@ public: } static GrowStrategy make(uint32_t docsInitialCapacity, float docsGrowFactor, uint32_t docsGrowDelta) { - return GrowStrategy(docsInitialCapacity, docsGrowFactor, docsGrowDelta, 0, 0.2); + return {docsInitialCapacity, docsGrowFactor, docsGrowDelta, 0, 0.2}; } float getMultiValueAllocGrowFactor() const { return _multiValueAllocGrowFactor; } diff --git a/searchlib/src/vespa/searchcommon/common/iblobconverter.h b/searchlib/src/vespa/searchcommon/common/iblobconverter.h index 6581c3e5ccb..4cb79a2547c 100644 --- a/searchlib/src/vespa/searchcommon/common/iblobconverter.h +++ b/searchlib/src/vespa/searchcommon/common/iblobconverter.h @@ -13,7 +13,7 @@ public: using SP = std::shared_ptr<BlobConverter>; using UP = std::unique_ptr<BlobConverter>; using ConstBufferRef = vespalib::ConstBufferRef; - virtual ~BlobConverter() { } + virtual ~BlobConverter() = default; ConstBufferRef convert(const ConstBufferRef & src) const { return onConvert(src); } private: virtual ConstBufferRef onConvert(const ConstBufferRef & src) const = 0; diff --git a/searchlib/src/vespa/searchlib/attribute/CMakeLists.txt b/searchlib/src/vespa/searchlib/attribute/CMakeLists.txt index 5e86c350b55..704db67aa03 100644 --- a/searchlib/src/vespa/searchlib/attribute/CMakeLists.txt +++ b/searchlib/src/vespa/searchlib/attribute/CMakeLists.txt @@ -73,6 +73,7 @@ vespa_add_library(searchlib_attribute OBJECT loadedenumvalue.cpp loadednumericvalue.cpp loadedvalue.cpp + multi_enum_search_context.cpp multi_numeric_enum_search_context.cpp multi_numeric_flag_search_context.cpp multi_numeric_search_context.cpp @@ -119,6 +120,7 @@ vespa_add_library(searchlib_attribute OBJECT singlesmallnumericattribute.cpp singlestringattribute.cpp singlestringpostattribute.cpp + single_enum_search_context.cpp single_numeric_enum_search_context.cpp single_numeric_search_context.cpp single_small_numeric_search_context.cpp diff --git a/searchlib/src/vespa/searchlib/attribute/attributevector.cpp b/searchlib/src/vespa/searchlib/attribute/attributevector.cpp index dd7af1e8c4b..7f8f3f92f9e 100644 --- a/searchlib/src/vespa/searchlib/attribute/attributevector.cpp +++ b/searchlib/src/vespa/searchlib/attribute/attributevector.cpp @@ -64,7 +64,7 @@ allow_paged(const search::attribute::Config& config) return false; } if (config.basicType() == Type::TENSOR) { - return (!config.tensorType().is_error() && config.tensorType().is_dense()); + return (!config.tensorType().is_error() && (config.tensorType().is_dense() || !config.fastSearch())); } return true; } @@ -184,10 +184,10 @@ void AttributeVector::incGeneration() { // Freeze trees etc, to stop new readers from accessing currently held data - onGenerationChange(_genHandler.getNextGeneration()); + before_inc_generation(_genHandler.getCurrentGeneration()); _genHandler.incGeneration(); // Remove old data on hold lists that can no longer be reached by readers - removeAllOldGenerations(); + reclaim_unused_memory(); } void @@ -237,8 +237,8 @@ AttributeVector::headerTypeOK(const vespalib::GenericHeader &header) const getConfig().collectionType().asString(); } -void AttributeVector::removeOldGenerations(generation_t firstUsed) { (void) firstUsed; } -void AttributeVector::onGenerationChange(generation_t generation) { (void) generation; } +void AttributeVector::reclaim_memory(generation_t oldest_used_gen) { (void) oldest_used_gen; } +void AttributeVector::before_inc_generation(generation_t current_gen) { (void) current_gen; } const IEnumStore* AttributeVector::getEnumStoreBase() const { return nullptr; } IEnumStore* AttributeVector::getEnumStoreBase() { return nullptr; } const attribute::MultiValueMappingBase * AttributeVector::getMultiValueBase() const { return nullptr; } @@ -408,9 +408,9 @@ bool AttributeVector::applyWeight(DocId, const FieldValue &, const ArithmeticVal bool AttributeVector::applyWeight(DocId, const FieldValue&, const AssignValueUpdate&) { return false; } void -AttributeVector::removeAllOldGenerations() { - _genHandler.updateFirstUsedGeneration(); - removeOldGenerations(_genHandler.getFirstUsedGeneration()); +AttributeVector::reclaim_unused_memory() { + _genHandler.update_oldest_used_generation(); + reclaim_memory(_genHandler.get_oldest_used_generation()); } @@ -483,19 +483,17 @@ AttributeVector::compactLidSpace(uint32_t wantedLidLimit) { incGeneration(); } - bool AttributeVector::canShrinkLidSpace() const { return wantShrinkLidSpace() && - _compactLidSpaceGeneration.load(std::memory_order_relaxed) < getFirstUsedGeneration(); + _compactLidSpaceGeneration.load(std::memory_order_relaxed) < get_oldest_used_generation(); } - void AttributeVector::shrinkLidSpace() { commit(); - removeAllOldGenerations(); + reclaim_unused_memory(); if (!canShrinkLidSpace()) { return; } @@ -717,7 +715,7 @@ AttributeVector::drain_hold(uint64_t hold_limit) { incGeneration(); for (int retry = 0; retry < 40; ++retry) { - removeAllOldGenerations(); + reclaim_unused_memory(); updateStat(true); if (_status.getOnHold() <= hold_limit) { return; diff --git a/searchlib/src/vespa/searchlib/attribute/attributevector.h b/searchlib/src/vespa/searchlib/attribute/attributevector.h index f245a216aeb..261290247ad 100644 --- a/searchlib/src/vespa/searchlib/attribute/attributevector.h +++ b/searchlib/src/vespa/searchlib/attribute/attributevector.h @@ -156,10 +156,10 @@ protected: public: void incGeneration(); - void removeAllOldGenerations(); + void reclaim_unused_memory(); - generation_t getFirstUsedGeneration() const { - return _genHandler.getFirstUsedGeneration(); + generation_t get_oldest_used_generation() const { + return _genHandler.get_oldest_used_generation(); } generation_t getCurrentGeneration() const { @@ -446,8 +446,8 @@ private: GenerationHandler::Guard takeGenerationGuard() { return _genHandler.takeGuard(); } /// Clean up [0, firstUsed> - virtual void removeOldGenerations(generation_t firstUsed); - virtual void onGenerationChange(generation_t generation); + virtual void reclaim_memory(generation_t oldest_used_gen); + virtual void before_inc_generation(generation_t current_gen); virtual void onUpdateStat() = 0; /** * Used to regulate access to critical resources. Apply the @@ -466,8 +466,8 @@ public: /** * Should be called by the writer thread. */ - void updateFirstUsedGeneration() { - _genHandler.updateFirstUsedGeneration(); + void update_oldest_used_generation() { + _genHandler.update_oldest_used_generation(); } /** diff --git a/searchlib/src/vespa/searchlib/attribute/attrvector.cpp b/searchlib/src/vespa/searchlib/attribute/attrvector.cpp index d5ef41243e7..4a0bdafae8f 100644 --- a/searchlib/src/vespa/searchlib/attribute/attrvector.cpp +++ b/searchlib/src/vespa/searchlib/attribute/attrvector.cpp @@ -203,4 +203,7 @@ bool StringDirectAttribute::addDoc(DocId & doc) return false; } +template class NumericDirectAttribute<IntegerAttributeTemplate<int64_t>>; +template class NumericDirectAttribute<FloatingPointAttributeTemplate<double>>; + } // namespace search diff --git a/searchlib/src/vespa/searchlib/attribute/enumstore.h b/searchlib/src/vespa/searchlib/attribute/enumstore.h index 52f42ed368e..0a0b2040b2a 100644 --- a/searchlib/src/vespa/searchlib/attribute/enumstore.h +++ b/searchlib/src/vespa/searchlib/attribute/enumstore.h @@ -96,8 +96,8 @@ public: vespalib::AddressSpace get_values_address_space_usage() const override; - void transfer_hold_lists(generation_t generation); - void trim_hold_lists(generation_t first_used); + void assign_generation(generation_t current_gen); + void reclaim_memory(generation_t first_used); ssize_t load_unique_values(const void* src, size_t available, IndexVector& idx) override; diff --git a/searchlib/src/vespa/searchlib/attribute/enumstore.hpp b/searchlib/src/vespa/searchlib/attribute/enumstore.hpp index 1ef194f6812..b863e56fb4a 100644 --- a/searchlib/src/vespa/searchlib/attribute/enumstore.hpp +++ b/searchlib/src/vespa/searchlib/attribute/enumstore.hpp @@ -104,17 +104,17 @@ EnumStoreT<EntryT>::get_values_address_space_usage() const template <typename EntryT> void -EnumStoreT<EntryT>::transfer_hold_lists(generation_t generation) +EnumStoreT<EntryT>::assign_generation(generation_t current_gen) { - _store.transferHoldLists(generation); + _store.assign_generation(current_gen); } template <typename EntryT> void -EnumStoreT<EntryT>::trim_hold_lists(generation_t firstUsed) +EnumStoreT<EntryT>::reclaim_memory(generation_t oldest_used_gen) { // remove generations in the range [0, firstUsed> - _store.trimHoldLists(firstUsed); + _store.reclaim_memory(oldest_used_gen); } template <typename EntryT> diff --git a/searchlib/src/vespa/searchlib/attribute/flagattribute.cpp b/searchlib/src/vespa/searchlib/attribute/flagattribute.cpp index ef796d3f3d2..f8cf742bdb2 100644 --- a/searchlib/src/vespa/searchlib/attribute/flagattribute.cpp +++ b/searchlib/src/vespa/searchlib/attribute/flagattribute.cpp @@ -223,16 +223,16 @@ FlagAttributeT<B>::resizeBitVectors(uint32_t neededSize) } } _bitVectorSize = newSize; - _bitVectorHolder.transferHoldLists(this->getCurrentGeneration()); + _bitVectorHolder.assign_generation(this->getCurrentGeneration()); } template <typename B> void -FlagAttributeT<B>::removeOldGenerations(vespalib::GenerationHandler::generation_t firstUsed) +FlagAttributeT<B>::reclaim_memory(vespalib::GenerationHandler::generation_t oldest_used_gen) { - B::removeOldGenerations(firstUsed); - _bitVectorHolder.trimHoldLists(firstUsed); + B::reclaim_memory(oldest_used_gen); + _bitVectorHolder.reclaim(oldest_used_gen); } template class FlagAttributeT<FlagBaseImpl>; diff --git a/searchlib/src/vespa/searchlib/attribute/flagattribute.h b/searchlib/src/vespa/searchlib/attribute/flagattribute.h index 796c1493cc9..df75e7afa04 100644 --- a/searchlib/src/vespa/searchlib/attribute/flagattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/flagattribute.h @@ -33,7 +33,7 @@ private: void ensureGuardBit(); void clearGuardBit(DocId doc); void resizeBitVectors(uint32_t neededSize); - void removeOldGenerations(vespalib::GenerationHandler::generation_t firstUsed) override; + void reclaim_memory(vespalib::GenerationHandler::generation_t oldest_used_gen) override; uint32_t getOffset(int8_t value) const { return value + 128; } using AtomicBitVectorPtr = vespalib::datastore::AtomicValueWrapper<BitVector *>; diff --git a/searchlib/src/vespa/searchlib/attribute/load_utils.hpp b/searchlib/src/vespa/searchlib/attribute/load_utils.hpp index 62d645326ce..463a62ab01a 100644 --- a/searchlib/src/vespa/searchlib/attribute/load_utils.hpp +++ b/searchlib/src/vespa/searchlib/attribute/load_utils.hpp @@ -68,7 +68,7 @@ loadFromEnumeratedSingleValue(Vector &vector, using ValueType = typename Vector::ValueType; using NonAtomicValueType = atomic_utils::NonAtomicValue_t<ValueType>; uint32_t numDocs = attrReader.getEnumCount(); - genHolder.clearHoldLists(); + genHolder.reclaim_all(); vector.reset(); vector.unsafe_reserve(numDocs); for (uint32_t doc = 0; doc < numDocs; ++doc) { diff --git a/searchlib/src/vespa/searchlib/attribute/multi_enum_search_context.cpp b/searchlib/src/vespa/searchlib/attribute/multi_enum_search_context.cpp new file mode 100644 index 00000000000..566d8e37d89 --- /dev/null +++ b/searchlib/src/vespa/searchlib/attribute/multi_enum_search_context.cpp @@ -0,0 +1,15 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "multi_enum_search_context.hpp" +#include "string_search_context.h" + +using ValueRef = vespalib::datastore::AtomicEntryRef; +using WeightedValueRef = search::multivalue::WeightedValue<vespalib::datastore::AtomicEntryRef>; + +namespace search::attribute { + +template class MultiEnumSearchContext<const char *, StringSearchContext, ValueRef>; + +template class MultiEnumSearchContext<const char *, StringSearchContext, WeightedValueRef>; + +} diff --git a/searchlib/src/vespa/searchlib/attribute/multi_value_mapping.h b/searchlib/src/vespa/searchlib/attribute/multi_value_mapping.h index 61798959f3e..98587baadd2 100644 --- a/searchlib/src/vespa/searchlib/attribute/multi_value_mapping.h +++ b/searchlib/src/vespa/searchlib/attribute/multi_value_mapping.h @@ -48,8 +48,8 @@ public: */ ReadView make_read_view(size_t read_size) const { return ReadView(_indices.make_read_view(read_size), &_store); } // Pass on hold list management to underlying store - void transferHoldLists(generation_t generation) { _store.transferHoldLists(generation); } - void trimHoldLists(generation_t firstUsed) { _store.trimHoldLists(firstUsed); } + void assign_generation(generation_t current_gen) { _store.assign_generation(current_gen); } + void reclaim_memory(generation_t oldest_used_gen) { _store.reclaim_memory(oldest_used_gen); } void prepareLoadFromMultiValue() { _store.setInitializing(true); } void doneLoadFromMultiValue() { _store.setInitializing(false); } diff --git a/searchlib/src/vespa/searchlib/attribute/multienumattribute.h b/searchlib/src/vespa/searchlib/attribute/multienumattribute.h index ee8f3181fd9..a073060afc5 100644 --- a/searchlib/src/vespa/searchlib/attribute/multienumattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/multienumattribute.h @@ -63,8 +63,8 @@ public: void onCommit() override; void onUpdateStat() override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; //----------------------------------------------------------------------------------------------------------------- // Attribute read API diff --git a/searchlib/src/vespa/searchlib/attribute/multienumattribute.hpp b/searchlib/src/vespa/searchlib/attribute/multienumattribute.hpp index ec948882312..e4e451ffcda 100644 --- a/searchlib/src/vespa/searchlib/attribute/multienumattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/multienumattribute.hpp @@ -147,7 +147,7 @@ MultiValueEnumAttribute<B, M>::onCommit() updater.commit(); this->freezeEnumDictionary(); std::atomic_thread_fence(std::memory_order_release); - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); if (this->_mvMapping.considerCompact(this->getConfig().getCompactionStrategy())) { this->incGeneration(); this->updateStat(true); @@ -194,15 +194,15 @@ MultiValueEnumAttribute<B, M>::onUpdateStat() template <typename B, typename M> void -MultiValueEnumAttribute<B, M>::removeOldGenerations(generation_t firstUsed) +MultiValueEnumAttribute<B, M>::reclaim_memory(generation_t oldest_used_gen) { - this->_enumStore.trim_hold_lists(firstUsed); - this->_mvMapping.trimHoldLists(firstUsed); + this->_enumStore.reclaim_memory(oldest_used_gen); + this->_mvMapping.reclaim_memory(oldest_used_gen); } template <typename B, typename M> void -MultiValueEnumAttribute<B, M>::onGenerationChange(generation_t generation) +MultiValueEnumAttribute<B, M>::before_inc_generation(generation_t current_gen) { /* * Freeze tree before generation is increased in attribute vector @@ -211,8 +211,8 @@ MultiValueEnumAttribute<B, M>::onGenerationChange(generation_t generation) * sufficiently new frozen tree. */ freezeEnumDictionary(); - this->_mvMapping.transferHoldLists(generation - 1); - this->_enumStore.transfer_hold_lists(generation - 1); + this->_mvMapping.assign_generation(current_gen); + this->_enumStore.assign_generation(current_gen); } template <typename B, typename M> diff --git a/searchlib/src/vespa/searchlib/attribute/multinumericattribute.h b/searchlib/src/vespa/searchlib/attribute/multinumericattribute.h index 0a29b4af48d..ed78f7776f1 100644 --- a/searchlib/src/vespa/searchlib/attribute/multinumericattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/multinumericattribute.h @@ -60,9 +60,9 @@ public: uint32_t getValueCount(DocId doc) const override; void onCommit() override; void onUpdateStat() override; - void removeOldGenerations(generation_t firstUsed) override; + void reclaim_memory(generation_t oldest_used_gen) override; - void onGenerationChange(generation_t generation) override; + void before_inc_generation(generation_t current_gen) override; bool onLoad(vespalib::Executor *executor) override; virtual bool onLoadEnumerated(ReaderBase &attrReader); diff --git a/searchlib/src/vespa/searchlib/attribute/multinumericattribute.hpp b/searchlib/src/vespa/searchlib/attribute/multinumericattribute.hpp index 8cabd8483bf..6dde909821e 100644 --- a/searchlib/src/vespa/searchlib/attribute/multinumericattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/multinumericattribute.hpp @@ -63,7 +63,7 @@ MultiValueNumericAttribute<B, M>::onCommit() } std::atomic_thread_fence(std::memory_order_release); - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); this->_changes.clear(); if (this->_mvMapping.considerCompact(this->getConfig().getCompactionStrategy())) { @@ -96,16 +96,16 @@ void MultiValueNumericAttribute<B, M>::setNewValues(DocId doc, const std::vector } template <typename B, typename M> -void MultiValueNumericAttribute<B, M>::removeOldGenerations(generation_t firstUsed) +void MultiValueNumericAttribute<B, M>::reclaim_memory(generation_t oldest_used_gen) { - this->_mvMapping.trimHoldLists(firstUsed); + this->_mvMapping.reclaim_memory(oldest_used_gen); } template <typename B, typename M> -void MultiValueNumericAttribute<B, M>::onGenerationChange(generation_t generation) +void MultiValueNumericAttribute<B, M>::before_inc_generation(generation_t current_gen) { - this->_mvMapping.transferHoldLists(generation - 1); + this->_mvMapping.assign_generation(current_gen); } template <typename B, typename M> diff --git a/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.h b/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.h index 4bd8ad6e99f..a22a6241ab2 100644 --- a/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.h @@ -80,8 +80,8 @@ public: MultiValueNumericPostingAttribute(const vespalib::string & name, const AttributeVector::Config & cfg); ~MultiValueNumericPostingAttribute(); - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; std::unique_ptr<attribute::SearchContext> getSearch(QueryTermSimpleUP term, const attribute::SearchContextParams & params) const override; diff --git a/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.hpp b/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.hpp index 9a8c9738bc0..deee72dcf39 100644 --- a/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/multinumericpostattribute.hpp @@ -56,19 +56,19 @@ MultiValueNumericPostingAttribute<B, M>::~MultiValueNumericPostingAttribute() template <typename B, typename M> void -MultiValueNumericPostingAttribute<B, M>::removeOldGenerations(generation_t firstUsed) +MultiValueNumericPostingAttribute<B, M>::reclaim_memory(generation_t oldest_used_gen) { - MultiValueNumericEnumAttribute<B, M>::removeOldGenerations(firstUsed); - _postingList.trimHoldLists(firstUsed); + MultiValueNumericEnumAttribute<B, M>::reclaim_memory(oldest_used_gen); + _postingList.reclaim_memory(oldest_used_gen); } template <typename B, typename M> void -MultiValueNumericPostingAttribute<B, M>::onGenerationChange(generation_t generation) +MultiValueNumericPostingAttribute<B, M>::before_inc_generation(generation_t current_gen) { _postingList.freeze(); - MultiValueNumericEnumAttribute<B, M>::onGenerationChange(generation); - _postingList.transferHoldLists(generation - 1); + MultiValueNumericEnumAttribute<B, M>::before_inc_generation(current_gen); + _postingList.assign_generation(current_gen); } template <typename B, typename M> diff --git a/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.h b/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.h index 4deb71e9759..2e355a9aed2 100644 --- a/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.h @@ -77,8 +77,8 @@ public: MultiValueStringPostingAttributeT(const vespalib::string & name); ~MultiValueStringPostingAttributeT(); - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; std::unique_ptr<attribute::SearchContext> getSearch(QueryTermSimpleUP term, const attribute::SearchContextParams & params) const override; diff --git a/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.hpp b/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.hpp index fef3db582c8..cfd00f84636 100644 --- a/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/multistringpostattribute.hpp @@ -75,19 +75,19 @@ MultiValueStringPostingAttributeT<B, T>::mergeMemoryStats(vespalib::MemoryUsage template <typename B, typename T> void -MultiValueStringPostingAttributeT<B, T>::removeOldGenerations(generation_t firstUsed) +MultiValueStringPostingAttributeT<B, T>::reclaim_memory(generation_t oldest_used_gen) { - MultiValueStringAttributeT<B, T>::removeOldGenerations(firstUsed); - _postingList.trimHoldLists(firstUsed); + MultiValueStringAttributeT<B, T>::reclaim_memory(oldest_used_gen); + _postingList.reclaim_memory(oldest_used_gen); } template <typename B, typename T> void -MultiValueStringPostingAttributeT<B, T>::onGenerationChange(generation_t generation) +MultiValueStringPostingAttributeT<B, T>::before_inc_generation(generation_t current_gen) { _postingList.freeze(); - MultiValueStringAttributeT<B, T>::onGenerationChange(generation); - _postingList.transferHoldLists(generation - 1); + MultiValueStringAttributeT<B, T>::before_inc_generation(current_gen); + _postingList.assign_generation(current_gen); } diff --git a/searchlib/src/vespa/searchlib/attribute/multivalueattribute.hpp b/searchlib/src/vespa/searchlib/attribute/multivalueattribute.hpp index 2066b6fa845..2456c57140c 100644 --- a/searchlib/src/vespa/searchlib/attribute/multivalueattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/multivalueattribute.hpp @@ -245,7 +245,7 @@ MultiValueAttribute<B, M>::addDoc(DocId & doc) if (incGen) { this->incGeneration(); } else - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); return true; } diff --git a/searchlib/src/vespa/searchlib/attribute/predicate_attribute.cpp b/searchlib/src/vespa/searchlib/attribute/predicate_attribute.cpp index c1897c71366..f34099de758 100644 --- a/searchlib/src/vespa/searchlib/attribute/predicate_attribute.cpp +++ b/searchlib/src/vespa/searchlib/attribute/predicate_attribute.cpp @@ -89,7 +89,7 @@ PredicateAttribute::PredicateAttribute(const vespalib::string &base_file_name, c PredicateAttribute::~PredicateAttribute() { - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); } void PredicateAttribute::populateIfNeeded() { @@ -118,24 +118,24 @@ PredicateAttribute::onUpdateStat() combined.merge(_min_feature.getMemoryUsage()); combined.merge(_interval_range_vector.getMemoryUsage()); combined.merge(_index->getMemoryUsage()); - combined.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + combined.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); this->updateStatistics(_min_feature.size(), _min_feature.size(), combined.allocatedBytes(), combined.usedBytes(), combined.deadBytes(), combined.allocatedBytesOnHold()); } void -PredicateAttribute::removeOldGenerations(generation_t firstUsed) +PredicateAttribute::reclaim_memory(generation_t oldest_used_gen) { - getGenerationHolder().trimHoldLists(firstUsed); - _index->trimHoldLists(firstUsed); + getGenerationHolder().reclaim(oldest_used_gen); + _index->reclaim_memory(oldest_used_gen); } void -PredicateAttribute::onGenerationChange(generation_t generation) +PredicateAttribute::before_inc_generation(generation_t current_gen) { - getGenerationHolder().transferHoldLists(generation - 1); - _index->transferHoldLists(generation - 1); + getGenerationHolder().assign_generation(current_gen); + _index->assign_generation(current_gen); } void diff --git a/searchlib/src/vespa/searchlib/attribute/predicate_attribute.h b/searchlib/src/vespa/searchlib/attribute/predicate_attribute.h index f5d789298a0..159e71e99e3 100644 --- a/searchlib/src/vespa/searchlib/attribute/predicate_attribute.h +++ b/searchlib/src/vespa/searchlib/attribute/predicate_attribute.h @@ -48,8 +48,8 @@ public: void onSave(IAttributeSaveTarget & saveTarget) override; bool onLoad(vespalib::Executor *executor) override; void onCommit() override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; void onUpdateStat() override; bool addDoc(DocId &doc_id) override; uint32_t clearDoc(DocId doc_id) override; diff --git a/searchlib/src/vespa/searchlib/attribute/reference_attribute.cpp b/searchlib/src/vespa/searchlib/attribute/reference_attribute.cpp index 0ebef4af8b0..fcee60ddac5 100644 --- a/searchlib/src/vespa/searchlib/attribute/reference_attribute.cpp +++ b/searchlib/src/vespa/searchlib/attribute/reference_attribute.cpp @@ -87,7 +87,7 @@ ReferenceAttribute::addDoc(DocId &doc) if (incGen) { incGeneration(); } else { - removeAllOldGenerations(); + reclaim_unused_memory(); } return true; } @@ -161,21 +161,21 @@ ReferenceAttribute::clearDoc(DocId doc) } void -ReferenceAttribute::removeOldGenerations(generation_t firstUsed) +ReferenceAttribute::reclaim_memory(generation_t oldest_used_gen) { - _referenceMappings.trimHoldLists(firstUsed); - _store.trimHoldLists(firstUsed); - getGenerationHolder().trimHoldLists(firstUsed); + _referenceMappings.reclaim_memory(oldest_used_gen); + _store.reclaim_memory(oldest_used_gen); + getGenerationHolder().reclaim(oldest_used_gen); } void -ReferenceAttribute::onGenerationChange(generation_t generation) +ReferenceAttribute::before_inc_generation(generation_t current_gen) { _referenceMappings.freeze(); _store.freeze(); - _referenceMappings.transferHoldLists(generation - 1); - _store.transferHoldLists(generation - 1); - getGenerationHolder().transferHoldLists(generation - 1); + _referenceMappings.assign_generation(current_gen); + _store.assign_generation(current_gen); + getGenerationHolder().assign_generation(current_gen); } void @@ -203,7 +203,7 @@ ReferenceAttribute::onUpdateStat() _compaction_spec = ReferenceAttributeCompactionSpec(compaction_strategy.should_compact_memory(total), compaction_strategy.should_compact_memory(dictionary_memory_usage)); total.merge(dictionary_memory_usage); - total.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + total.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); total.merge(_indices.getMemoryUsage()); total.merge(_referenceMappings.getMemoryUsage()); updateStatistics(getTotalValueCount(), getUniqueValueCount(), diff --git a/searchlib/src/vespa/searchlib/attribute/reference_attribute.h b/searchlib/src/vespa/searchlib/attribute/reference_attribute.h index dc3e2ad729a..e0ae906eb23 100644 --- a/searchlib/src/vespa/searchlib/attribute/reference_attribute.h +++ b/searchlib/src/vespa/searchlib/attribute/reference_attribute.h @@ -50,8 +50,8 @@ private: ReferenceMappings _referenceMappings; void onAddDocs(DocId docIdLimit) override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; void onCommit() override; void onUpdateStat() override; std::unique_ptr<AttributeSaver> onInitSave(vespalib::stringref fileName) override; diff --git a/searchlib/src/vespa/searchlib/attribute/reference_mappings.h b/searchlib/src/vespa/searchlib/attribute/reference_mappings.h index 2ccc164bf08..cf26b424208 100644 --- a/searchlib/src/vespa/searchlib/attribute/reference_mappings.h +++ b/searchlib/src/vespa/searchlib/attribute/reference_mappings.h @@ -59,9 +59,9 @@ public: void clearMapping(const Reference &entry); // Hold list management & freezing - void trimHoldLists(generation_t usedGen) { _reverseMapping.trimHoldLists(usedGen); } + void reclaim_memory(generation_t oldest_used_gen) { _reverseMapping.reclaim_memory(oldest_used_gen); } void freeze() { _reverseMapping.freeze(); } - void transferHoldLists(generation_t generation) { _reverseMapping.transferHoldLists(generation); } + void assign_generation(generation_t current_gen) { _reverseMapping.assign_generation(current_gen); } // Handle mapping changes void notifyReferencedPut(const Reference &entry, uint32_t targetLid); diff --git a/searchlib/src/vespa/searchlib/attribute/single_enum_search_context.cpp b/searchlib/src/vespa/searchlib/attribute/single_enum_search_context.cpp new file mode 100644 index 00000000000..c7faeaba977 --- /dev/null +++ b/searchlib/src/vespa/searchlib/attribute/single_enum_search_context.cpp @@ -0,0 +1,18 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "single_enum_search_context.hpp" +#include "string_search_context.h" +#include "numeric_range_matcher.h" +#include "numeric_search_context.h" + +namespace search::attribute { + +template class SingleEnumSearchContext<const char*, StringSearchContext>; +template class SingleEnumSearchContext<int8_t, NumericSearchContext<NumericRangeMatcher<int8_t>>>; +template class SingleEnumSearchContext<int16_t, NumericSearchContext<NumericRangeMatcher<int16_t>>>; +template class SingleEnumSearchContext<int32_t, NumericSearchContext<NumericRangeMatcher<int32_t>>>; +template class SingleEnumSearchContext<int64_t, NumericSearchContext<NumericRangeMatcher<int64_t>>>; +template class SingleEnumSearchContext<float, NumericSearchContext<NumericRangeMatcher<float>>>; +template class SingleEnumSearchContext<double, NumericSearchContext<NumericRangeMatcher<double>>>; + +} diff --git a/searchlib/src/vespa/searchlib/attribute/singleboolattribute.cpp b/searchlib/src/vespa/searchlib/attribute/singleboolattribute.cpp index 7574508517d..15fc819300c 100644 --- a/searchlib/src/vespa/searchlib/attribute/singleboolattribute.cpp +++ b/searchlib/src/vespa/searchlib/attribute/singleboolattribute.cpp @@ -29,7 +29,7 @@ SingleBoolAttribute(const vespalib::string &baseFileName, const GrowStrategy & g SingleBoolAttribute::~SingleBoolAttribute() { - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); } void @@ -53,7 +53,7 @@ SingleBoolAttribute::addDoc(DocId & doc) { incNumDocs(); doc = getNumDocs() - 1; updateUncommittedDocIdLimit(doc); - removeAllOldGenerations(); + reclaim_unused_memory(); return true; } @@ -80,7 +80,7 @@ SingleBoolAttribute::onCommit() { } std::atomic_thread_fence(std::memory_order_release); - removeAllOldGenerations(); + reclaim_unused_memory(); _changes.clear(); } @@ -95,7 +95,7 @@ SingleBoolAttribute::onUpdateStat() { vespalib::MemoryUsage usage; usage.setAllocatedBytes(_bv.writer().extraByteSize()); usage.setUsedBytes(_bv.writer().sizeBytes()); - usage.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + usage.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); usage.merge(this->getChangeVectorMemoryUsage()); this->updateStatistics(_bv.writer().size(), _bv.writer().size(), usage.allocatedBytes(), usage.usedBytes(), usage.deadBytes(), usage.allocatedBytesOnHold()); @@ -191,7 +191,7 @@ SingleBoolAttribute::onLoad(vespalib::Executor *) bool ok(attrReader.hasData()); if (ok) { setCreateSerialNum(attrReader.getCreateSerialNum()); - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); _bv.writer().clear(); uint32_t numDocs = attrReader.getNextData(); _bv.extend(numDocs); @@ -257,13 +257,13 @@ SingleBoolAttribute::getEstimatedSaveByteSize() const } void -SingleBoolAttribute::removeOldGenerations(generation_t firstUsed) { - getGenerationHolder().trimHoldLists(firstUsed); +SingleBoolAttribute::reclaim_memory(generation_t oldest_used_gen) { + getGenerationHolder().reclaim(oldest_used_gen); } void -SingleBoolAttribute::onGenerationChange(generation_t generation) { - getGenerationHolder().transferHoldLists(generation - 1); +SingleBoolAttribute::before_inc_generation(generation_t current_gen) { + getGenerationHolder().assign_generation(current_gen); } } diff --git a/searchlib/src/vespa/searchlib/attribute/singleboolattribute.h b/searchlib/src/vespa/searchlib/attribute/singleboolattribute.h index 7868c228e77..a02d5c7d80d 100644 --- a/searchlib/src/vespa/searchlib/attribute/singleboolattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/singleboolattribute.h @@ -28,8 +28,8 @@ public: void onSave(IAttributeSaveTarget &saveTarget) override; void clearDocs(DocId lidLow, DocId lidLimit, bool in_shrink_lid_space) override; void onShrinkLidSpace() override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; uint64_t getEstimatedSaveByteSize() const override; std::unique_ptr<attribute::SearchContext> diff --git a/searchlib/src/vespa/searchlib/attribute/singleenumattribute.h b/searchlib/src/vespa/searchlib/attribute/singleenumattribute.h index 6e46c697fbc..dbf3e4e7c58 100644 --- a/searchlib/src/vespa/searchlib/attribute/singleenumattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/singleenumattribute.h @@ -111,8 +111,8 @@ public: uint32_t getValueCount(DocId doc) const override; void onCommit() override; void onUpdateStat() override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; EnumHandle getEnum(DocId doc) const override { return getE(doc); } diff --git a/searchlib/src/vespa/searchlib/attribute/singleenumattribute.hpp b/searchlib/src/vespa/searchlib/attribute/singleenumattribute.hpp index c4abcfbc25a..52e2d914af1 100644 --- a/searchlib/src/vespa/searchlib/attribute/singleenumattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/singleenumattribute.hpp @@ -25,7 +25,7 @@ SingleValueEnumAttribute(const vespalib::string &baseFileName, template <typename B> SingleValueEnumAttribute<B>::~SingleValueEnumAttribute() { - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); } template <typename B> @@ -66,7 +66,7 @@ SingleValueEnumAttribute<B>::addDoc(DocId & doc) if (incGen) { this->incGeneration(); } else - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); return true; } @@ -95,7 +95,7 @@ SingleValueEnumAttribute<B>::onCommit() updater.commit(); freezeEnumDictionary(); std::atomic_thread_fence(std::memory_order_release); - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); auto remapper = this->_enumStore.consider_compact_values(this->getConfig().getCompactionStrategy()); if (remapper) { remap_enum_store_refs(*remapper, *this); @@ -128,7 +128,7 @@ SingleValueEnumAttribute<B>::onUpdateStat() // update statistics vespalib::MemoryUsage total = _enumIndices.getMemoryUsage(); auto& compaction_strategy = this->getConfig().getCompactionStrategy(); - total.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + total.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); total.merge(this->_enumStore.update_stat(compaction_strategy)); total.merge(this->getChangeVectorMemoryUsage()); mergeMemoryStats(total); @@ -218,7 +218,7 @@ SingleValueEnumAttribute<B>::fillValues(LoadedVector & loaded) { if constexpr (!std::is_same_v<LoadedVector, NoLoadedVector>) { uint32_t numDocs = this->getNumDocs(); - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); _enumIndices.reset(); _enumIndices.unsafe_reserve(numDocs); for (DocId doc = 0; doc < numDocs; ++doc, loaded.next()) { @@ -264,15 +264,15 @@ SingleValueEnumAttribute<B>::load_enumerated_data(ReaderBase& attrReader, template <typename B> void -SingleValueEnumAttribute<B>::removeOldGenerations(generation_t firstUsed) +SingleValueEnumAttribute<B>::reclaim_memory(generation_t oldest_used_gen) { - this->_enumStore.trim_hold_lists(firstUsed); - getGenerationHolder().trimHoldLists(firstUsed); + this->_enumStore.reclaim_memory(oldest_used_gen); + getGenerationHolder().reclaim(oldest_used_gen); } template <typename B> void -SingleValueEnumAttribute<B>::onGenerationChange(generation_t generation) +SingleValueEnumAttribute<B>::before_inc_generation(generation_t current_gen) { /* * Freeze tree before generation is increased in attribute vector @@ -281,8 +281,8 @@ SingleValueEnumAttribute<B>::onGenerationChange(generation_t generation) * sufficiently new frozen tree. */ freezeEnumDictionary(); - getGenerationHolder().transferHoldLists(generation - 1); - this->_enumStore.transfer_hold_lists(generation - 1); + getGenerationHolder().assign_generation(current_gen); + this->_enumStore.assign_generation(current_gen); } diff --git a/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.h b/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.h index fd2767eaee1..c6387323fea 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.h @@ -55,8 +55,8 @@ public: void onCommit() override; void onAddDocs(DocId lidLimit) override; void onUpdateStat() override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; bool addDoc(DocId & doc) override; bool onLoad(vespalib::Executor *executor) override; diff --git a/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.hpp b/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.hpp index b9c1c3686de..a105d980986 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/singlenumericattribute.hpp @@ -32,7 +32,7 @@ SingleValueNumericAttribute(const vespalib::string & baseFileName, const Attribu template <typename B> SingleValueNumericAttribute<B>::~SingleValueNumericAttribute() { - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); } template <typename B> @@ -55,7 +55,7 @@ SingleValueNumericAttribute<B>::onCommit() } } - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); this->_changes.clear(); } @@ -65,7 +65,7 @@ void SingleValueNumericAttribute<B>::onUpdateStat() { vespalib::MemoryUsage usage = _data.getMemoryUsage(); - usage.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + usage.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); usage.merge(this->getChangeVectorMemoryUsage()); this->updateStatistics(_data.size(), _data.size(), usage.allocatedBytes(), usage.usedBytes(), usage.deadBytes(), usage.allocatedBytesOnHold()); @@ -89,22 +89,22 @@ SingleValueNumericAttribute<B>::addDoc(DocId & doc) { if (incGen) { this->incGeneration(); } else - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); return true; } template <typename B> void -SingleValueNumericAttribute<B>::removeOldGenerations(generation_t firstUsed) +SingleValueNumericAttribute<B>::reclaim_memory(generation_t oldest_used_gen) { - getGenerationHolder().trimHoldLists(firstUsed); + getGenerationHolder().reclaim(oldest_used_gen); } template <typename B> void -SingleValueNumericAttribute<B>::onGenerationChange(generation_t generation) +SingleValueNumericAttribute<B>::before_inc_generation(generation_t current_gen) { - getGenerationHolder().transferHoldLists(generation - 1); + getGenerationHolder().assign_generation(current_gen); } template <typename B> @@ -143,7 +143,7 @@ SingleValueNumericAttribute<B>::onLoad(vespalib::Executor *) return onLoadEnumerated(attrReader); const size_t sz(attrReader.getDataCount()); - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); _data.reset(); _data.unsafe_reserve(sz); for (uint32_t i = 0; i < sz; ++i) { diff --git a/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.h b/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.h index 720fb211e1a..f2343c1a57c 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.h @@ -69,8 +69,8 @@ public: SingleValueNumericPostingAttribute(const vespalib::string & name, const AttributeVector::Config & cfg); ~SingleValueNumericPostingAttribute(); - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; std::unique_ptr<attribute::SearchContext> getSearch(QueryTermSimpleUP term, const attribute::SearchContextParams & params) const override; diff --git a/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.hpp b/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.hpp index 2050f887c33..1775774171d 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/singlenumericpostattribute.hpp @@ -127,19 +127,19 @@ SingleValueNumericPostingAttribute<B>::applyValueChanges(EnumStoreBatchUpdater& template <typename B> void -SingleValueNumericPostingAttribute<B>::removeOldGenerations(generation_t firstUsed) +SingleValueNumericPostingAttribute<B>::reclaim_memory(generation_t oldest_used_gen) { - SingleValueNumericEnumAttribute<B>::removeOldGenerations(firstUsed); - _postingList.trimHoldLists(firstUsed); + SingleValueNumericEnumAttribute<B>::reclaim_memory(oldest_used_gen); + _postingList.reclaim_memory(oldest_used_gen); } template <typename B> void -SingleValueNumericPostingAttribute<B>::onGenerationChange(generation_t generation) +SingleValueNumericPostingAttribute<B>::before_inc_generation(generation_t current_gen) { _postingList.freeze(); - SingleValueNumericEnumAttribute<B>::onGenerationChange(generation); - _postingList.transferHoldLists(generation - 1); + SingleValueNumericEnumAttribute<B>::before_inc_generation(current_gen); + _postingList.assign_generation(current_gen); } template <typename B> diff --git a/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.cpp b/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.cpp index b69ed017b52..13bf2f932e8 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.cpp +++ b/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.cpp @@ -36,7 +36,7 @@ SingleValueSmallNumericAttribute(const vespalib::string & baseFileName, SingleValueSmallNumericAttribute::~SingleValueSmallNumericAttribute() { - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); } void @@ -67,7 +67,7 @@ SingleValueSmallNumericAttribute::onCommit() } std::atomic_thread_fence(std::memory_order_release); - removeAllOldGenerations(); + reclaim_unused_memory(); _changes.clear(); } @@ -84,7 +84,7 @@ SingleValueSmallNumericAttribute::addDoc(DocId & doc) { if (incGen) { this->incGeneration(); } else - this->removeAllOldGenerations(); + this->reclaim_unused_memory(); } else { B::incNumDocs(); doc = B::getNumDocs() - 1; @@ -97,7 +97,7 @@ void SingleValueSmallNumericAttribute::onUpdateStat() { vespalib::MemoryUsage usage = _wordData.getMemoryUsage(); - usage.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + usage.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); uint32_t numDocs = B::getNumDocs(); updateStatistics(numDocs, numDocs, usage.allocatedBytes(), usage.usedBytes(), @@ -106,16 +106,16 @@ SingleValueSmallNumericAttribute::onUpdateStat() void -SingleValueSmallNumericAttribute::removeOldGenerations(generation_t firstUsed) +SingleValueSmallNumericAttribute::reclaim_memory(generation_t oldest_used_gen) { - getGenerationHolder().trimHoldLists(firstUsed); + getGenerationHolder().reclaim(oldest_used_gen); } void -SingleValueSmallNumericAttribute::onGenerationChange(generation_t generation) +SingleValueSmallNumericAttribute::before_inc_generation(generation_t current_gen) { - getGenerationHolder().transferHoldLists(generation - 1); + getGenerationHolder().assign_generation(current_gen); } @@ -127,7 +127,7 @@ SingleValueSmallNumericAttribute::onLoad(vespalib::Executor *) if (ok) { setCreateSerialNum(attrReader.getCreateSerialNum()); const size_t sz(attrReader.getDataCount()); - getGenerationHolder().clearHoldLists(); + getGenerationHolder().reclaim_all(); _wordData.reset(); _wordData.unsafe_reserve(sz - 1); Word numDocs = attrReader.getNextData(); diff --git a/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.h b/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.h index 4bf120d7952..b2af8752fa4 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/singlesmallnumericattribute.h @@ -72,8 +72,8 @@ public: void onCommit() override; void onAddDocs(DocId docIdLimit) override; void onUpdateStat() override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; bool addDoc(DocId & doc) override; bool onLoad(vespalib::Executor *executor) override; void onSave(IAttributeSaveTarget &saveTarget) override; diff --git a/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.h b/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.h index 30549f3048a..358c95f65dc 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.h +++ b/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.h @@ -71,8 +71,8 @@ public: SingleValueStringPostingAttributeT(const vespalib::string & name); ~SingleValueStringPostingAttributeT(); - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; std::unique_ptr<attribute::SearchContext> getSearch(QueryTermSimpleUP term, const attribute::SearchContextParams & params) const override; diff --git a/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.hpp b/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.hpp index f340d9f70c3..eef72984e79 100644 --- a/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.hpp +++ b/searchlib/src/vespa/searchlib/attribute/singlestringpostattribute.hpp @@ -127,19 +127,19 @@ SingleValueStringPostingAttributeT<B>::applyValueChanges(EnumStoreBatchUpdater& template <typename B> void -SingleValueStringPostingAttributeT<B>::removeOldGenerations(generation_t firstUsed) +SingleValueStringPostingAttributeT<B>::reclaim_memory(generation_t oldest_used_gen) { - SingleValueStringAttributeT<B>::removeOldGenerations(firstUsed); - _postingList.trimHoldLists(firstUsed); + SingleValueStringAttributeT<B>::reclaim_memory(oldest_used_gen); + _postingList.reclaim_memory(oldest_used_gen); } template <typename B> void -SingleValueStringPostingAttributeT<B>::onGenerationChange(generation_t generation) +SingleValueStringPostingAttributeT<B>::before_inc_generation(generation_t current_gen) { _postingList.freeze(); - SingleValueStringAttributeT<B>::onGenerationChange(generation); - _postingList.transferHoldLists(generation - 1); + SingleValueStringAttributeT<B>::before_inc_generation(current_gen); + _postingList.assign_generation(current_gen); } template <typename B> diff --git a/searchlib/src/vespa/searchlib/attribute/stringbase.cpp b/searchlib/src/vespa/searchlib/attribute/stringbase.cpp index b262b74a468..d27c61d6ff0 100644 --- a/searchlib/src/vespa/searchlib/attribute/stringbase.cpp +++ b/searchlib/src/vespa/searchlib/attribute/stringbase.cpp @@ -6,11 +6,9 @@ #include "readerbase.h" #include "enum_store_loaders.h" #include <vespa/searchlib/common/sort.h> -#include <vespa/document/fieldvalue/fieldvalue.h> #include <vespa/searchlib/query/query_term_ucs4.h> #include <vespa/searchcommon/attribute/config.h> #include <vespa/vespalib/locale/c.h> -#include <vespa/vespalib/util/array.hpp> #include <vespa/log/log.h> LOG_SETUP(".searchlib.attribute.stringbase"); @@ -61,7 +59,7 @@ StringAttribute::~StringAttribute() = default; uint32_t StringAttribute::get(DocId doc, WeightedInt * v, uint32_t sz) const { - WeightedConstChar * s = new WeightedConstChar[sz]; + auto * s = new WeightedConstChar[sz]; uint32_t n = static_cast<const AttributeVector *>(this)->get(doc, s, sz); for(uint32_t i(0),m(std::min(n,sz)); i<m; i++) { v[i] = WeightedInt(strtoll(s[i].getValue(), nullptr, 0), s[i].getWeight()); @@ -73,7 +71,7 @@ StringAttribute::get(DocId doc, WeightedInt * v, uint32_t sz) const uint32_t StringAttribute::get(DocId doc, WeightedFloat * v, uint32_t sz) const { - WeightedConstChar * s = new WeightedConstChar[sz]; + auto * s = new WeightedConstChar[sz]; uint32_t n = static_cast<const AttributeVector *>(this)->get(doc, s, sz); for(uint32_t i(0),m(std::min(n,sz)); i<m; i++) { v[i] = WeightedFloat(vespalib::locale::c::strtod(s[i].getValue(), nullptr), s[i].getWeight()); @@ -114,11 +112,11 @@ StringAttribute::get(DocId doc, largeint_t * v, uint32_t sz) const long StringAttribute::onSerializeForAscendingSort(DocId doc, void * serTo, long available, const common::BlobConverter * bc) const { - unsigned char *dst = static_cast<unsigned char *>(serTo); + auto *dst = static_cast<unsigned char *>(serTo); const char *value(get(doc)); int size = strlen(value) + 1; vespalib::ConstBufferRef buf(value, size); - if (bc != 0) { + if (bc != nullptr) { buf = bc->convert(buf); } if (available >= (long)buf.size()) { @@ -133,15 +131,15 @@ long StringAttribute::onSerializeForDescendingSort(DocId doc, void * serTo, long available, const common::BlobConverter * bc) const { (void) bc; - unsigned char *dst = static_cast<unsigned char *>(serTo); + auto *dst = static_cast<unsigned char *>(serTo); const char *value(get(doc)); int size = strlen(value) + 1; vespalib::ConstBufferRef buf(value, size); - if (bc != 0) { + if (bc != nullptr) { buf = bc->convert(buf); } if (available >= (long)buf.size()) { - const uint8_t * src(static_cast<const uint8_t *>(buf.data())); + const auto * src(static_cast<const uint8_t *>(buf.data())); for (size_t i(0), m(buf.size()); i < m; ++i) { dst[i] = 0xff - src[i]; } diff --git a/searchlib/src/vespa/searchlib/common/growablebitvector.cpp b/searchlib/src/vespa/searchlib/common/growablebitvector.cpp index e3334be3fd9..5f971e21cd3 100644 --- a/searchlib/src/vespa/searchlib/common/growablebitvector.cpp +++ b/searchlib/src/vespa/searchlib/common/growablebitvector.cpp @@ -72,7 +72,7 @@ bool GrowableBitVector::hold(GenerationHeldBase::UP v) { if (v) { - _generationHolder.hold(std::move(v)); + _generationHolder.insert(std::move(v)); return true; } return false; diff --git a/searchlib/src/vespa/searchlib/common/sortresults.cpp b/searchlib/src/vespa/searchlib/common/sortresults.cpp index 1add3501f61..4eed49defc5 100644 --- a/searchlib/src/vespa/searchlib/common/sortresults.cpp +++ b/searchlib/src/vespa/searchlib/common/sortresults.cpp @@ -3,9 +3,9 @@ #include "sortresults.h" #include "sort.h" #include <vespa/searchcommon/attribute/iattributecontext.h> -#include <vespa/vespalib/util/array.hpp> - +#include <vespa/vespalib/util/array.h> #include <vespa/vespalib/util/issue.h> + using vespalib::Issue; #include <vespa/log/log.h> @@ -34,28 +34,25 @@ public: } }; -} // namespace <unnamed> - - -inline void -FastS_insertion_sort(RankedHit a[], uint32_t n) -{ +void +insertion_sort(RankedHit a[], uint32_t n) { uint32_t i, j; RankedHit swap; typedef RadixHelper<search::HitRank> RT; RT R; - for (i=1; i<n ; i++) { + for (i = 1; i < n; i++) { swap = a[i]; j = i; - while (R(swap.getRank()) > R(a[j-1].getRank())) { - a[j] = a[j-1]; - if (!(--j)) break;; + while (R(swap.getRank()) > R(a[j - 1].getRank())) { + a[j] = a[j - 1]; + if (!(--j)) break; } a[j] = swap; } } +} template<int SHIFT> void @@ -133,14 +130,12 @@ FastS_radixsort(RankedHit a[], uint32_t n, uint32_t ntop) if ((last[i]-cnt[i])<ntop) { if (cnt[i]>INSERT_SORT_LEVEL) { if (last[i]<ntop) { - FastS_radixsort<SHIFT - 8>(&a[last[i]-cnt[i]], cnt[i], - cnt[i]); + FastS_radixsort<SHIFT - 8>(&a[last[i]-cnt[i]], cnt[i], cnt[i]); } else { - FastS_radixsort<SHIFT - 8>(&a[last[i]-cnt[i]], cnt[i], - cnt[i]+ntop-last[i]); + FastS_radixsort<SHIFT - 8>(&a[last[i]-cnt[i]], cnt[i], cnt[i]+ntop-last[i]); } } else if (cnt[i]>1) { - FastS_insertion_sort(&a[last[i]-cnt[i]], cnt[i]); + insertion_sort(&a[last[i]-cnt[i]], cnt[i]); } } } @@ -156,13 +151,13 @@ FastS_SortResults(RankedHit a[], uint32_t n, uint32_t ntop) if (n > INSERT_SORT_LEVEL) { FastS_radixsort<sizeof(search::HitRank)*8 - 8>(a, n, ntop); } else { - FastS_insertion_sort(a, n); + insertion_sort(a, n); } } //----------------------------------------------------------------------------- -FastS_DefaultResultSorter FastS_DefaultResultSorter::__instance; +FastS_DefaultResultSorter FastS_DefaultResultSorter::_instance; //----------------------------------------------------------------------------- @@ -173,12 +168,13 @@ FastS_SortSpec::Add(IAttributeContext & vecMan, const SortInfo & sInfo) return false; uint32_t type = ASC_VECTOR; - const IAttributeVector * vector(NULL); + const IAttributeVector * vector(nullptr); if ((sInfo._field.size() == 6) && (sInfo._field == "[rank]")) { type = (sInfo._ascending) ? ASC_RANK : DESC_RANK; } else if ((sInfo._field.size() == 7) && (sInfo._field == "[docid]")) { type = (sInfo._ascending) ? ASC_DOCID : DESC_DOCID; + vector = vecMan.getAttribute(_documentmetastore); } else { type = (sInfo._ascending) ? ASC_VECTOR : DESC_VECTOR; vector = vecMan.getAttribute(sInfo._field); @@ -220,16 +216,18 @@ FastS_SortSpec::initSortData(const RankedHit *hits, uint32_t n) freeSortData(); size_t fixedWidth = 0; size_t variableWidth = 0; - for (auto iter = _vectors.begin(); iter != _vectors.end(); ++iter) { - if (iter->_type >= ASC_DOCID) { // doc id - fixedWidth += sizeof(uint32_t) + sizeof(uint16_t); - }else if (iter->_type >= ASC_RANK) { // rank value + for (const auto & vec : _vectors) { + if (vec._type >= ASC_DOCID) { // doc id + fixedWidth = (vec._vector != nullptr) + ? vec._vector->getFixedWidth() + : sizeof(uint32_t) + sizeof(uint16_t); + } else if (vec._type >= ASC_RANK) { // rank value fixedWidth += sizeof(search::HitRank); } else { - size_t numBytes = iter->_vector->getFixedWidth(); + size_t numBytes = vec._vector->getFixedWidth(); if (numBytes == 0) { // string variableWidth += 11; - } else if (!iter->_vector->hasMultiValue()) { + } else if (!vec._vector->hasMultiValue()) { fixedWidth += numBytes; } } @@ -243,22 +241,30 @@ FastS_SortSpec::initSortData(const RankedHit *hits, uint32_t n) for (uint32_t i(0), idx(0); (i < n) && !_doom.hard_doom(); ++i) { uint32_t len = 0; - for (auto iter = _vectors.begin(); iter != _vectors.end(); ++iter) { - int written(0); + for (const auto & vec : _vectors) { + long written(0); if (available < std::max(sizeof(hits->_docId) + sizeof(_partitionId), sizeof(hits->_rankValue))) { mySortData = realloc(n, variableWidth, available, dataSize, mySortData); } do { - switch (iter->_type) { + switch (vec._type) { case ASC_DOCID: - serializeForSort<convertForSort<uint32_t, true> >(hits[i].getDocId(), mySortData); - serializeForSort<convertForSort<uint16_t, true> >(_partitionId, mySortData + sizeof(hits->_docId)); - written = sizeof(hits->_docId) + sizeof(_partitionId); + if (vec._vector != nullptr) { + written = vec._vector->serializeForAscendingSort(hits[i].getDocId(), mySortData, available, vec._converter); + } else { + serializeForSort<convertForSort<uint32_t, true> >(hits[i].getDocId(), mySortData); + serializeForSort<convertForSort<uint16_t, true> >(_partitionId, mySortData + sizeof(hits->_docId)); + written = sizeof(hits->_docId) + sizeof(_partitionId); + } break; case DESC_DOCID: - serializeForSort<convertForSort<uint32_t, false> >(hits[i].getDocId(), mySortData); - serializeForSort<convertForSort<uint16_t, false> >(_partitionId, mySortData + sizeof(hits->_docId)); - written = sizeof(hits->_docId) + sizeof(_partitionId); + if (vec._vector != nullptr) { + written = vec._vector->serializeForDescendingSort(hits[i].getDocId(), mySortData, available, vec._converter); + } else { + serializeForSort<convertForSort<uint32_t, false> >(hits[i].getDocId(), mySortData); + serializeForSort<convertForSort<uint16_t, false> >(_partitionId, mySortData + sizeof(hits->_docId)); + written = sizeof(hits->_docId) + sizeof(_partitionId); + } break; case ASC_RANK: serializeForSort<convertForSort<search::HitRank, true> >(hits[i].getRank(), mySortData); @@ -269,10 +275,10 @@ FastS_SortSpec::initSortData(const RankedHit *hits, uint32_t n) written = sizeof(hits->_rankValue); break; case ASC_VECTOR: - written = iter->_vector->serializeForAscendingSort(hits[i].getDocId(), mySortData, available, iter->_converter); + written = vec._vector->serializeForAscendingSort(hits[i].getDocId(), mySortData, available, vec._converter); break; case DESC_VECTOR: - written = iter->_vector->serializeForDescendingSort(hits[i].getDocId(), mySortData, available, iter->_converter); + written = vec._vector->serializeForDescendingSort(hits[i].getDocId(), mySortData, available, vec._converter); break; } if (written == -1) { @@ -293,13 +299,13 @@ FastS_SortSpec::initSortData(const RankedHit *hits, uint32_t n) } } - -FastS_SortSpec::FastS_SortSpec(uint32_t partitionId, const Doom & doom, const ConverterFactory & ucaFactory) : - _partitionId(partitionId), - _doom(doom), - _ucaFactory(ucaFactory), - _sortSpec(), - _vectors() +FastS_SortSpec::FastS_SortSpec(vespalib::stringref documentmetastore, uint32_t partitionId, const Doom & doom, const ConverterFactory & ucaFactory) + : _documentmetastore(documentmetastore), + _partitionId(partitionId), + _doom(doom), + _ucaFactory(ucaFactory), + _sortSpec(), + _vectors() { } @@ -308,7 +314,6 @@ FastS_SortSpec::~FastS_SortSpec() freeSortData(); } - bool FastS_SortSpec::Init(const string & sortStr, IAttributeContext & vecMan) { @@ -316,7 +321,7 @@ FastS_SortSpec::Init(const string & sortStr, IAttributeContext & vecMan) bool retval(true); try { _sortSpec = SortSpec(sortStr, _ucaFactory); - for (SortSpec::const_iterator it(_sortSpec.begin()), mt(_sortSpec.end()); retval && (it < mt); it++) { + for (auto it(_sortSpec.begin()); retval && (it != _sortSpec.end()); it++) { retval = Add(vecMan, *it); } } catch (const std::exception & e) { @@ -374,26 +379,11 @@ FastS_SortSpec::initWithoutSorting(const RankedHit * hits, uint32_t hitCnt) initSortData(hits, hitCnt); } -inline int -FastS_SortSpec::Compare(const FastS_SortSpec *self, const SortData &a, - const SortData &b) -{ - const uint8_t * ref = self->_binarySortData.data(); - uint32_t len = a._len < b._len ? a._len : b._len; - int retval = memcmp(ref + a._idx, - ref + b._idx, len); - if (retval < 0) { - return -1; - } else if (retval > 0) { - return 1; - } - return 0; -} class StdSortDataCompare { public: - StdSortDataCompare(const uint8_t * s) : _sortSpec(s) { } + explicit StdSortDataCompare(const uint8_t * s) : _sortSpec(s) { } bool operator() (const FastS_SortSpec::SortData & x, const FastS_SortSpec::SortData & y) const { return cmp(x, y) < 0; } @@ -409,7 +399,7 @@ private: class SortDataRadix { public: - SortDataRadix(const uint8_t * s) : _data(s) { } + explicit SortDataRadix(const uint8_t * s) : _data(s) { } uint32_t operator () (FastS_SortSpec::SortData & a) const { uint32_t r(0); uint32_t left(a._len - a._pos); @@ -448,10 +438,11 @@ void FastS_SortSpec::sortResults(RankedHit a[], uint32_t n, uint32_t topn) { initSortData(a, n); - SortData * sortData = _sortDataArray.data(); { + SortData * sortData = _sortDataArray.data(); + const uint8_t * binary = _binarySortData.data(); Array<uint32_t> radixScratchPad(n, Alloc::alloc(0, MMAP_LIMIT)); - search::radix_sort(SortDataRadix(_binarySortData.data()), StdSortDataCompare(_binarySortData.data()), SortDataEof(), 1, sortData, n, radixScratchPad.data(), 0, 96, topn); + search::radix_sort(SortDataRadix(binary), StdSortDataCompare(binary), SortDataEof(), 1, sortData, n, radixScratchPad.data(), 0, 96, topn); } for (uint32_t i(0), m(_sortDataArray.size()); i < m; ++i) { a[i]._rankValue = _sortDataArray[i]._rankValue; diff --git a/searchlib/src/vespa/searchlib/common/sortresults.h b/searchlib/src/vespa/searchlib/common/sortresults.h index 3e64aca269d..337863601d5 100644 --- a/searchlib/src/vespa/searchlib/common/sortresults.h +++ b/searchlib/src/vespa/searchlib/common/sortresults.h @@ -4,7 +4,7 @@ #include "rankedhit.h" #include "sortspec.h" -#include <vespa/vespalib/util/array.h> +#include <vespa/vespalib/stllike/allocator.h> #include <vespa/vespalib/util/doom.h> #define INSERT_SORT_LEVEL 80 @@ -28,7 +28,7 @@ struct FastS_IResultSorter { /** * Destructor. No cleanup needed for base class. */ - virtual ~FastS_IResultSorter() {} + virtual ~FastS_IResultSorter() = default; /** * Sort the given array of results. @@ -45,10 +45,10 @@ struct FastS_IResultSorter { class FastS_DefaultResultSorter : public FastS_IResultSorter { private: - static FastS_DefaultResultSorter __instance; + static FastS_DefaultResultSorter _instance; public: - static FastS_DefaultResultSorter *instance() { return &__instance; } + static FastS_DefaultResultSorter *instance() { return &_instance; } void sortResults(search::RankedHit a[], uint32_t n, uint32_t ntop) override { return FastS_SortResults(a, n, ntop); } @@ -72,7 +72,7 @@ public: struct VectorRef { - VectorRef(uint32_t type, const search::attribute::IAttributeVector * vector, const search::common::BlobConverter *converter) + VectorRef(uint32_t type, const search::attribute::IAttributeVector * vector, const search::common::BlobConverter *converter) noexcept : _type(type), _vector(vector), _converter(converter) @@ -84,16 +84,18 @@ public: struct SortData : public search::RankedHit { + SortData() noexcept : RankedHit(), _idx(0u), _len(0u), _pos(0u) {} uint32_t _idx; uint32_t _len; uint32_t _pos; }; private: - typedef std::vector<VectorRef> VectorRefList; - typedef vespalib::Array<uint8_t> BinarySortData; - typedef vespalib::Array<SortData> SortDataArray; + using VectorRefList = std::vector<VectorRef>; + using BinarySortData = std::vector<uint8_t, vespalib::allocator_large<uint8_t>>; + using SortDataArray = std::vector<SortData, vespalib::allocator_large<SortData>>; using ConverterFactory = search::common::ConverterFactory; + vespalib::string _documentmetastore; uint16_t _partitionId; vespalib::Doom _doom; const ConverterFactory & _ucaFactory; @@ -109,12 +111,11 @@ private: public: FastS_SortSpec(const FastS_SortSpec &) = delete; FastS_SortSpec & operator = (const FastS_SortSpec &) = delete; - FastS_SortSpec(uint32_t partitionId, const vespalib::Doom & doom, const ConverterFactory & ucaFactory); - ~FastS_SortSpec(); + FastS_SortSpec(vespalib::stringref documentmetastore, uint32_t partitionId, const vespalib::Doom & doom, const ConverterFactory & ucaFactory); + ~FastS_SortSpec() override; std::pair<const char *, size_t> getSortRef(size_t i) const { - return std::pair<const char *, size_t>((const char*)(&_binarySortData[0] + _sortDataArray[i]._idx), - _sortDataArray[i]._len); + return {(const char*)(&_binarySortData[0] + _sortDataArray[i]._idx), _sortDataArray[i]._len }; } bool Init(const vespalib::string & sortSpec, search::attribute::IAttributeContext & vecMan); void sortResults(search::RankedHit a[], uint32_t n, uint32_t topn) override; @@ -122,7 +123,6 @@ public: void copySortData(uint32_t offset, uint32_t n, uint32_t *idx, char *buf); void freeSortData(); void initWithoutSorting(const search::RankedHit * hits, uint32_t hitCnt); - static int Compare(const FastS_SortSpec *self, const SortData &a, const SortData &b); }; //----------------------------------------------------------------------------- diff --git a/searchlib/src/vespa/searchlib/common/sortspec.cpp b/searchlib/src/vespa/searchlib/common/sortspec.cpp index 99def167460..16f0c884535 100644 --- a/searchlib/src/vespa/searchlib/common/sortspec.cpp +++ b/searchlib/src/vespa/searchlib/common/sortspec.cpp @@ -36,8 +36,8 @@ LowercaseConverter::onConvert(const ConstBufferRef & src) const return {_buffer.begin(), _buffer.size()}; } -SortInfo::SortInfo(const vespalib::string & field, bool ascending, const BlobConverter::SP & converter) - : _field(field), _ascending(ascending), _converter(converter) +SortInfo::SortInfo(vespalib::stringref field, bool ascending, BlobConverter::SP converter) noexcept + : _field(field), _ascending(ascending), _converter(std::move(converter)) { } SortInfo::~SortInfo() = default; @@ -72,13 +72,13 @@ SortSpec::SortSpec(const vespalib::string & spec, const ConverterFactory & ucaFa for(; (p < e) && (*p != ')'); p++); if (*p == ')') { vespalib::string strength(strengthName, p - strengthName); - push_back(SortInfo(attr, ascending, BlobConverter::SP(ucaFactory.create(locale, strength)))); + emplace_back(attr, ascending, ucaFactory.create(locale, strength)); } else { throw std::runtime_error(make_string("Missing ')' at %s attr=%s locale=%s strength=%s", p, attr.c_str(), localeName, strengthName)); } } else if (*p == ')') { vespalib::string locale(localeName, p-localeName); - push_back(SortInfo(attr, ascending, BlobConverter::SP(ucaFactory.create(locale, "")))); + emplace_back(attr, ascending, ucaFactory.create(locale, "")); } else { throw std::runtime_error(make_string("Missing ')' or ',' at %s attr=%s locale=%s", p, attr.c_str(), localeName)); } @@ -91,7 +91,7 @@ SortSpec::SortSpec(const vespalib::string & spec, const ConverterFactory & ucaFa for(; (p < e) && (*p != ')'); p++); if (*p == ')') { vespalib::string attr(attrName, p-attrName); - push_back(SortInfo(attr, ascending, BlobConverter::SP(new LowercaseConverter()))); + emplace_back(attr, ascending, std::make_shared<LowercaseConverter>()); } else { throw std::runtime_error("Missing ')'"); } @@ -99,7 +99,7 @@ SortSpec::SortSpec(const vespalib::string & spec, const ConverterFactory & ucaFa throw std::runtime_error("Unknown func " + vespalib::string(func, p-func)); } } else { - push_back(SortInfo(funcSpec, ascending, BlobConverter::SP())); + emplace_back(funcSpec, ascending, std::shared_ptr<search::common::BlobConverter>()); } } } diff --git a/searchlib/src/vespa/searchlib/common/sortspec.h b/searchlib/src/vespa/searchlib/common/sortspec.h index da98d791734..682612afd43 100644 --- a/searchlib/src/vespa/searchlib/common/sortspec.h +++ b/searchlib/src/vespa/searchlib/common/sortspec.h @@ -11,7 +11,7 @@ namespace search::common { struct SortInfo { - SortInfo(const vespalib::string & field, bool ascending, const BlobConverter::SP & converter); + SortInfo(vespalib::stringref field, bool ascending, BlobConverter::SP converter) noexcept; ~SortInfo(); vespalib::string _field; bool _ascending; diff --git a/searchlib/src/vespa/searchlib/common/threaded_compactable_lid_space.cpp b/searchlib/src/vespa/searchlib/common/threaded_compactable_lid_space.cpp index cd4069ac959..80f5f1ac5e8 100644 --- a/searchlib/src/vespa/searchlib/common/threaded_compactable_lid_space.cpp +++ b/searchlib/src/vespa/searchlib/common/threaded_compactable_lid_space.cpp @@ -8,7 +8,7 @@ namespace search::common { ThreadedCompactableLidSpace::ThreadedCompactableLidSpace(std::shared_ptr<ICompactableLidSpace> target, ISequencedTaskExecutor &executor, ISequencedTaskExecutor::ExecutorId id) - : _target(target), + : _target(std::move(target)), _executor(executor), _executorId(id) { diff --git a/searchlib/src/vespa/searchlib/docstore/logdatastore.cpp b/searchlib/src/vespa/searchlib/docstore/logdatastore.cpp index 7036ef238b6..381bf0715b0 100644 --- a/searchlib/src/vespa/searchlib/docstore/logdatastore.cpp +++ b/searchlib/src/vespa/searchlib/docstore/logdatastore.cpp @@ -109,8 +109,8 @@ LogDataStore::~LogDataStore() { // Must be called before ending threads as there are sanity checks. _fileChunks.clear(); - _genHandler.updateFirstUsedGeneration(); - _lidInfo.removeOldGenerations(_genHandler.getFirstUsedGeneration()); + _genHandler.update_oldest_used_generation(); + _lidInfo.reclaim_memory(_genHandler.get_oldest_used_generation()); } void @@ -485,8 +485,8 @@ void LogDataStore::compactFile(FileId fileId) FileChunk::UP toDie; for (;;) { MonitorGuard guard(_updateLock); - _genHandler.updateFirstUsedGeneration(); - if (currentGeneration < _genHandler.getFirstUsedGeneration()) { + _genHandler.update_oldest_used_generation(); + if (currentGeneration < _genHandler.get_oldest_used_generation()) { if (_holdFileChunks[fc->getFileId().getId()] == 0u) { toDie = std::move(fc); break; @@ -939,8 +939,8 @@ LogDataStore::setLid(const MonitorGuard &guard, uint32_t lid, const LidInfo &met { (void) guard; if (lid < _lidInfo.size()) { - _genHandler.updateFirstUsedGeneration(); - _lidInfo.removeOldGenerations(_genHandler.getFirstUsedGeneration()); + _genHandler.update_oldest_used_generation(); + _lidInfo.reclaim_memory(_genHandler.get_oldest_used_generation()); const LidInfo prev = vespalib::atomic::load_ref_relaxed(_lidInfo[lid]); if (prev.valid()) { _fileChunks[prev.getFileId()]->remove(lid, prev.size()); @@ -958,8 +958,7 @@ LogDataStore::incGeneration() { _lidInfo.setGeneration(_genHandler.getNextGeneration()); _genHandler.incGeneration(); - _genHandler.updateFirstUsedGeneration(); - _lidInfo.removeOldGenerations(_genHandler.getFirstUsedGeneration()); + _lidInfo.reclaim_memory(_genHandler.get_oldest_used_generation()); } size_t @@ -1213,7 +1212,7 @@ LogDataStore::canShrinkLidSpace(const MonitorGuard &) const { // Update lock is held, allowing call to _lidInfo.get_size() return getDocIdLimit() < _lidInfo.get_size() && - _compactLidSpaceGeneration < _genHandler.getFirstUsedGeneration(); + _compactLidSpaceGeneration < _genHandler.get_oldest_used_generation(); } size_t diff --git a/searchlib/src/vespa/searchlib/index/CMakeLists.txt b/searchlib/src/vespa/searchlib/index/CMakeLists.txt index 958614844d1..0bf912a8a11 100644 --- a/searchlib/src/vespa/searchlib/index/CMakeLists.txt +++ b/searchlib/src/vespa/searchlib/index/CMakeLists.txt @@ -2,11 +2,8 @@ vespa_add_library(searchlib_searchlib_index OBJECT SOURCES dictionaryfile.cpp - docbuilder.cpp docidandfeatures.cpp - doctypebuilder.cpp dummyfileheadercontext.cpp - empty_doc_builder.cpp indexbuilder.cpp postinglisthandle.cpp postinglistcounts.cpp diff --git a/searchlib/src/vespa/searchlib/index/docbuilder.cpp b/searchlib/src/vespa/searchlib/index/docbuilder.cpp deleted file mode 100644 index d6169f2f396..00000000000 --- a/searchlib/src/vespa/searchlib/index/docbuilder.cpp +++ /dev/null @@ -1,814 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include "docbuilder.h" -#include <vespa/document/datatype/urldatatype.h> -#include <vespa/document/datatype/documenttype.h> -#include <vespa/document/repo/documenttyperepo.h> -#include <vespa/fastlib/text/unicodeutil.h> -#include <vespa/vespalib/geo/zcurve.h> -#include <vespa/vespalib/text/utf8.h> -#include <vespa/eval/eval/value.h> -#include <vespa/vespalib/data/slime/slime.h> - -using namespace document; -using namespace search::index; - -using search::index::schema::CollectionType; -using vespalib::Utf8Reader; -using vespalib::Utf8Writer; -using vespalib::geo::ZCurve; - -namespace { - -void -insertStr(const Schema::Field & sfield, document::FieldValue * fvalue, const vespalib::string & val) -{ - if (sfield.getDataType() == schema::DataType::STRING || - sfield.getDataType() == schema::DataType::RAW) - { - (dynamic_cast<LiteralFieldValueB *>(fvalue))->setValue(val); - } else { - throw DocBuilder::Error(vespalib::make_string("Field '%s' not compatible", sfield.getName().c_str())); - } -} - -void -insertInt(const Schema::Field & sfield, document::FieldValue * fvalue, int64_t val) -{ - if (sfield.getDataType() == schema::DataType::INT8) { - (dynamic_cast<ByteFieldValue *>(fvalue))->setValue((uint8_t)val); - } else if (sfield.getDataType() == schema::DataType::INT16) { - (dynamic_cast<ShortFieldValue *>(fvalue))->setValue((int16_t)val); - } else if (sfield.getDataType() == schema::DataType::INT32) { - (dynamic_cast<IntFieldValue *>(fvalue))->setValue((int32_t)val); - } else if (sfield.getDataType() == schema::DataType::INT64) { - (dynamic_cast<LongFieldValue *>(fvalue))->setValue(val); - } else { - throw DocBuilder::Error(vespalib::make_string("Field '%s' not compatible", sfield.getName().c_str())); - } -} - -void -insertFloat(const Schema::Field & sfield, document::FieldValue * fvalue, double val) -{ - if (sfield.getDataType() == schema::DataType::FLOAT) { - (dynamic_cast<FloatFieldValue *>(fvalue))->setValue((float)val); - } else if (sfield.getDataType() == schema::DataType::DOUBLE) { - (dynamic_cast<DoubleFieldValue *>(fvalue))->setValue(val); - } else { - throw DocBuilder::Error(vespalib::make_string("Field '%s' not compatible", sfield.getName().c_str())); - } -} - -void insertPredicate(const Schema::Field &sfield, - document::FieldValue *fvalue, - std::unique_ptr<vespalib::Slime> val) { - if (sfield.getDataType() == schema::DataType::BOOLEANTREE) { - *(dynamic_cast<PredicateFieldValue *>(fvalue)) = - PredicateFieldValue(std::move(val)); - } else { - throw DocBuilder::Error(vespalib::make_string( - "Field '%s' not compatible", - sfield.getName().c_str())); - } -} - -void insertTensor(const Schema::Field &schemaField, - document::FieldValue *fvalue, - std::unique_ptr<vespalib::eval::Value> val) { - if (schemaField.getDataType() == schema::DataType::TENSOR) { - *(dynamic_cast<TensorFieldValue *>(fvalue)) = std::move(val); - } else { - throw DocBuilder::Error(vespalib::make_string( - "Field '%s' not compatible", - schemaField.getName().c_str())); - } -} - -void -insertPosition(const Schema::Field & sfield, - document::FieldValue * fvalue, int32_t xpos, int32_t ypos) -{ - assert(*fvalue->getDataType() == *DataType::LONG); - assert(sfield.getDataType() == schema::DataType::INT64); - (void) sfield; - int64_t zpos = ZCurve::encode(xpos, ypos); - document::LongFieldValue *zvalue = - dynamic_cast<LongFieldValue *>(fvalue); - zvalue->setValue(zpos); -} - -} - -namespace docbuilderkludge -{ - -namespace linguistics -{ - -const vespalib::string SPANTREE_NAME("linguistics"); - -enum TokenType { - UNKNOWN = 0, - SPACE = 1, - PUNCTUATION = 2, - SYMBOL = 3, - ALPHABETIC = 4, - NUMERIC = 5, - MARKER = 6 -}; - -} - -} - -using namespace docbuilderkludge; - -namespace { - -Annotation -makeTokenType(linguistics::TokenType type) -{ - return Annotation(*AnnotationType::TOKEN_TYPE, std::make_unique<IntFieldValue>(type)); -} - -} - -namespace search::index { - -VESPA_IMPLEMENT_EXCEPTION(DocBuilderError, vespalib::Exception); - -DocBuilder::FieldHandle::FieldHandle(const document::Field & dfield, const Schema::Field & field) : - _sfield(field), - _value(), - _element() -{ - _value = dfield.createValue(); -} - -DocBuilder::CollectionFieldHandle::CollectionFieldHandle(const document::Field & dfield, const Schema::Field & field) : - FieldHandle(dfield, field), - _elementWeight(1) -{ -} - -void -DocBuilder::CollectionFieldHandle::startElement(int32_t weight) -{ - assert(!_element); - _elementWeight = weight; - const CollectionFieldValue * value = dynamic_cast<CollectionFieldValue *>(_value.get()); - _element = value->createNested(); -} - -void -DocBuilder::CollectionFieldHandle::endElement() -{ - if (_sfield.getCollectionType() == CollectionType::ARRAY) { - onEndElement(); - ArrayFieldValue * value = dynamic_cast<ArrayFieldValue *>(_value.get()); - value->add(*_element); - } else if (_sfield.getCollectionType() == CollectionType::WEIGHTEDSET) { - onEndElement(); - WeightedSetFieldValue * value = dynamic_cast<WeightedSetFieldValue *>(_value.get()); - value->add(*_element, _elementWeight); - } else { - throw Error(vespalib::make_string("Field '%s' not compatible", _sfield.getName().c_str())); - } - _element.reset(); -} - -DocBuilder::IndexFieldHandle::IndexFieldHandle(const FixedTypeRepo & repo, const document::Field & dfield, const Schema::Field & sfield) - : CollectionFieldHandle(dfield, sfield), - _str(), - _strSymbols(0u), - _spanList(nullptr), - _spanTree(), - _lastSpan(nullptr), - _spanStart(0u), - _autoAnnotate(true), - _autoSpace(true), - _skipAutoSpace(true), - _uriField(false), - _subField(), - _repo(repo) -{ - _str.reserve(1023); - - if (_sfield.getCollectionType() == CollectionType::SINGLE) { - if (*_value->getDataType() == document::UrlDataType::getInstance()) { - _uriField = true; - } - } else { - const CollectionFieldValue * value = dynamic_cast<CollectionFieldValue *>(_value.get()); - if (value->getNestedType() == document::UrlDataType::getInstance()) { - _uriField = true; - } - } - startAnnotate(); -} - -void -DocBuilder::IndexFieldHandle::append(const vespalib::string &val) -{ - _strSymbols += val.size(); - _str += val; -} - -void -DocBuilder::IndexFieldHandle::addStr(const vespalib::string &val) -{ - assert(_spanTree); - if (val.empty()) { - return; - } - if (!_skipAutoSpace && _autoSpace) { - addSpace(); - } - _skipAutoSpace = false; - _spanStart = _strSymbols; - append(val); - if (_autoAnnotate) { - addSpan(); - addTermAnnotation(); - if (val[0] >= '0' && val[0] <= '9') { - addNumericTokenAnnotation(); - } else { - addAlphabeticTokenAnnotation(); - } - } -} - -void -DocBuilder::IndexFieldHandle::addSpace() -{ - addNoWordStr(" "); -} - -void -DocBuilder::IndexFieldHandle::addNoWordStr(const vespalib::string &val) -{ - assert(_spanTree); - if (val.empty()) { - return; - } - _spanStart = _strSymbols; - append(val); - if (_autoAnnotate) { - addSpan(); - if (val[0] == ' ' || val[0] == '\t') { - addSpaceTokenAnnotation(); - } else if (val[0] >= '0' && val[0] <= '9') { - addNumericTokenAnnotation(); - } else { - addAlphabeticTokenAnnotation(); - } - - } - _skipAutoSpace = true; -} - -void -DocBuilder::IndexFieldHandle::addTokenizedString(const vespalib::string &val, - bool urlMode) -{ - Utf8Reader r(val); - vespalib::string sbuf; - Utf8Writer w(sbuf); - uint32_t c = 0u; - bool oldWord = false; - assert(_uriField == urlMode); - assert(_uriField != _subField.empty()); - - while (r.hasMore()) { - c = r.getChar(); - bool newWord = Fast_UnicodeUtil::IsWordChar(c) || - (urlMode && (c == '-' || c == '_')); - if (oldWord != newWord) { - if (!sbuf.empty()) { - if (oldWord) { - addStr(sbuf); - } else { - addNoWordStr(sbuf); - } - sbuf.clear(); - } - oldWord = newWord; - } - w.putChar(c); - } - if (!sbuf.empty()) { - if (oldWord) { - addStr(sbuf); - } else { - addNoWordStr(sbuf); - } - } -} - -void -DocBuilder::IndexFieldHandle::addSpan(size_t start, size_t len) -{ - const SpanNode &span = _spanList->add(std::make_unique<Span>(start, len)); - _lastSpan = &span; -} - -void -DocBuilder::IndexFieldHandle::addSpan() -{ - size_t endPos = _strSymbols; - assert(endPos > _spanStart); - addSpan(_spanStart, endPos - _spanStart); - _spanStart = endPos; -} - -void -DocBuilder::IndexFieldHandle::addSpaceTokenAnnotation() -{ - assert(_spanTree); - assert(_lastSpan != nullptr); - _spanTree->annotate(*_lastSpan, makeTokenType(linguistics::SPACE)); -} - -void -DocBuilder::IndexFieldHandle::addNumericTokenAnnotation() -{ - assert(_spanTree); - assert(_lastSpan != nullptr); - _spanTree->annotate(*_lastSpan, makeTokenType(linguistics::NUMERIC)); -} - -void -DocBuilder::IndexFieldHandle::addAlphabeticTokenAnnotation() -{ - assert(_spanTree); - assert(_lastSpan != nullptr); - _spanTree->annotate(*_lastSpan, makeTokenType(linguistics::ALPHABETIC)); -} - -void -DocBuilder::IndexFieldHandle::addTermAnnotation() -{ - assert(_spanTree); - assert(_lastSpan != nullptr); - _spanTree->annotate(*_lastSpan, *AnnotationType::TERM); -} - -void -DocBuilder::IndexFieldHandle::addTermAnnotation(const vespalib::string &val) -{ - assert(_spanTree); - assert(_lastSpan != nullptr); - _spanTree->annotate(*_lastSpan, - Annotation(*AnnotationType::TERM, - std::make_unique<StringFieldValue>(val))); -} - -void -DocBuilder::IndexFieldHandle::onEndElement() -{ - // Flush data for index field. - assert(_subField.empty()); - if (_uriField) { - return; - } - StringFieldValue * value; - if (_sfield.getCollectionType() != CollectionType::SINGLE) { - value = dynamic_cast<StringFieldValue *>(_element.get()); - } else { - value = dynamic_cast<StringFieldValue *>(_value.get()); - } - value->setValue(_str); - // Also drop all spans no annotation for now - if (_spanTree->numAnnotations() > 0u) { - StringFieldValue::SpanTrees trees; - trees.emplace_back(std::move(_spanTree)); - value->setSpanTrees(trees, _repo); - } else { - _spanTree.reset(); - } - _spanList = nullptr; - _lastSpan = nullptr; - _spanStart = 0u; - _strSymbols = 0u; - _str.clear(); - _skipAutoSpace = true; - startAnnotate(); -} - -void -DocBuilder::IndexFieldHandle::onEndField() -{ - if (_sfield.getCollectionType() == CollectionType::SINGLE) { - onEndElement(); - } -} - -void -DocBuilder::IndexFieldHandle::startAnnotate() -{ - SpanList::UP span_list(new SpanList); - _spanList = span_list.get(); - _spanTree.reset(new SpanTree(linguistics::SPANTREE_NAME, std::move(span_list))); -} - -void -DocBuilder::IndexFieldHandle::setAutoAnnotate(bool autoAnnotate) -{ - _autoAnnotate = autoAnnotate; -} - -void -DocBuilder::IndexFieldHandle::setAutoSpace(bool autoSpace) -{ - _autoSpace = autoSpace; -} - -void -DocBuilder::IndexFieldHandle::startSubField(const vespalib::string &subField) -{ - assert(_subField.empty()); - assert(_uriField); - _subField = subField; -} - -void -DocBuilder::IndexFieldHandle::endSubField() -{ - assert(!_subField.empty()); - assert(_uriField); - StructuredFieldValue *sValue; - if (_sfield.getCollectionType() != CollectionType::SINGLE) { - sValue = dynamic_cast<StructFieldValue *>(_element.get()); - } else { - sValue = dynamic_cast<StructFieldValue *>(_value.get()); - } - const Field &f = sValue->getField(_subField); - FieldValue::UP fval(f.getDataType().createFieldValue()); - *fval = _str; - StringFieldValue *value = dynamic_cast<StringFieldValue *>(fval.get()); - StringFieldValue::SpanTrees trees; - trees.emplace_back(std::move(_spanTree)); - value->setSpanTrees(trees, _repo); - sValue->setValue(f, *fval); - _spanList = nullptr; - _lastSpan = nullptr; - _spanStart = 0u; - _strSymbols = 0u; - _str.clear(); - _skipAutoSpace = true; - startAnnotate(); - _subField.clear(); -} - -DocBuilder::AttributeFieldHandle:: -AttributeFieldHandle(const document::Field &dfield, - const Schema::Field &sfield) - : CollectionFieldHandle(dfield, sfield) -{ -} - -void -DocBuilder::AttributeFieldHandle::addStr(const vespalib::string & val) -{ - if (_element) { - insertStr(_sfield, _element.get(), val); - } else { - insertStr(_sfield, _value.get(), val); - } -} - -void -DocBuilder::AttributeFieldHandle::addInt(int64_t val) -{ - if (_element) { - insertInt(_sfield, _element.get(), val); - } else { - insertInt(_sfield, _value.get(), val); - } -} - -void -DocBuilder::AttributeFieldHandle::addFloat(double val) -{ - if (_element) { - insertFloat(_sfield, _element.get(), val); - } else { - insertFloat(_sfield, _value.get(), val); - } -} - -void -DocBuilder::AttributeFieldHandle::addPredicate( - std::unique_ptr<vespalib::Slime> val) -{ - if (_element) { - insertPredicate(_sfield, _element.get(), std::move(val)); - } else { - insertPredicate(_sfield, _value.get(), std::move(val)); - } -} - -void -DocBuilder::AttributeFieldHandle::addTensor( - std::unique_ptr<vespalib::eval::Value> val) -{ - if (_element) { - insertTensor(_sfield, _element.get(), std::move(val)); - } else { - insertTensor(_sfield, _value.get(), std::move(val)); - } -} - -void -DocBuilder::AttributeFieldHandle::addPosition(int32_t xpos, int32_t ypos) -{ - if (_element) { - insertPosition(_sfield, _element.get(), xpos, ypos); - } else { - insertPosition(_sfield, _value.get(), xpos, ypos); - } -} - -DocBuilder::DocumentHandle::DocumentHandle(document::Document &doc, const vespalib::string & docId) - : _type(&doc.getType()), - _doc(&doc), - _fieldHandle(), - _repo(*_doc->getRepo(), *_type) -{ - (void) docId; -} - -DocBuilder::DocumentHandle::~DocumentHandle() = default; - -void -DocBuilder::DocumentHandle::startIndexField(const Schema::Field & sfield) { - _fieldHandle.reset(new IndexFieldHandle(_repo, _type->getField(sfield.getName()), sfield)); -} -void -DocBuilder::DocumentHandle::startAttributeField(const Schema::Field & sfield) { - _fieldHandle.reset(new AttributeFieldHandle(_type->getField(sfield.getName()), sfield)); -} - -void -DocBuilder::DocumentHandle::endField() { - _fieldHandle->onEndField(); - _doc->setValue(_type->getField(_fieldHandle->getField().getName()), *_fieldHandle->getValue()); - _fieldHandle.reset(); -} - -DocBuilder::DocBuilder(const Schema &schema) - : _schema(schema), - _doctypes_config(DocTypeBuilder(schema).makeConfig()), - _repo(std::make_shared<DocumentTypeRepo>(_doctypes_config)), - _docType(*_repo->getDocumentType("searchdocument")), - _doc(), - _handleDoc(), - _currDoc() -{ -} - -DocBuilder::~DocBuilder() = default; - -DocBuilder & -DocBuilder::startDocument(const vespalib::string & docId) -{ - _doc = std::make_unique<Document>(_docType, DocumentId(docId)); - _doc->setRepo(*_repo); - _handleDoc = std::make_shared<DocumentHandle>(*_doc, docId); - return *this; -} - -document::Document::UP -DocBuilder::endDocument() -{ - _handleDoc->endDocument(_doc); - return std::move(_doc); -} - -DocBuilder & -DocBuilder::startIndexField(const vespalib::string & name) -{ - assert(!_handleDoc->getFieldHandle()); - uint32_t field_id = _schema.getIndexFieldId(name); - assert(field_id != Schema::UNKNOWN_FIELD_ID); - _handleDoc->startIndexField(_schema.getIndexField(field_id)); - _currDoc = _handleDoc.get(); - return *this; -} - -DocBuilder & -DocBuilder::startAttributeField(const vespalib::string & name) -{ - assert(!_handleDoc->getFieldHandle()); - uint32_t field_id = _schema.getIndexFieldId(name); - assert(field_id == Schema::UNKNOWN_FIELD_ID); - field_id = _schema.getAttributeFieldId(name); - assert(field_id != Schema::UNKNOWN_FIELD_ID); - _handleDoc->startAttributeField(_schema.getAttributeField(field_id)); - _currDoc = _handleDoc.get(); - return *this; -} - -DocBuilder & -DocBuilder::endField() -{ - assert(_currDoc != nullptr); - _currDoc->endField(); - _currDoc = nullptr; - return *this; -} - -DocBuilder & -DocBuilder::startElement(int32_t weight) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->startElement(weight); - return *this; -} - -DocBuilder & -DocBuilder::endElement() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->endElement(); - return *this; -} - -DocBuilder & -DocBuilder::addStr(const vespalib::string & str) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addStr(str); - return *this; -} - -DocBuilder & -DocBuilder::addSpace() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addSpace(); - return *this; -} - -DocBuilder & -DocBuilder::addNoWordStr(const vespalib::string & str) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addNoWordStr(str); - return *this; -} - -DocBuilder & -DocBuilder::addTokenizedString(const vespalib::string &str) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addTokenizedString(str, false); - return *this; -} - -DocBuilder & -DocBuilder::addUrlTokenizedString(const vespalib::string &str) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addTokenizedString(str, true); - return *this; -} - -DocBuilder & -DocBuilder::addInt(int64_t val) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addInt(val); - return *this; -} - -DocBuilder & -DocBuilder::addFloat(double val) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addFloat(val); - return *this; -} - -DocBuilder & -DocBuilder::addPredicate(std::unique_ptr<vespalib::Slime> val) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addPredicate(std::move(val)); - return *this; -} - -DocBuilder & -DocBuilder::addTensor(std::unique_ptr<vespalib::eval::Value> val) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addTensor(std::move(val)); - return *this; -} - -DocBuilder & -DocBuilder::addSpan(size_t start, size_t len) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addSpan(start, len); - return *this; -} - -DocBuilder & -DocBuilder::addSpan() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addSpan(); - return *this; -} - -DocBuilder & -DocBuilder::addSpaceTokenAnnotation() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addSpaceTokenAnnotation(); - return *this; -} - -DocBuilder & -DocBuilder::addNumericTokenAnnotation() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addNumericTokenAnnotation(); - return *this; -} - -DocBuilder & -DocBuilder::addAlphabeticTokenAnnotation() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addAlphabeticTokenAnnotation(); - return *this; -} - -DocBuilder& -DocBuilder::addTermAnnotation() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addTermAnnotation(); - return *this; -} - -DocBuilder & -DocBuilder::addTermAnnotation(const vespalib::string &val) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addTermAnnotation(val); - return *this; -} - -DocBuilder & -DocBuilder::addPosition(int32_t xpos, int32_t ypos) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addPosition(xpos, ypos); - return *this; -} - -DocBuilder & -DocBuilder::addRaw(const void *buf, size_t len) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->addRaw(buf, len); - return *this; -} - -DocBuilder & -DocBuilder::startSubField(const vespalib::string &subField) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->startSubField(subField); - return *this; -} - -DocBuilder & -DocBuilder::endSubField() -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->endSubField(); - return *this; -} - -DocBuilder & -DocBuilder::setAutoAnnotate(bool autoAnnotate) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->setAutoAnnotate(autoAnnotate); - return *this; -} - -DocBuilder & -DocBuilder::setAutoSpace(bool autoSpace) -{ - assert(_currDoc != nullptr); - _currDoc->getFieldHandle()->setAutoSpace(autoSpace); - return *this; -} - -} diff --git a/searchlib/src/vespa/searchlib/index/docbuilder.h b/searchlib/src/vespa/searchlib/index/docbuilder.h deleted file mode 100644 index a8a37b57070..00000000000 --- a/searchlib/src/vespa/searchlib/index/docbuilder.h +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#pragma once - -#include "doctypebuilder.h" -#include <vespa/document/repo/fixedtyperepo.h> -#include <vespa/document/fieldvalue/fieldvalues.h> -#include <vespa/document/annotation/annotation.h> -#include <vespa/document/annotation/span.h> -#include <vespa/document/annotation/spanlist.h> -#include <vespa/document/annotation/spantree.h> -#include <vespa/vespalib/util/exception.h> -#include <vespa/vespalib/util/stringfmt.h> - -namespace vespalib::eval { struct Value; } - -namespace search::index { - -VESPA_DEFINE_EXCEPTION(DocBuilderError, vespalib::Exception); - -/** - * Builder class used to generate a search document that corresponds - * to an index schema. - **/ -class DocBuilder { -public: - typedef DocBuilderError Error; - -private: - /** - * Base class for handling the construction of a field. - **/ - class FieldHandle { - public: - typedef std::shared_ptr<FieldHandle> SP; - protected: - const Schema::Field & _sfield; - document::FieldValue::UP _value; - document::FieldValue::UP _element; - public: - FieldHandle(const document::Field & dfield, const Schema::Field & field); - virtual ~FieldHandle() {} - virtual void startElement(int32_t weight) { (void) weight; throw Error("Function not supported"); } - virtual void endElement() { throw Error("Function not supported"); } - virtual void addStr(const vespalib::string & val) { (void) val; throw Error("Function not supported"); } - - virtual void addSpace() { - throw Error("Function not supported"); - } - - virtual void addNoWordStr(const vespalib::string & val) { - (void) val; - throw Error("Function not supported"); - } - - virtual void addTokenizedString(const vespalib::string &val, bool urlMode) { - (void) val; - (void) urlMode; - throw Error("Function not supported"); - } - - virtual void addSpan(size_t start, size_t len) { - (void) start; - (void) len; - throw Error("Function not supported"); - } - - virtual void addSpan() { - throw Error("Function not supported"); - } - - virtual void addSpaceTokenAnnotation() { - throw Error("Function not supported"); - } - - virtual void addNumericTokenAnnotation() { - throw Error("Function not supported"); - } - - virtual void addAlphabeticTokenAnnotation() { - throw Error("Function not supported"); - } - - virtual void addTermAnnotation() { - throw Error("Function not supported"); - } - - virtual void addTermAnnotation(const vespalib::string &val) { - (void) val; - throw Error("Function not supported"); - } - - virtual void addInt(int64_t val) { (void) val; throw Error("Function not supported"); } - virtual void addFloat(double val) { (void) val; throw Error("Function not supported"); } - virtual void addPredicate(std::unique_ptr<vespalib::Slime>) { - throw Error("Function not supported"); - } - virtual void addTensor(std::unique_ptr<vespalib::eval::Value>) { - throw Error("Function not supported"); - } - const document::FieldValue::UP & getValue() const { return _value; } - const Schema::Field & getField() const { return _sfield; } - - virtual void onEndElement() {} - virtual void onEndField() {} - - virtual void setAutoAnnotate(bool autoAnnotate) { - (void) autoAnnotate; - throw Error("Function not supported"); - } - - virtual void setAutoSpace(bool autoSpace) { - (void) autoSpace; - throw Error("Function not supported"); - } - - virtual void addPosition(int32_t xpos, int32_t ypos) { - (void) xpos; - (void) ypos; - throw Error("Function not supported"); - } - - virtual void addRaw(const void *buf, size_t len) { - (void) buf; - (void) len; - throw Error("Function not supported"); - } - - virtual void startSubField(const vespalib::string &subField) { - (void) subField; - throw Error("Function not supported"); - } - - virtual void endSubField() { - throw Error("Function not supported"); - } - }; - - /** - * Class that can handle multi value fields. - **/ - class CollectionFieldHandle : public FieldHandle { - private: - int32_t _elementWeight; - public: - CollectionFieldHandle(const document::Field & dfield, const Schema::Field & sfield); - void startElement(int32_t weight) override; - void endElement() override; - }; - - /** - * Class for handling the construction of the content of an index field. - **/ - class IndexFieldHandle : public CollectionFieldHandle { - vespalib::string _str; // adjusted as word comes along - size_t _strSymbols; // symbols in string, assuming UTF8 - document::SpanList *_spanList; // owned by _spanTree - document::SpanTree::UP _spanTree; - const document::SpanNode *_lastSpan; - size_t _spanStart; // start of span - bool _autoAnnotate; // Add annotation when adding strings - bool _autoSpace; // Add space before strings - bool _skipAutoSpace; // one shot skip of adding space - bool _uriField; // URI handling (special struct case) - vespalib::string _subField; - const document::FixedTypeRepo & _repo; - - void append(const vespalib::string &val); - - public: - IndexFieldHandle(const document::FixedTypeRepo & repo, - const document::Field &dfield, - const Schema::Field &sfield); - - void addStr(const vespalib::string & val) override; - void addSpace() override; - void addNoWordStr(const vespalib::string & val) override; - void addTokenizedString(const vespalib::string &val, bool urlMode) override; - void addSpan(size_t start, size_t len) override; - void addSpan() override; - void addSpaceTokenAnnotation() override; - void addNumericTokenAnnotation() override; - void addAlphabeticTokenAnnotation() override; - void addTermAnnotation() override; - void addTermAnnotation(const vespalib::string &val) override; - void onEndElement() override; - void onEndField() override; - void startAnnotate(); - void setAutoAnnotate(bool autoAnnotate) override; - void setAutoSpace(bool autoSpace) override; - void startSubField(const vespalib::string &subField) override; - void endSubField() override; - }; - - /** - * Class for handling the construction of the content of an attribute field. - **/ - class AttributeFieldHandle : public CollectionFieldHandle { - public: - AttributeFieldHandle(const document::Field & dfield, const Schema::Field & sfield); - void addStr(const vespalib::string & val) override; - void addInt(int64_t val) override; - void addFloat(double val) override; - void addPredicate(std::unique_ptr<vespalib::Slime> val) override; - void addTensor(std::unique_ptr<vespalib::eval::Value> val) override; - void addPosition(int32_t xpos, int32_t ypos) override; - }; - - /** - * Class for handling the construction of a document (set of fields). - **/ - class DocumentHandle { - public: - typedef std::shared_ptr<DocumentHandle> SP; - private: - const document::DocumentType * _type; - document::Document *const _doc; - FieldHandle::SP _fieldHandle; - document::FixedTypeRepo _repo; - public: - DocumentHandle(document::Document &doc, const vespalib::string & docId); - ~DocumentHandle(); - const FieldHandle::SP & getFieldHandle() const { return _fieldHandle; } - void startIndexField(const Schema::Field & sfield); - void startAttributeField(const Schema::Field & sfield); - void endField(); - void endDocument(const document::Document::UP & doc) { - (void) doc; - } - }; - - const Schema & _schema; - document::config::DocumenttypesConfig _doctypes_config; - std::shared_ptr<const document::DocumentTypeRepo> _repo; - const document::DocumentType &_docType; - document::Document::UP _doc; // the document we are about to generate - - DocumentHandle::SP _handleDoc; // handle for all fields - DocumentHandle * _currDoc; // the current document handle - -public: - DocBuilder(const Schema & schema); - ~DocBuilder(); - - DocBuilder & startDocument(const vespalib::string & docId); - document::Document::UP endDocument(); - - DocBuilder & startIndexField(const vespalib::string & name); - DocBuilder & startAttributeField(const vespalib::string & name); - DocBuilder & endField(); - DocBuilder & startElement(int32_t weight = 1); - DocBuilder & endElement(); - DocBuilder & addStr(const vespalib::string & val); - DocBuilder & addSpace(); - DocBuilder & addNoWordStr(const vespalib::string & val); - DocBuilder & addInt(int64_t val); - DocBuilder & addFloat(double val); - DocBuilder & addPredicate(std::unique_ptr<vespalib::Slime> val); - DocBuilder & addTensor(std::unique_ptr<vespalib::eval::Value> val); - DocBuilder &addTokenizedString(const vespalib::string &val); - DocBuilder &addUrlTokenizedString(const vespalib::string &val); - DocBuilder &addSpan(size_t start, size_t len); - DocBuilder &addSpan(); - DocBuilder &addSpaceTokenAnnotation(); - DocBuilder &addNumericTokenAnnotation(); - DocBuilder &addAlphabeticTokenAnnotation(); - DocBuilder &addTermAnnotation(); - DocBuilder &addTermAnnotation(const vespalib::string &val); - DocBuilder &setAutoAnnotate(bool autoAnnotate); - DocBuilder &setAutoSpace(bool autoSpace); - DocBuilder &addPosition(int32_t xpos, int32_t ypos); - DocBuilder &addRaw(const void *buf, size_t len); - DocBuilder &startSubField(const vespalib::string &subField); - DocBuilder &endSubField(); - static bool hasAnnotations() { return true; } - - const document::DocumentType &getDocumentType() const { return _docType; } - const std::shared_ptr<const document::DocumentTypeRepo> &getDocumentTypeRepo() const { return _repo; } - document::config::DocumenttypesConfig getDocumenttypesConfig() const { return _doctypes_config; } -}; - -} diff --git a/searchlib/src/vespa/searchlib/index/doctypebuilder.cpp b/searchlib/src/vespa/searchlib/index/doctypebuilder.cpp deleted file mode 100644 index 5f655419471..00000000000 --- a/searchlib/src/vespa/searchlib/index/doctypebuilder.cpp +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include "doctypebuilder.h" -#include <vespa/document/datatype/urldatatype.h> -#include <vespa/document/datatype/tensor_data_type.h> -#include <vespa/document/datatype/documenttype.h> -#include <vespa/document/repo/configbuilder.h> -#include <set> - -using namespace document; - -namespace search::index { -namespace { - -DataType::Type convert(Schema::DataType type) { - switch (type) { - case schema::DataType::BOOL: - case schema::DataType::UINT2: - case schema::DataType::UINT4: - case schema::DataType::INT8: - return DataType::T_BYTE; - case schema::DataType::INT16: - return DataType::T_SHORT; - case schema::DataType::INT32: - return DataType::T_INT; - case schema::DataType::INT64: - return DataType::T_LONG; - case schema::DataType::FLOAT: - return DataType::T_FLOAT; - case schema::DataType::DOUBLE: - return DataType::T_DOUBLE; - case schema::DataType::STRING: - return DataType::T_STRING; - case schema::DataType::RAW: - return DataType::T_RAW; - case schema::DataType::BOOLEANTREE: - return DataType::T_PREDICATE; - case schema::DataType::TENSOR: - return DataType::T_TENSOR; - default: - break; - } - assert(!"Unknown datatype in schema"); - return DataType::MAX; -} - -void -insertStructType(document::config::DocumenttypesConfig::Documenttype & cfg, const StructDataType & structType) -{ - typedef document::config::DocumenttypesConfig DTC; - DTC::Documenttype::Datatype::Sstruct cfgStruct; - cfgStruct.name = structType.getName(); - Field::Set fieldSet = structType.getFieldSet(); - for (const Field * field : fieldSet) { - DTC::Documenttype::Datatype::Sstruct::Field sField; - sField.name = field->getName(); - sField.datatype = field->getDataType().getId(); - sField.id = field->getId(); - cfgStruct.field.push_back(sField); - } - cfg.datatype.push_back(DTC::Documenttype::Datatype()); - cfg.datatype.back().sstruct = cfgStruct; - cfg.datatype.back().id = structType.getId(); -} - -using namespace document::config_builder; - -TypeOrId makeCollection(TypeOrId datatype, Schema::CollectionType collection_type) { - switch (collection_type) { - case schema::CollectionType::ARRAY: - return Array(datatype); - case schema::CollectionType::WEIGHTEDSET: - // TODO: consider using array of struct<primitive,int32> to keep order - return Wset(datatype); - default: - return datatype; - } -} - -struct TypeCache { - std::map<std::pair<int, Schema::CollectionType>, TypeOrId> types; - - TypeOrId getType(TypeOrId datatype, Schema::CollectionType c_type) { - TypeOrId type = makeCollection(datatype, c_type); - std::pair<int, Schema::CollectionType> key = std::make_pair(datatype.id, c_type); - if (types.find(key) == types.end()) { - types.insert(std::make_pair(key, type)); - } - return types.find(key)->second; - } -}; - -} - -DocTypeBuilder::DocTypeBuilder(const Schema &schema) - : _schema(schema), - _iFields() -{ - _iFields.setup(schema); -} - -document::config::DocumenttypesConfig DocTypeBuilder::makeConfig() const { - using namespace document::config_builder; - TypeCache type_cache; - - typedef std::set<vespalib::string> UsedFields; - UsedFields usedFields; - - Struct header_struct("searchdocument.header"); - header_struct.setId(-1505212454); - - for (size_t i = 0; i < _iFields._textFields.size(); ++i) { - const Schema::IndexField &field = - _schema.getIndexField(_iFields._textFields[i]); - - // only handles string fields for now - assert(field.getDataType() == schema::DataType::STRING); - header_struct.addField(field.getName(), type_cache.getType( - DataType::T_STRING, field.getCollectionType())); - usedFields.insert(field.getName()); - } - - const int32_t uri_type = document::UrlDataType::getInstance().getId(); - for (size_t i = 0; i < _iFields._uriFields.size(); ++i) { - const Schema::IndexField &field = - _schema.getIndexField(_iFields._uriFields[i]._all); - - // only handles string fields for now - assert(field.getDataType() == schema::DataType::STRING); - header_struct.addField(field.getName(), type_cache.getType( - uri_type, field.getCollectionType())); - usedFields.insert(field.getName()); - } - - for (uint32_t i = 0; i < _schema.getNumAttributeFields(); ++i) { - const Schema::AttributeField &field = _schema.getAttributeField(i); - UsedFields::const_iterator usf = usedFields.find(field.getName()); - if (usf != usedFields.end()) { - continue; // taken as index field - } - auto type_id = convert(field.getDataType()); - if (type_id == DataType::T_TENSOR) { - header_struct.addTensorField(field.getName(), field.get_tensor_spec()); - } else { - header_struct.addField(field.getName(), type_cache.getType( - type_id, field.getCollectionType())); - } - usedFields.insert(field.getName()); - } - - DocumenttypesConfigBuilderHelper builder; - builder.document(-645763131, "searchdocument", - header_struct, Struct("searchdocument.body")); - return builder.config(); -} - -document::config::DocumenttypesConfig -DocTypeBuilder::makeConfig(const DocumentType &docType) -{ - typedef document::config::DocumenttypesConfigBuilder DTC; - DTC cfg; - { // document type - DTC::Documenttype dtype; - dtype.id = docType.getId(); - dtype.name = docType.getName(); - // TODO(vekterli): remove header/body config - dtype.headerstruct = docType.getFieldsType().getId(); - dtype.bodystruct = docType.getFieldsType().getId(); - cfg.documenttype.push_back(dtype); - } - insertStructType(cfg.documenttype[0], docType.getFieldsType()); - return cfg; -} - -} diff --git a/searchlib/src/vespa/searchlib/index/doctypebuilder.h b/searchlib/src/vespa/searchlib/index/doctypebuilder.h deleted file mode 100644 index 4db0ba5b0e3..00000000000 --- a/searchlib/src/vespa/searchlib/index/doctypebuilder.h +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#pragma once - -#include "schema_index_fields.h" -#include <vespa/document/config/config-documenttypes.h> -#include <vespa/document/fieldvalue/fieldvalues.h> -#include <vespa/vespalib/util/exception.h> -#include <vespa/vespalib/util/stringfmt.h> - -namespace search::index { - -/** - * Builder for the indexingdocument document type based on an index schema. - **/ -class DocTypeBuilder { - const Schema &_schema; - SchemaIndexFields _iFields; - -public: - DocTypeBuilder(const Schema & schema); - document::config::DocumenttypesConfig makeConfig() const; - - static document::config::DocumenttypesConfig - makeConfig(const document::DocumentType &docType); -}; - -} diff --git a/searchlib/src/vespa/searchlib/index/empty_doc_builder.cpp b/searchlib/src/vespa/searchlib/index/empty_doc_builder.cpp deleted file mode 100644 index 45588791926..00000000000 --- a/searchlib/src/vespa/searchlib/index/empty_doc_builder.cpp +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include "empty_doc_builder.h" -#include <vespa/document/datatype/documenttype.h> -#include <vespa/document/fieldvalue/document.h> -#include <vespa/document/repo/documenttyperepo.h> -#include <vespa/document/repo/configbuilder.h> -#include <cassert> - -using document::DataType; -using document::Document; -using document::DocumentId; -using document::DocumentTypeRepo; - -namespace search::index { - -namespace { - -DocumenttypesConfig -get_document_types_config(EmptyDocBuilder::AddFieldsType add_fields) -{ - using namespace document::config_builder; - DocumenttypesConfigBuilderHelper builder; - Struct header("searchdocument.header"); - add_fields(header); - builder.document(42, "searchdocument", - header, - Struct("searchdocument.body")); - return builder.config(); -} - -} - -EmptyDocBuilder::EmptyDocBuilder(AddFieldsType add_fields) - : _repo(std::make_shared<const DocumentTypeRepo>(get_document_types_config(add_fields))), - _document_type(_repo->getDocumentType("searchdocument")) -{ -} - -EmptyDocBuilder::~EmptyDocBuilder() = default; - - -std::unique_ptr<Document> -EmptyDocBuilder::make_document(vespalib::string document_id) -{ - auto doc = std::make_unique<Document>(get_document_type(), DocumentId(document_id)); - doc->setRepo(get_repo()); - return doc; -} - -const DataType& -EmptyDocBuilder::get_data_type(const vespalib::string &name) const -{ - const DataType *type = _repo->getDataType(*_document_type, name); - assert(type); - return *type; -} - -} diff --git a/searchlib/src/vespa/searchlib/memoryindex/feature_store.cpp b/searchlib/src/vespa/searchlib/memoryindex/feature_store.cpp index d2a28ebc04a..72f4a7ae579 100644 --- a/searchlib/src/vespa/searchlib/memoryindex/feature_store.cpp +++ b/searchlib/src/vespa/searchlib/memoryindex/feature_store.cpp @@ -2,6 +2,9 @@ #include "feature_store.h" #include <vespa/searchlib/index/schemautil.h> +#include <vespa/vespalib/datastore/compacting_buffers.h> +#include <vespa/vespalib/datastore/compaction_spec.h> +#include <vespa/vespalib/datastore/compaction_strategy.h> #include <vespa/vespalib/datastore/datastore.hpp> namespace search::memoryindex { @@ -9,6 +12,8 @@ namespace search::memoryindex { constexpr size_t MIN_BUFFER_ARRAYS = 1024u; using index::SchemaUtil; +using vespalib::datastore::CompactionSpec; +using vespalib::datastore::CompactionStrategy; using vespalib::datastore::EntryRef; uint64_t @@ -63,8 +68,6 @@ FeatureStore::moveFeatures(EntryRef ref, uint64_t bitLen) const uint8_t *src = getBits(ref); uint64_t byteLen = (bitLen + 7) / 8; EntryRef newRef = addFeatures(src, byteLen); - // Mark old features as dead - _store.incDead(ref, byteLen + Aligner::pad(byteLen)); return newRef; } @@ -112,7 +115,6 @@ FeatureStore::add_features_guard_bytes() uint32_t pad = Aligner::pad(len); auto result = _store.rawAllocator<uint8_t>(_typeId).alloc(len + pad); memset(result.data, 0, len + pad); - _store.incDead(result.ref, len + pad); } void @@ -143,4 +145,13 @@ FeatureStore::moveFeatures(uint32_t packedIndex, EntryRef ref) return moveFeatures(ref, bitLen); } +std::unique_ptr<vespalib::datastore::CompactingBuffers> +FeatureStore::start_compact() +{ + // Use a compaction strategy that will compact all active buffers + auto compaction_strategy = CompactionStrategy::make_compact_all_active_buffers_strategy(); + CompactionSpec compaction_spec(true, false); + return _store.start_compact_worst_buffers(compaction_spec, compaction_strategy); +} + } diff --git a/searchlib/src/vespa/searchlib/memoryindex/feature_store.h b/searchlib/src/vespa/searchlib/memoryindex/feature_store.h index 3ecb61f1cd1..5f5e782a382 100644 --- a/searchlib/src/vespa/searchlib/memoryindex/feature_store.h +++ b/searchlib/src/vespa/searchlib/memoryindex/feature_store.h @@ -205,13 +205,12 @@ public: const std::vector<PosOccFieldsParams> &getFieldsParams() const { return _fieldsParams; } - void trimHoldLists(generation_t usedGen) { _store.trimHoldLists(usedGen); } - void transferHoldLists(generation_t generation) { _store.transferHoldLists(generation); } - void clearHoldLists() { _store.clearHoldLists();} - std::vector<uint32_t> startCompact() { return _store.startCompact(_typeId); } - void finishCompact(const std::vector<uint32_t> & toHold) { _store.finishCompact(toHold); } + void reclaim_memory(generation_t oldest_used_gen) { _store.reclaim_memory(oldest_used_gen); } + void assign_generation(generation_t current_gen) { _store.assign_generation(current_gen); } + void reclaim_all_memory() { _store.reclaim_all_memory();} + std::unique_ptr<vespalib::datastore::CompactingBuffers> start_compact(); vespalib::MemoryUsage getMemoryUsage() const { return _store.getMemoryUsage(); } - vespalib::datastore::DataStoreBase::MemStats getMemStats() const { return _store.getMemStats(); } + vespalib::datastore::MemoryStats getMemStats() const { return _store.getMemStats(); } }; } diff --git a/searchlib/src/vespa/searchlib/memoryindex/field_index.cpp b/searchlib/src/vespa/searchlib/memoryindex/field_index.cpp index d731be7fe22..4be3031303e 100644 --- a/searchlib/src/vespa/searchlib/memoryindex/field_index.cpp +++ b/searchlib/src/vespa/searchlib/memoryindex/field_index.cpp @@ -69,12 +69,12 @@ FieldIndex<interleaved_features>::~FieldIndex() } _postingListStore.clearBuilder(); freeze(); // Flush all pending posting list tree freezes - transferHoldLists(); + assign_generation(); _dict.clear(); // Clear dictionary freeze(); // Flush pending freeze for dictionary tree. - transferHoldLists(); + assign_generation(); incGeneration(); - trimHoldLists(); + reclaim_memory(); } template <bool interleaved_features> @@ -103,9 +103,7 @@ template <bool interleaved_features> void FieldIndex<interleaved_features>::compactFeatures() { - std::vector<uint32_t> toHold; - - toHold = _featureStore.startCompact(); + auto compacting_buffers = _featureStore.start_compact(); auto itr = _dict.begin(); uint32_t packedIndex = _fieldId; for (; itr.valid(); ++itr) { @@ -143,9 +141,9 @@ FieldIndex<interleaved_features>::compactFeatures() } } using generation_t = GenerationHandler::generation_t; - _featureStore.finishCompact(toHold); + compacting_buffers->finish(); generation_t generation = _generationHandler.getCurrentGeneration(); - _featureStore.transferHoldLists(generation); + _featureStore.assign_generation(generation); } template <bool interleaved_features> diff --git a/searchlib/src/vespa/searchlib/memoryindex/field_index.h b/searchlib/src/vespa/searchlib/memoryindex/field_index.h index fb02ed880b4..187ec5ee971 100644 --- a/searchlib/src/vespa/searchlib/memoryindex/field_index.h +++ b/searchlib/src/vespa/searchlib/memoryindex/field_index.h @@ -52,20 +52,20 @@ private: _dict.getAllocator().freeze(); } - void trimHoldLists() { - GenerationHandler::generation_t usedGen = - _generationHandler.getFirstUsedGeneration(); - _postingListStore.trimHoldLists(usedGen); - _dict.getAllocator().trimHoldLists(usedGen); - _featureStore.trimHoldLists(usedGen); + void reclaim_memory() { + GenerationHandler::generation_t oldest_used_gen = + _generationHandler.get_oldest_used_generation(); + _postingListStore.reclaim_memory(oldest_used_gen); + _dict.getAllocator().reclaim_memory(oldest_used_gen); + _featureStore.reclaim_memory(oldest_used_gen); } - void transferHoldLists() { + void assign_generation() { GenerationHandler::generation_t generation = _generationHandler.getCurrentGeneration(); - _postingListStore.transferHoldLists(generation); - _dict.getAllocator().transferHoldLists(generation); - _featureStore.transferHoldLists(generation); + _postingListStore.assign_generation(generation); + _dict.getAllocator().assign_generation(generation); + _featureStore.assign_generation(generation); } void incGeneration() { @@ -90,9 +90,9 @@ public: void commit() override { _remover.flush(); freeze(); - transferHoldLists(); + assign_generation(); incGeneration(); - trimHoldLists(); + reclaim_memory(); } /** diff --git a/searchlib/src/vespa/searchlib/predicate/predicate_index.cpp b/searchlib/src/vespa/searchlib/predicate/predicate_index.cpp index c64c490039b..f21ca1b11cc 100644 --- a/searchlib/src/vespa/searchlib/predicate/predicate_index.cpp +++ b/searchlib/src/vespa/searchlib/predicate/predicate_index.cpp @@ -213,19 +213,19 @@ PredicateIndex::commit() { } void -PredicateIndex::trimHoldLists(generation_t used_generation) { - _interval_index.trimHoldLists(used_generation); - _bounds_index.trimHoldLists(used_generation); - _interval_store.trimHoldLists(used_generation); - _zero_constraint_docs.getAllocator().trimHoldLists(used_generation); +PredicateIndex::reclaim_memory(generation_t oldest_used_gen) { + _interval_index.reclaim_memory(oldest_used_gen); + _bounds_index.reclaim_memory(oldest_used_gen); + _interval_store.reclaim_memory(oldest_used_gen); + _zero_constraint_docs.getAllocator().reclaim_memory(oldest_used_gen); } void -PredicateIndex::transferHoldLists(generation_t generation) { - _interval_index.transferHoldLists(generation); - _bounds_index.transferHoldLists(generation); - _interval_store.transferHoldLists(generation); - _zero_constraint_docs.getAllocator().transferHoldLists(generation); +PredicateIndex::assign_generation(generation_t current_gen) { + _interval_index.assign_generation(current_gen); + _bounds_index.assign_generation(current_gen); + _interval_store.assign_generation(current_gen); + _zero_constraint_docs.getAllocator().assign_generation(current_gen); } vespalib::MemoryUsage diff --git a/searchlib/src/vespa/searchlib/predicate/predicate_index.h b/searchlib/src/vespa/searchlib/predicate/predicate_index.h index 1bad95c6aa9..238314e17f9 100644 --- a/searchlib/src/vespa/searchlib/predicate/predicate_index.h +++ b/searchlib/src/vespa/searchlib/predicate/predicate_index.h @@ -73,8 +73,8 @@ public: void indexDocument(uint32_t doc_id, const PredicateTreeAnnotations &annotations); void removeDocument(uint32_t doc_id); void commit(); - void trimHoldLists(generation_t used_generation); - void transferHoldLists(generation_t generation); + void reclaim_memory(generation_t oldest_used_gen); + void assign_generation(generation_t current_gen); vespalib::MemoryUsage getMemoryUsage() const; int getArity() const { return _arity; } diff --git a/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.cpp b/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.cpp index 379c859f6c3..af809b2fa69 100644 --- a/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.cpp +++ b/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.cpp @@ -100,13 +100,13 @@ PredicateIntervalStore::remove(EntryRef ref) { } void -PredicateIntervalStore::trimHoldLists(generation_t used_generation) { - _store.trimHoldLists(used_generation); +PredicateIntervalStore::reclaim_memory(generation_t oldest_used_gen) { + _store.reclaim_memory(oldest_used_gen); } void -PredicateIntervalStore::transferHoldLists(generation_t generation) { - _store.transferHoldLists(generation); +PredicateIntervalStore::assign_generation(generation_t current_gen) { + _store.assign_generation(current_gen); } } diff --git a/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.h b/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.h index 0b3e32ec6b7..a96c208393d 100644 --- a/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.h +++ b/searchlib/src/vespa/searchlib/predicate/predicate_interval_store.h @@ -71,9 +71,9 @@ public: */ void remove(vespalib::datastore::EntryRef ref); - void trimHoldLists(generation_t used_generation); + void reclaim_memory(generation_t oldest_used_gen); - void transferHoldLists(generation_t generation); + void assign_generation(generation_t current_gen); /** * Return memory usage (only the data store is included) diff --git a/searchlib/src/vespa/searchlib/predicate/simple_index.h b/searchlib/src/vespa/searchlib/predicate/simple_index.h index 78805820a30..d49e42a1e35 100644 --- a/searchlib/src/vespa/searchlib/predicate/simple_index.h +++ b/searchlib/src/vespa/searchlib/predicate/simple_index.h @@ -187,8 +187,8 @@ public: // (and after doc id limits values are determined) to promote posting lists to vectors. void promoteOverThresholdVectors(); void commit(); - void trimHoldLists(generation_t used_generation); - void transferHoldLists(generation_t generation); + void reclaim_memory(generation_t oldest_used_gen); + void assign_generation(generation_t current_gen); vespalib::MemoryUsage getMemoryUsage() const; template <typename FunctionType> void foreach_frozen_key(vespalib::datastore::EntryRef ref, Key key, FunctionType func) const; diff --git a/searchlib/src/vespa/searchlib/predicate/simple_index.hpp b/searchlib/src/vespa/searchlib/predicate/simple_index.hpp index cb37fec26ea..c6f640d72ed 100644 --- a/searchlib/src/vespa/searchlib/predicate/simple_index.hpp +++ b/searchlib/src/vespa/searchlib/predicate/simple_index.hpp @@ -54,17 +54,17 @@ SimpleIndex<Posting, Key, DocId>::~SimpleIndex() { _vector_posting_lists.disableElemHoldList(); _vector_posting_lists.clear(); _vector_posting_lists.getAllocator().freeze(); - _vector_posting_lists.getAllocator().clearHoldLists(); + _vector_posting_lists.getAllocator().reclaim_all_memory(); _dictionary.disableFreeLists(); _dictionary.disableElemHoldList(); _dictionary.clear(); _dictionary.getAllocator().freeze(); - _dictionary.getAllocator().clearHoldLists(); + _dictionary.getAllocator().reclaim_all_memory(); _btree_posting_lists.clearBuilder(); _btree_posting_lists.freeze(); - _btree_posting_lists.clearHoldLists(); + _btree_posting_lists.reclaim_all_memory(); } template <typename Posting, typename Key, typename DocId> @@ -291,19 +291,19 @@ SimpleIndex<Posting, Key, DocId>::commit() { template <typename Posting, typename Key, typename DocId> void -SimpleIndex<Posting, Key, DocId>::trimHoldLists(generation_t used_generation) { - _btree_posting_lists.trimHoldLists(used_generation); - _dictionary.getAllocator().trimHoldLists(used_generation); - _vector_posting_lists.getAllocator().trimHoldLists(used_generation); +SimpleIndex<Posting, Key, DocId>::reclaim_memory(generation_t oldest_used_gen) { + _btree_posting_lists.reclaim_memory(oldest_used_gen); + _dictionary.getAllocator().reclaim_memory(oldest_used_gen); + _vector_posting_lists.getAllocator().reclaim_memory(oldest_used_gen); } template <typename Posting, typename Key, typename DocId> void -SimpleIndex<Posting, Key, DocId>::transferHoldLists(generation_t generation) { - _dictionary.getAllocator().transferHoldLists(generation); - _btree_posting_lists.transferHoldLists(generation); - _vector_posting_lists.getAllocator().transferHoldLists(generation); +SimpleIndex<Posting, Key, DocId>::assign_generation(generation_t current_gen) { + _dictionary.getAllocator().assign_generation(current_gen); + _btree_posting_lists.assign_generation(current_gen); + _vector_posting_lists.getAllocator().assign_generation(current_gen); } template <typename Posting, typename Key, typename DocId> diff --git a/searchlib/src/vespa/searchlib/tensor/CMakeLists.txt b/searchlib/src/vespa/searchlib/tensor/CMakeLists.txt index 46bfc0909aa..0d87881711c 100644 --- a/searchlib/src/vespa/searchlib/tensor/CMakeLists.txt +++ b/searchlib/src/vespa/searchlib/tensor/CMakeLists.txt @@ -9,7 +9,6 @@ vespa_add_library(searchlib_tensor OBJECT dense_tensor_attribute_saver.cpp dense_tensor_store.cpp direct_tensor_attribute.cpp - direct_tensor_saver.cpp direct_tensor_store.cpp distance_calculator.cpp distance_function_factory.cpp @@ -29,14 +28,13 @@ vespa_add_library(searchlib_tensor OBJECT nearest_neighbor_index_saver.cpp serialized_fast_value_attribute.cpp small_subspaces_buffer_type.cpp - streamed_value_saver.cpp - streamed_value_store.cpp tensor_attribute.cpp tensor_buffer_operations.cpp tensor_buffer_store.cpp tensor_buffer_type_mapper.cpp tensor_deserialize.cpp tensor_store.cpp + tensor_store_saver.cpp reusable_set_visited_tracker.cpp DEPENDS ) diff --git a/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.cpp b/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.cpp index d4353532309..56b9473b6e6 100644 --- a/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.cpp +++ b/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.cpp @@ -112,7 +112,7 @@ void DenseTensorAttribute::internal_set_tensor(DocId docid, const vespalib::eval::Value& tensor) { consider_remove_from_index(docid); - EntryRef ref = _denseTensorStore.setTensor(tensor); + EntryRef ref = _denseTensorStore.store_tensor(tensor); setTensorRef(docid, ref); } @@ -172,8 +172,8 @@ DenseTensorAttribute::DenseTensorAttribute(vespalib::stringref baseFileName, con DenseTensorAttribute::~DenseTensorAttribute() { - getGenerationHolder().clearHoldLists(); - _tensorStore.clearHoldLists(); + getGenerationHolder().reclaim_all(); + _tensorStore.reclaim_all_memory(); } uint32_t @@ -229,10 +229,7 @@ DenseTensorAttribute::getTensor(DocId docId) const if (docId < getCommittedDocIdLimit()) { ref = acquire_entry_ref(docId); } - if (!ref.valid()) { - return {}; - } - return _denseTensorStore.getTensor(ref); + return _denseTensorStore.get_tensor(ref); } vespalib::eval::TypedCells @@ -453,22 +450,20 @@ DenseTensorAttribute::onCommit() } void -DenseTensorAttribute::onGenerationChange(generation_t next_gen) +DenseTensorAttribute::before_inc_generation(generation_t current_gen) { - // TODO: Change onGenerationChange() to send current generation instead of next generation. - // This applies for entire attribute vector code. - TensorAttribute::onGenerationChange(next_gen); + TensorAttribute::before_inc_generation(current_gen); if (_index) { - _index->transfer_hold_lists(next_gen - 1); + _index->assign_generation(current_gen); } } void -DenseTensorAttribute::removeOldGenerations(generation_t first_used_gen) +DenseTensorAttribute::reclaim_memory(generation_t oldest_used_gen) { - TensorAttribute::removeOldGenerations(first_used_gen); + TensorAttribute::reclaim_memory(oldest_used_gen); if (_index) { - _index->trim_hold_lists(first_used_gen); + _index->reclaim_memory(oldest_used_gen); } } diff --git a/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.h b/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.h index b0991aa57aa..3aa52fe622a 100644 --- a/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.h +++ b/searchlib/src/vespa/searchlib/tensor/dense_tensor_attribute.h @@ -47,8 +47,8 @@ public: std::unique_ptr<AttributeSaver> onInitSave(vespalib::stringref fileName) override; uint32_t getVersion() const override; void onCommit() override; - void onGenerationChange(generation_t next_gen) override; - void removeOldGenerations(generation_t first_used_gen) override; + void before_inc_generation(generation_t current_gen) override; + void reclaim_memory(generation_t oldest_used_gen) override; void get_state(const vespalib::slime::Inserter& inserter) const override; void onShrinkLidSpace() override; diff --git a/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.cpp b/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.cpp index 1bc84a7216d..60a3546578a 100644 --- a/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.cpp +++ b/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.cpp @@ -11,6 +11,7 @@ using vespalib::datastore::CompactionContext; using vespalib::datastore::CompactionSpec; using vespalib::datastore::CompactionStrategy; +using vespalib::datastore::EntryRef; using vespalib::datastore::Handle; using vespalib::datastore::ICompactionContext; using vespalib::eval::CellType; @@ -120,7 +121,7 @@ DenseTensorStore::holdTensor(EntryRef ref) } TensorStore::EntryRef -DenseTensorStore::move(EntryRef ref) +DenseTensorStore::move_on_compact(EntryRef ref) { if (!ref.valid()) { return RefType(); @@ -128,7 +129,6 @@ DenseTensorStore::move(EntryRef ref) auto oldraw = getRawBuffer(ref); auto newraw = allocRawBuffer(); memcpy(newraw.data, static_cast<const char *>(oldraw), getBufSize()); - _concreteStore.holdElem(ref, _tensorSizeCalc.alignedSize()); return newraw.ref; } @@ -147,19 +147,8 @@ DenseTensorStore::start_compact(const CompactionStrategy& compaction_strategy) return std::make_unique<CompactionContext>(*this, std::move(compacting_buffers)); } -std::unique_ptr<Value> -DenseTensorStore::getTensor(EntryRef ref) const -{ - if (!ref.valid()) { - return {}; - } - vespalib::eval::TypedCells cells_ref(getRawBuffer(ref), _type.cell_type(), getNumCells()); - return std::make_unique<vespalib::eval::DenseValueView>(_type, cells_ref); -} - -template <class TensorType> -TensorStore::EntryRef -DenseTensorStore::setDenseTensor(const TensorType &tensor) +EntryRef +DenseTensorStore::store_tensor(const Value& tensor) { assert(tensor.type() == _type); auto cells = tensor.cells(); @@ -170,10 +159,29 @@ DenseTensorStore::setDenseTensor(const TensorType &tensor) return raw.ref; } -TensorStore::EntryRef -DenseTensorStore::setTensor(const vespalib::eval::Value &tensor) +EntryRef +DenseTensorStore::store_encoded_tensor(vespalib::nbostream& encoded) +{ + (void) encoded; + abort(); +} + +std::unique_ptr<Value> +DenseTensorStore::get_tensor(EntryRef ref) const +{ + if (!ref.valid()) { + return {}; + } + vespalib::eval::TypedCells cells_ref(getRawBuffer(ref), _type.cell_type(), getNumCells()); + return std::make_unique<vespalib::eval::DenseValueView>(_type, cells_ref); +} + +bool +DenseTensorStore::encode_stored_tensor(EntryRef ref, vespalib::nbostream& target) const { - return setDenseTensor(tensor); + (void) ref; + (void) target; + abort(); } } diff --git a/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.h b/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.h index bd83772ee55..298e58ee410 100644 --- a/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.h +++ b/searchlib/src/vespa/searchlib/tensor/dense_tensor_store.h @@ -51,10 +51,6 @@ private: BufferType _bufferType; ValueType _type; // type of dense tensor std::vector<char> _emptySpace; - - template <class TensorType> - TensorStore::EntryRef - setDenseTensor(const TensorType &tensor); public: DenseTensorStore(const ValueType &type, std::shared_ptr<vespalib::alloc::MemoryAllocator> allocator); ~DenseTensorStore() override; @@ -67,15 +63,18 @@ public: } vespalib::datastore::Handle<char> allocRawBuffer(); void holdTensor(EntryRef ref) override; - EntryRef move(EntryRef ref) override; + EntryRef move_on_compact(EntryRef ref) override; vespalib::MemoryUsage update_stat(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; std::unique_ptr<vespalib::datastore::ICompactionContext> start_compact(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; - std::unique_ptr<vespalib::eval::Value> getTensor(EntryRef ref) const; + EntryRef store_tensor(const vespalib::eval::Value &tensor) override; + EntryRef store_encoded_tensor(vespalib::nbostream &encoded) override; + std::unique_ptr<vespalib::eval::Value> get_tensor(EntryRef ref) const override; + bool encode_stored_tensor(EntryRef ref, vespalib::nbostream &target) const override; + vespalib::eval::TypedCells get_typed_cells(EntryRef ref) const { return vespalib::eval::TypedCells(ref.valid() ? getRawBuffer(ref) : &_emptySpace[0], _type.cell_type(), getNumCells()); } - EntryRef setTensor(const vespalib::eval::Value &tensor); // The following method is meant to be used only for unit tests. uint32_t getArraySize() const { return _bufferType.getArraySize(); } }; diff --git a/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.cpp b/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.cpp index e2271e63425..22db5dc5b47 100644 --- a/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.cpp +++ b/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.cpp @@ -1,16 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "direct_tensor_attribute.h" -#include "direct_tensor_saver.h" - #include <vespa/eval/eval/fast_value.h> #include <vespa/eval/eval/value.h> -#include <vespa/searchlib/attribute/readerbase.h> -#include <vespa/searchlib/util/fileutil.h> -#include <vespa/vespalib/util/array.h> - -#include "blob_sequence_reader.h" -#include "tensor_deserialize.h" using vespalib::eval::FastValueBuilderFactory; @@ -23,41 +15,8 @@ DirectTensorAttribute::DirectTensorAttribute(stringref name, const Config &cfg) DirectTensorAttribute::~DirectTensorAttribute() { - getGenerationHolder().clearHoldLists(); - _tensorStore.clearHoldLists(); -} - -bool -DirectTensorAttribute::onLoad(vespalib::Executor *) -{ - BlobSequenceReader tensorReader(*this); - if (!tensorReader.hasData()) { - return false; - } - setCreateSerialNum(tensorReader.getCreateSerialNum()); - assert(tensorReader.getVersion() == getVersion()); - uint32_t numDocs = tensorReader.getDocIdLimit(); - _refVector.reset(); - _refVector.unsafe_reserve(numDocs); - vespalib::Array<char> buffer(1024); - for (uint32_t lid = 0; lid < numDocs; ++lid) { - uint32_t tensorSize = tensorReader.getNextSize(); - if (tensorSize != 0) { - if (tensorSize > buffer.size()) { - buffer.resize(tensorSize + 1024); - } - tensorReader.readBlob(&buffer[0], tensorSize); - auto tensor = deserialize_tensor(&buffer[0], tensorSize); - EntryRef ref = _direct_store.store_tensor(std::move(tensor)); - _refVector.push_back(AtomicEntryRef(ref)); - } else { - EntryRef invalid; - _refVector.push_back(AtomicEntryRef(invalid)); - } - } - setNumDocs(numDocs); - setCommittedDocIdLimit(numDocs); - return true; + getGenerationHolder().reclaim_all(); + _tensorStore.reclaim_all_memory(); } void @@ -84,7 +43,7 @@ DirectTensorAttribute::update_tensor(DocId docId, ref = _refVector[docId].load_relaxed(); } if (ref.valid()) { - auto ptr = _direct_store.get_tensor(ref); + auto ptr = _direct_store.get_tensor_ptr(ref); if (ptr) { auto new_value = update.apply_to(*ptr, FastValueBuilderFactory::get()); if (new_value) { @@ -109,7 +68,7 @@ DirectTensorAttribute::getTensor(DocId docId) const ref = acquire_entry_ref(docId); } if (ref.valid()) { - auto ptr = _direct_store.get_tensor(ref); + auto ptr = _direct_store.get_tensor_ptr(ref); if (ptr) { return FastValueBuilderFactory::get().copy(*ptr); } @@ -123,21 +82,10 @@ DirectTensorAttribute::get_tensor_ref(DocId docId) const { if (docId >= getCommittedDocIdLimit()) { return *_emptyTensor; } - auto ptr = _direct_store.get_tensor(acquire_entry_ref(docId)); + auto ptr = _direct_store.get_tensor_ptr(acquire_entry_ref(docId)); if ( ptr == nullptr) { return *_emptyTensor; } return *ptr; } -std::unique_ptr<AttributeSaver> -DirectTensorAttribute::onInitSave(vespalib::stringref fileName) -{ - vespalib::GenerationHandler::Guard guard(getGenerationHandler().takeGuard()); - return std::make_unique<DirectTensorAttributeSaver> - (std::move(guard), - this->createAttributeHeader(fileName), - getRefCopy(), - _direct_store); -} - } // namespace diff --git a/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.h b/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.h index 2dfb5c1efcd..6466c6f7537 100644 --- a/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.h +++ b/searchlib/src/vespa/searchlib/tensor/direct_tensor_attribute.h @@ -21,8 +21,6 @@ public: const document::TensorUpdate &update, bool create_empty_if_non_existing) override; std::unique_ptr<vespalib::eval::Value> getTensor(DocId docId) const override; - bool onLoad(vespalib::Executor *executor) override; - std::unique_ptr<AttributeSaver> onInitSave(vespalib::stringref fileName) override; void set_tensor(DocId docId, std::unique_ptr<vespalib::eval::Value> tensor); const vespalib::eval::Value &get_tensor_ref(DocId docId) const override; bool supports_get_tensor_ref() const override { return true; } diff --git a/searchlib/src/vespa/searchlib/tensor/direct_tensor_saver.cpp b/searchlib/src/vespa/searchlib/tensor/direct_tensor_saver.cpp deleted file mode 100644 index 024b2fe5467..00000000000 --- a/searchlib/src/vespa/searchlib/tensor/direct_tensor_saver.cpp +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include "direct_tensor_saver.h" -#include "direct_tensor_store.h" - -#include <vespa/eval/eval/value_codec.h> -#include <vespa/searchlib/attribute/iattributesavetarget.h> -#include <vespa/searchlib/util/bufferwriter.h> -#include <vespa/vespalib/objects/nbostream.h> - -using vespalib::GenerationHandler; - -namespace search::tensor { - -DirectTensorAttributeSaver:: -DirectTensorAttributeSaver(GenerationHandler::Guard &&guard, - const attribute::AttributeHeader &header, - RefCopyVector &&refs, - const DirectTensorStore &tensorStore) - : AttributeSaver(std::move(guard), header), - _refs(std::move(refs)), - _tensorStore(tensorStore) -{ -} - - -DirectTensorAttributeSaver::~DirectTensorAttributeSaver() -{ -} - -bool -DirectTensorAttributeSaver::onSave(IAttributeSaveTarget &saveTarget) -{ - auto datWriter = saveTarget.datWriter().allocBufferWriter(); - const uint32_t docIdLimit(_refs.size()); - vespalib::nbostream stream; - for (uint32_t lid = 0; lid < docIdLimit; ++lid) { - const vespalib::eval::Value *tensor = _tensorStore.get_tensor(_refs[lid]); - if (tensor) { - stream.clear(); - encode_value(*tensor, stream); - uint32_t sz = stream.size(); - datWriter->write(&sz, sizeof(sz)); - datWriter->write(stream.peek(), stream.size()); - } else { - uint32_t sz = 0; - datWriter->write(&sz, sizeof(sz)); - } - } - datWriter->flush(); - return true; -} - -} // namespace search::tensor diff --git a/searchlib/src/vespa/searchlib/tensor/direct_tensor_saver.h b/searchlib/src/vespa/searchlib/tensor/direct_tensor_saver.h deleted file mode 100644 index 132e1570f0f..00000000000 --- a/searchlib/src/vespa/searchlib/tensor/direct_tensor_saver.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#pragma once - -#include <vespa/searchlib/attribute/attributesaver.h> -#include "tensor_attribute.h" - -namespace search::tensor { - -class DirectTensorStore; - -/* - * Class for saving a tensor attribute. - */ -class DirectTensorAttributeSaver : public AttributeSaver -{ -public: - using RefCopyVector = TensorAttribute::RefCopyVector; -private: - using GenerationHandler = vespalib::GenerationHandler; - - RefCopyVector _refs; - const DirectTensorStore &_tensorStore; - - bool onSave(IAttributeSaveTarget &saveTarget) override; -public: - DirectTensorAttributeSaver(GenerationHandler::Guard &&guard, - const attribute::AttributeHeader &header, - RefCopyVector &&refs, - const DirectTensorStore &tensorStore); - - virtual ~DirectTensorAttributeSaver(); -}; - -} // namespace search::tensor diff --git a/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.cpp b/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.cpp index fa3b1486c84..013e7dedeba 100644 --- a/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.cpp +++ b/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.cpp @@ -1,7 +1,10 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "direct_tensor_store.h" +#include "tensor_deserialize.h" +#include <vespa/eval/eval/fast_value.h> #include <vespa/eval/eval/value.h> +#include <vespa/eval/eval/value_codec.h> #include <vespa/vespalib/datastore/compacting_buffers.h> #include <vespa/vespalib/datastore/compaction_context.h> #include <vespa/vespalib/datastore/compaction_strategy.h> @@ -14,6 +17,8 @@ using vespalib::datastore::CompactionSpec; using vespalib::datastore::CompactionStrategy; using vespalib::datastore::EntryRef; using vespalib::datastore::ICompactionContext; +using vespalib::eval::FastValueBuilderFactory; +using vespalib::eval::Value; namespace search::tensor { @@ -41,7 +46,7 @@ DirectTensorStore::add_entry(TensorSP tensor) { auto ref = _tensor_store.addEntry(tensor); auto& state = _tensor_store.getBufferState(RefType(ref).bufferId()); - state.incExtraUsedBytes(tensor->get_memory_usage().allocatedBytes()); + state.stats().inc_extra_used_bytes(tensor->get_memory_usage().allocatedBytes()); return ref; } @@ -54,13 +59,6 @@ DirectTensorStore::DirectTensorStore() DirectTensorStore::~DirectTensorStore() = default; -EntryRef -DirectTensorStore::store_tensor(std::unique_ptr<vespalib::eval::Value> tensor) -{ - assert(tensor); - return add_entry(TensorSP(std::move(tensor))); -} - void DirectTensorStore::holdTensor(EntryRef ref) { @@ -73,16 +71,14 @@ DirectTensorStore::holdTensor(EntryRef ref) } EntryRef -DirectTensorStore::move(EntryRef ref) +DirectTensorStore::move_on_compact(EntryRef ref) { if (!ref.valid()) { return EntryRef(); } const auto& old_tensor = _tensor_store.getEntry(ref); assert(old_tensor); - auto new_ref = add_entry(old_tensor); - _tensor_store.holdElem(ref, 1, old_tensor->get_memory_usage().allocatedBytes()); - return new_ref; + return add_entry(old_tensor); } vespalib::MemoryUsage @@ -100,4 +96,42 @@ DirectTensorStore::start_compact(const CompactionStrategy& compaction_strategy) return std::make_unique<CompactionContext>(*this, std::move(compacting_buffers)); } +EntryRef +DirectTensorStore::store_tensor(std::unique_ptr<Value> tensor) +{ + assert(tensor); + return add_entry(std::move(tensor)); +} + +EntryRef +DirectTensorStore::store_tensor(const Value& tensor) +{ + return add_entry(FastValueBuilderFactory::get().copy(tensor)); +} + +EntryRef +DirectTensorStore::store_encoded_tensor(vespalib::nbostream& encoded) +{ + return add_entry(deserialize_tensor(encoded)); +} + +std::unique_ptr<Value> +DirectTensorStore::get_tensor(EntryRef ref) const +{ + if (!ref.valid()) { + return {}; + } + return FastValueBuilderFactory::get().copy(*_tensor_store.getEntry(ref)); +} + +bool +DirectTensorStore::encode_stored_tensor(EntryRef ref, vespalib::nbostream& target) const +{ + if (!ref.valid()) { + return false; + } + vespalib::eval::encode_value(*_tensor_store.getEntry(ref), target); + return true; +} + } diff --git a/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.h b/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.h index 57e7453ff99..c55dda5646a 100644 --- a/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.h +++ b/searchlib/src/vespa/searchlib/tensor/direct_tensor_store.h @@ -40,7 +40,7 @@ public: ~DirectTensorStore() override; using RefType = TensorStoreType::RefType; - const vespalib::eval::Value * get_tensor(EntryRef ref) const { + const vespalib::eval::Value * get_tensor_ptr(EntryRef ref) const { if (!ref.valid()) { return nullptr; } @@ -49,9 +49,13 @@ public: EntryRef store_tensor(std::unique_ptr<vespalib::eval::Value> tensor); void holdTensor(EntryRef ref) override; - EntryRef move(EntryRef ref) override; + EntryRef move_on_compact(EntryRef ref) override; vespalib::MemoryUsage update_stat(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; std::unique_ptr<vespalib::datastore::ICompactionContext> start_compact(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; + EntryRef store_tensor(const vespalib::eval::Value& tensor) override; + EntryRef store_encoded_tensor(vespalib::nbostream& encoded) override; + std::unique_ptr<vespalib::eval::Value> get_tensor(EntryRef ref) const override; + bool encode_stored_tensor(EntryRef ref, vespalib::nbostream& target) const override; }; } diff --git a/searchlib/src/vespa/searchlib/tensor/hnsw_index.cpp b/searchlib/src/vespa/searchlib/tensor/hnsw_index.cpp index d23bbcfbed4..10f06a1e1ec 100644 --- a/searchlib/src/vespa/searchlib/tensor/hnsw_index.cpp +++ b/searchlib/src/vespa/searchlib/tensor/hnsw_index.cpp @@ -511,21 +511,21 @@ HnswIndex::remove_document(uint32_t docid) } void -HnswIndex::transfer_hold_lists(generation_t current_gen) +HnswIndex::assign_generation(generation_t current_gen) { // Note: RcuVector transfers hold lists as part of reallocation based on current generation. // We need to set the next generation here, as it is incremented on a higher level right after this call. _graph.node_refs.setGeneration(current_gen + 1); - _graph.nodes.transferHoldLists(current_gen); - _graph.links.transferHoldLists(current_gen); + _graph.nodes.assign_generation(current_gen); + _graph.links.assign_generation(current_gen); } void -HnswIndex::trim_hold_lists(generation_t first_used_gen) +HnswIndex::reclaim_memory(generation_t oldest_used_gen) { - _graph.node_refs.removeOldGenerations(first_used_gen); - _graph.nodes.trimHoldLists(first_used_gen); - _graph.links.trimHoldLists(first_used_gen); + _graph.node_refs.reclaim_memory(oldest_used_gen); + _graph.nodes.reclaim_memory(oldest_used_gen); + _graph.links.reclaim_memory(oldest_used_gen); } void diff --git a/searchlib/src/vespa/searchlib/tensor/hnsw_index.h b/searchlib/src/vespa/searchlib/tensor/hnsw_index.h index e3ffada1fc2..8a7422907ea 100644 --- a/searchlib/src/vespa/searchlib/tensor/hnsw_index.h +++ b/searchlib/src/vespa/searchlib/tensor/hnsw_index.h @@ -187,8 +187,8 @@ public: vespalib::GenerationHandler::Guard read_guard) const override; void complete_add_document(uint32_t docid, std::unique_ptr<PrepareResult> prepare_result) override; void remove_document(uint32_t docid) override; - void transfer_hold_lists(generation_t current_gen) override; - void trim_hold_lists(generation_t first_used_gen) override; + void assign_generation(generation_t current_gen) override; + void reclaim_memory(generation_t oldest_used_gen) override; void compact_level_arrays(CompactionSpec compaction_spec, const CompactionStrategy& compaction_strategy); void compact_link_arrays(CompactionSpec compaction_spec, const CompactionStrategy& compaction_strategy); bool consider_compact_level_arrays(const CompactionStrategy& compaction_strategy); diff --git a/searchlib/src/vespa/searchlib/tensor/large_subspaces_buffer_type.cpp b/searchlib/src/vespa/searchlib/tensor/large_subspaces_buffer_type.cpp index cdd4d35c1df..cb074348f08 100644 --- a/searchlib/src/vespa/searchlib/tensor/large_subspaces_buffer_type.cpp +++ b/searchlib/src/vespa/searchlib/tensor/large_subspaces_buffer_type.cpp @@ -56,7 +56,7 @@ LargeSubspacesBufferType::fallbackCopy(void *newBuffer, const void *oldBuffer, E auto& old_elem = old_elems[i]; new (new_elems + i) ArrayType(old_elem); if (!old_elem.empty()) { - _ops.copied_labels({old_elem.data(), old_elem.size()}); + _ops.copied_labels(unconstify(vespalib::ConstArrayRef<char>(old_elem.data(), old_elem.size()))); } } } diff --git a/searchlib/src/vespa/searchlib/tensor/nearest_neighbor_index.h b/searchlib/src/vespa/searchlib/tensor/nearest_neighbor_index.h index 51d66fdd14d..d40803dcafd 100644 --- a/searchlib/src/vespa/searchlib/tensor/nearest_neighbor_index.h +++ b/searchlib/src/vespa/searchlib/tensor/nearest_neighbor_index.h @@ -68,8 +68,8 @@ public: virtual void complete_add_document(uint32_t docid, std::unique_ptr<PrepareResult> prepare_result) = 0; virtual void remove_document(uint32_t docid) = 0; - virtual void transfer_hold_lists(generation_t current_gen) = 0; - virtual void trim_hold_lists(generation_t first_used_gen) = 0; + virtual void assign_generation(generation_t current_gen) = 0; + virtual void reclaim_memory(generation_t first_used_gen) = 0; virtual bool consider_compact(const CompactionStrategy& compaction_strategy) = 0; virtual vespalib::MemoryUsage update_stat(const CompactionStrategy& compaction_strategy) = 0; virtual vespalib::MemoryUsage memory_usage() const = 0; diff --git a/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.cpp b/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.cpp index 94bf3f1a37b..bb1c1a3d880 100644 --- a/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.cpp +++ b/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.cpp @@ -1,17 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "serialized_fast_value_attribute.h" -#include "streamed_value_saver.h" #include <vespa/eval/eval/value.h> -#include <vespa/fastlib/io/bufferedfile.h> #include <vespa/searchcommon/attribute/config.h> #include <vespa/log/log.h> LOG_SETUP(".searchlib.tensor.serialized_fast_value_attribute"); -#include "blob_sequence_reader.h" - using namespace vespalib; using namespace vespalib::eval; @@ -20,15 +16,15 @@ namespace search::tensor { SerializedFastValueAttribute::SerializedFastValueAttribute(stringref name, const Config &cfg) : TensorAttribute(name, cfg, _tensorBufferStore), _tensor_type(cfg.tensorType()), - _tensorBufferStore(_tensor_type, {}, 1000u) + _tensorBufferStore(_tensor_type, get_memory_allocator(), 1000u) { } SerializedFastValueAttribute::~SerializedFastValueAttribute() { - getGenerationHolder().clearHoldLists(); - _tensorStore.clearHoldLists(); + getGenerationHolder().reclaim_all(); + _tensorStore.reclaim_all_memory(); } void @@ -50,50 +46,4 @@ SerializedFastValueAttribute::getTensor(DocId docId) const return _tensorBufferStore.get_tensor(ref); } -bool -SerializedFastValueAttribute::onLoad(vespalib::Executor *) -{ - BlobSequenceReader tensorReader(*this); - if (!tensorReader.hasData()) { - return false; - } - setCreateSerialNum(tensorReader.getCreateSerialNum()); - assert(tensorReader.getVersion() == getVersion()); - uint32_t numDocs(tensorReader.getDocIdLimit()); - _refVector.reset(); - _refVector.unsafe_reserve(numDocs); - vespalib::Array<char> buffer(1024); - for (uint32_t lid = 0; lid < numDocs; ++lid) { - uint32_t tensorSize = tensorReader.getNextSize(); - if (tensorSize != 0) { - if (tensorSize > buffer.size()) { - buffer.resize(tensorSize + 1024); - } - tensorReader.readBlob(&buffer[0], tensorSize); - vespalib::nbostream source(&buffer[0], tensorSize); - EntryRef ref = _tensorBufferStore.store_encoded_tensor(source); - _refVector.push_back(AtomicEntryRef(ref)); - } else { - EntryRef invalid; - _refVector.push_back(AtomicEntryRef(invalid)); - } - } - setNumDocs(numDocs); - setCommittedDocIdLimit(numDocs); - return true; -} - - -std::unique_ptr<AttributeSaver> -SerializedFastValueAttribute::onInitSave(vespalib::stringref fileName) -{ - vespalib::GenerationHandler::Guard guard(getGenerationHandler(). - takeGuard()); - return std::make_unique<StreamedValueSaver> - (std::move(guard), - this->createAttributeHeader(fileName), - getRefCopy(), - _tensorBufferStore); -} - } diff --git a/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.h b/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.h index 5dd49a2bbc4..2124ddeb70a 100644 --- a/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.h +++ b/searchlib/src/vespa/searchlib/tensor/serialized_fast_value_attribute.h @@ -24,8 +24,6 @@ public: ~SerializedFastValueAttribute() override; void setTensor(DocId docId, const vespalib::eval::Value &tensor) override; std::unique_ptr<vespalib::eval::Value> getTensor(DocId docId) const override; - bool onLoad(vespalib::Executor *executor) override; - std::unique_ptr<AttributeSaver> onInitSave(vespalib::stringref fileName) override; }; } diff --git a/searchlib/src/vespa/searchlib/tensor/small_subspaces_buffer_type.cpp b/searchlib/src/vespa/searchlib/tensor/small_subspaces_buffer_type.cpp index adbd3dee2b7..ba27f017022 100644 --- a/searchlib/src/vespa/searchlib/tensor/small_subspaces_buffer_type.cpp +++ b/searchlib/src/vespa/searchlib/tensor/small_subspaces_buffer_type.cpp @@ -46,7 +46,7 @@ SmallSubspacesBufferType::fallbackCopy(void *newBuffer, const void *oldBuffer, E memcpy(newBuffer, oldBuffer, numElems); const char *elem = static_cast<const char *>(oldBuffer); while (numElems >= getArraySize()) { - _ops.copied_labels(vespalib::ConstArrayRef<char>(elem, getArraySize())); + _ops.copied_labels(unconstify(vespalib::ConstArrayRef<char>(elem, getArraySize()))); elem += getArraySize(); numElems -= getArraySize(); } diff --git a/searchlib/src/vespa/searchlib/tensor/streamed_value_store.cpp b/searchlib/src/vespa/searchlib/tensor/streamed_value_store.cpp deleted file mode 100644 index b4fddec25b3..00000000000 --- a/searchlib/src/vespa/searchlib/tensor/streamed_value_store.cpp +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include "streamed_value_store.h" -#include <vespa/eval/eval/value.h> -#include <vespa/eval/eval/value_codec.h> -#include <vespa/eval/eval/fast_value.hpp> -#include <vespa/eval/streamed/streamed_value_builder_factory.h> -#include <vespa/eval/streamed/streamed_value_view.h> -#include <vespa/vespalib/datastore/buffer_type.hpp> -#include <vespa/vespalib/datastore/compacting_buffers.h> -#include <vespa/vespalib/datastore/compaction_context.h> -#include <vespa/vespalib/datastore/compaction_strategy.h> -#include <vespa/vespalib/datastore/datastore.hpp> -#include <vespa/vespalib/objects/nbostream.h> -#include <vespa/vespalib/util/size_literals.h> -#include <vespa/vespalib/util/typify.h> -#include <vespa/log/log.h> - -LOG_SETUP(".searchlib.tensor.streamed_value_store"); - -using vespalib::datastore::CompactionContext; -using vespalib::datastore::CompactionSpec; -using vespalib::datastore::CompactionStrategy; -using vespalib::datastore::EntryRef; -using vespalib::datastore::Handle; -using vespalib::datastore::ICompactionContext; -using namespace vespalib::eval; -using vespalib::ConstArrayRef; -using vespalib::MemoryUsage; -using vespalib::string_id; -using vespalib::StringIdVector; - -namespace search::tensor { - -//----------------------------------------------------------------------------- - -namespace { - -template <typename CT, typename F> -void each_subspace(const Value &value, size_t num_mapped, size_t dense_size, F f) { - size_t subspace; - std::vector<string_id> addr(num_mapped); - std::vector<string_id*> refs; - refs.reserve(addr.size()); - for (string_id &label: addr) { - refs.push_back(&label); - } - auto cells = value.cells().typify<CT>(); - auto view = value.index().create_view({}); - view->lookup({}); - while (view->next_result(refs, subspace)) { - size_t offset = subspace * dense_size; - f(ConstArrayRef<string_id>(addr), ConstArrayRef<CT>(cells.begin() + offset, dense_size)); - } -} - -using TensorEntry = StreamedValueStore::TensorEntry; - -struct CreateTensorEntry { - template <typename CT> - static TensorEntry::SP invoke(const Value &value, size_t num_mapped, size_t dense_size) { - using EntryImpl = StreamedValueStore::TensorEntryImpl<CT>; - return std::make_shared<EntryImpl>(value, num_mapped, dense_size); - } -}; - -struct MyFastValueView final : Value { - const ValueType &my_type; - FastValueIndex my_index; - TypedCells my_cells; - MyFastValueView(const ValueType &type_ref, const StringIdVector &handle_view, TypedCells cells, size_t num_mapped, size_t num_spaces) - : my_type(type_ref), - my_index(num_mapped, handle_view, num_spaces), - my_cells(cells) - { - const StringIdVector &labels = handle_view; - for (size_t i = 0; i < num_spaces; ++i) { - ConstArrayRef<string_id> addr(labels.data() + (i * num_mapped), num_mapped); - my_index.map.add_mapping(FastAddrMap::hash_labels(addr)); - } - assert(my_index.map.size() == num_spaces); - } - const ValueType &type() const override { return my_type; } - const Value::Index &index() const override { return my_index; } - TypedCells cells() const override { return my_cells; } - MemoryUsage get_memory_usage() const override { - MemoryUsage usage = self_memory_usage<MyFastValueView>(); - usage.merge(my_index.map.estimate_extra_memory_usage()); - return usage; - } -}; - -} // <unnamed> - -//----------------------------------------------------------------------------- - -StreamedValueStore::TensorEntry::~TensorEntry() = default; - -StreamedValueStore::TensorEntry::SP -StreamedValueStore::TensorEntry::create_shared_entry(const Value &value) -{ - size_t num_mapped = value.type().count_mapped_dimensions(); - size_t dense_size = value.type().dense_subspace_size(); - return vespalib::typify_invoke<1,TypifyCellType,CreateTensorEntry>(value.type().cell_type(), value, num_mapped, dense_size); -} - -template <typename CT> -StreamedValueStore::TensorEntryImpl<CT>::TensorEntryImpl(const Value &value, size_t num_mapped, size_t dense_size) - : handles(), - cells() -{ - handles.reserve(num_mapped * value.index().size()); - cells.reserve(dense_size * value.index().size()); - auto store_subspace = [&](auto addr, auto data) { - for (string_id label: addr) { - handles.push_back(label); - } - for (CT entry: data) { - cells.push_back(entry); - } - }; - each_subspace<CT>(value, num_mapped, dense_size, store_subspace); -} - -template <typename CT> -Value::UP -StreamedValueStore::TensorEntryImpl<CT>::create_fast_value_view(const ValueType &type_ref) const -{ - size_t num_mapped = type_ref.count_mapped_dimensions(); - size_t dense_size = type_ref.dense_subspace_size(); - size_t num_spaces = cells.size() / dense_size; - assert(dense_size * num_spaces == cells.size()); - assert(num_mapped * num_spaces == handles.view().size()); - return std::make_unique<MyFastValueView>(type_ref, handles.view(), TypedCells(cells), num_mapped, num_spaces); -} - -template <typename CT> -void -StreamedValueStore::TensorEntryImpl<CT>::encode_value(const ValueType &type, vespalib::nbostream &target) const -{ - size_t num_mapped = type.count_mapped_dimensions(); - size_t dense_size = type.dense_subspace_size(); - size_t num_spaces = cells.size() / dense_size; - assert(dense_size * num_spaces == cells.size()); - assert(num_mapped * num_spaces == handles.view().size()); - StreamedValueView my_value(type, num_mapped, TypedCells(cells), num_spaces, handles.view()); - ::vespalib::eval::encode_value(my_value, target); -} - -template <typename CT> -MemoryUsage -StreamedValueStore::TensorEntryImpl<CT>::get_memory_usage() const -{ - MemoryUsage usage = self_memory_usage<TensorEntryImpl<CT>>(); - usage.merge(vector_extra_memory_usage(handles.view())); - usage.merge(vector_extra_memory_usage(cells)); - return usage; -} - -template <typename CT> -StreamedValueStore::TensorEntryImpl<CT>::~TensorEntryImpl() = default; - -//----------------------------------------------------------------------------- - -constexpr size_t MIN_BUFFER_ARRAYS = 8_Ki; - -StreamedValueStore::TensorBufferType::TensorBufferType() noexcept - : ParentType(1, MIN_BUFFER_ARRAYS, TensorStoreType::RefType::offsetSize()) -{ -} - -void -StreamedValueStore::TensorBufferType::cleanHold(void* buffer, size_t offset, ElemCount num_elems, CleanContext clean_ctx) -{ - TensorEntry::SP* elem = static_cast<TensorEntry::SP*>(buffer) + offset; - const auto& empty = empty_entry(); - for (size_t i = 0; i < num_elems; ++i) { - clean_ctx.extraBytesCleaned((*elem)->get_memory_usage().allocatedBytes()); - *elem = empty; - ++elem; - } -} - -StreamedValueStore::StreamedValueStore(const ValueType &tensor_type) - : TensorStore(_concrete_store), - _concrete_store(std::make_unique<TensorBufferType>()), - _tensor_type(tensor_type) -{ - _concrete_store.enableFreeLists(); -} - -StreamedValueStore::~StreamedValueStore() = default; - -EntryRef -StreamedValueStore::add_entry(TensorEntry::SP tensor) -{ - auto ref = _concrete_store.addEntry(tensor); - auto& state = _concrete_store.getBufferState(RefType(ref).bufferId()); - state.incExtraUsedBytes(tensor->get_memory_usage().allocatedBytes()); - return ref; -} - -const StreamedValueStore::TensorEntry * -StreamedValueStore::get_tensor_entry(EntryRef ref) const -{ - if (!ref.valid()) { - return nullptr; - } - const auto& entry = _concrete_store.getEntry(ref); - assert(entry); - return entry.get(); -} - -std::unique_ptr<vespalib::eval::Value> -StreamedValueStore::get_tensor(EntryRef ref) const -{ - if (const auto * ptr = get_tensor_entry(ref)) { - return ptr->create_fast_value_view(_tensor_type); - } - return {}; -} - -void -StreamedValueStore::holdTensor(EntryRef ref) -{ - if (!ref.valid()) { - return; - } - const auto& tensor = _concrete_store.getEntry(ref); - assert(tensor); - _concrete_store.holdElem(ref, 1, tensor->get_memory_usage().allocatedBytes()); -} - -TensorStore::EntryRef -StreamedValueStore::move(EntryRef ref) -{ - if (!ref.valid()) { - return EntryRef(); - } - const auto& old_tensor = _concrete_store.getEntry(ref); - assert(old_tensor); - auto new_ref = add_entry(old_tensor); - _concrete_store.holdElem(ref, 1, old_tensor->get_memory_usage().allocatedBytes()); - return new_ref; -} - -vespalib::MemoryUsage -StreamedValueStore::update_stat(const CompactionStrategy& compaction_strategy) -{ - auto memory_usage = _store.getMemoryUsage(); - _compaction_spec = CompactionSpec(compaction_strategy.should_compact_memory(memory_usage), false); - return memory_usage; -} - -std::unique_ptr<ICompactionContext> -StreamedValueStore::start_compact(const CompactionStrategy& compaction_strategy) -{ - auto compacting_buffers = _store.start_compact_worst_buffers(_compaction_spec, compaction_strategy); - return std::make_unique<CompactionContext>(*this, std::move(compacting_buffers)); -} - -bool -StreamedValueStore::encode_stored_tensor(EntryRef ref, vespalib::nbostream &target) const -{ - if (const auto * entry = get_tensor_entry(ref)) { - entry->encode_value(_tensor_type, target); - return true; - } else { - return false; - } -} - -TensorStore::EntryRef -StreamedValueStore::store_tensor(const Value &tensor) -{ - assert(tensor.type() == _tensor_type); - return add_entry(TensorEntry::create_shared_entry(tensor)); -} - -TensorStore::EntryRef -StreamedValueStore::store_encoded_tensor(vespalib::nbostream &encoded) -{ - const auto &factory = StreamedValueBuilderFactory::get(); - auto val = vespalib::eval::decode_value(encoded, factory); - return store_tensor(*val); -} - -} diff --git a/searchlib/src/vespa/searchlib/tensor/streamed_value_store.h b/searchlib/src/vespa/searchlib/tensor/streamed_value_store.h deleted file mode 100644 index 58137e316dd..00000000000 --- a/searchlib/src/vespa/searchlib/tensor/streamed_value_store.h +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#pragma once - -#include "tensor_store.h" -#include <vespa/eval/eval/value_type.h> -#include <vespa/eval/eval/value.h> -#include <vespa/eval/streamed/streamed_value.h> -#include <vespa/vespalib/datastore/datastore.h> -#include <vespa/vespalib/objects/nbostream.h> -#include <vespa/vespalib/util/shared_string_repo.h> - -namespace search::tensor { - -/** - * Class for StreamedValue tensors in memory. - */ -class StreamedValueStore : public TensorStore { -public: - using Value = vespalib::eval::Value; - using ValueType = vespalib::eval::ValueType; - using Handles = vespalib::SharedStringRepo::Handles; - using MemoryUsage = vespalib::MemoryUsage; - - // interface for tensor entries - struct TensorEntry { - using SP = std::shared_ptr<TensorEntry>; - virtual Value::UP create_fast_value_view(const ValueType &type_ref) const = 0; - virtual void encode_value(const ValueType &type, vespalib::nbostream &target) const = 0; - virtual MemoryUsage get_memory_usage() const = 0; - virtual ~TensorEntry(); - static TensorEntry::SP create_shared_entry(const Value &value); - }; - - // implementation of tensor entries - template <typename CT> - struct TensorEntryImpl : public TensorEntry { - Handles handles; - std::vector<CT> cells; - TensorEntryImpl(const Value &value, size_t num_mapped, size_t dense_size); - Value::UP create_fast_value_view(const ValueType &type_ref) const override; - void encode_value(const ValueType &type, vespalib::nbostream &target) const override; - MemoryUsage get_memory_usage() const override; - ~TensorEntryImpl() override; - }; - -private: - // Note: Must use SP (instead of UP) because of fallbackCopy() and initializeReservedElements() in BufferType, - // and implementation of move(). - using TensorStoreType = vespalib::datastore::DataStore<TensorEntry::SP>; - - class TensorBufferType : public vespalib::datastore::BufferType<TensorEntry::SP> { - private: - using ParentType = BufferType<TensorEntry::SP>; - using ParentType::empty_entry; - using CleanContext = typename ParentType::CleanContext; - public: - TensorBufferType() noexcept; - void cleanHold(void* buffer, size_t offset, ElemCount num_elems, CleanContext clean_ctx) override; - }; - TensorStoreType _concrete_store; - const vespalib::eval::ValueType _tensor_type; - EntryRef add_entry(TensorEntry::SP tensor); - const TensorEntry* get_tensor_entry(EntryRef ref) const; -public: - StreamedValueStore(const vespalib::eval::ValueType &tensor_type); - ~StreamedValueStore() override; - - using RefType = TensorStoreType::RefType; - - void holdTensor(EntryRef ref) override; - EntryRef move(EntryRef ref) override; - vespalib::MemoryUsage update_stat(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; - std::unique_ptr<vespalib::datastore::ICompactionContext> start_compact(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; - - std::unique_ptr<vespalib::eval::Value> get_tensor(EntryRef ref) const; - bool encode_stored_tensor(EntryRef ref, vespalib::nbostream &target) const; - - EntryRef store_tensor(const vespalib::eval::Value &tensor); - EntryRef store_encoded_tensor(vespalib::nbostream &encoded); -}; - - -} diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_attribute.cpp b/searchlib/src/vespa/searchlib/tensor/tensor_attribute.cpp index cdaea07176a..f29751cdabe 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_attribute.cpp +++ b/searchlib/src/vespa/searchlib/tensor/tensor_attribute.cpp @@ -1,6 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "tensor_attribute.h" +#include "blob_sequence_reader.h" +#include "tensor_store_saver.h" #include <vespa/document/base/exceptions.h> #include <vespa/document/datatype/tensor_data_type.h> #include <vespa/searchlib/attribute/address_space_components.h> @@ -108,17 +110,17 @@ TensorAttribute::onUpdateStat() } void -TensorAttribute::removeOldGenerations(generation_t firstUsed) +TensorAttribute::reclaim_memory(generation_t oldest_used_gen) { - _tensorStore.trimHoldLists(firstUsed); - getGenerationHolder().trimHoldLists(firstUsed); + _tensorStore.reclaim_memory(oldest_used_gen); + getGenerationHolder().reclaim(oldest_used_gen); } void -TensorAttribute::onGenerationChange(generation_t generation) +TensorAttribute::before_inc_generation(generation_t current_gen) { - getGenerationHolder().transferHoldLists(generation - 1); - _tensorStore.transferHoldLists(generation - 1); + getGenerationHolder().assign_generation(current_gen); + _tensorStore.assign_generation(current_gen); } bool @@ -132,7 +134,7 @@ TensorAttribute::addDoc(DocId &docId) if (incGen) { incGeneration(); } else { - removeAllOldGenerations(); + reclaim_unused_memory(); } return true; } @@ -167,7 +169,7 @@ TensorAttribute::update_stat() { vespalib::MemoryUsage result = _refVector.getMemoryUsage(); result.merge(_tensorStore.update_stat(getConfig().getCompactionStrategy())); - result.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + result.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); return result; } @@ -176,7 +178,7 @@ TensorAttribute::memory_usage() const { vespalib::MemoryUsage result = _refVector.getMemoryUsage(); result.merge(_tensorStore.getMemoryUsage()); - result.mergeGenerationHeldBytes(getGenerationHolder().getHeldBytes()); + result.mergeGenerationHeldBytes(getGenerationHolder().get_held_bytes()); return result; } @@ -277,6 +279,51 @@ TensorAttribute::getRefCopy() const return result; } +bool +TensorAttribute::onLoad(vespalib::Executor*) +{ + BlobSequenceReader tensorReader(*this); + if (!tensorReader.hasData()) { + return false; + } + setCreateSerialNum(tensorReader.getCreateSerialNum()); + assert(tensorReader.getVersion() == getVersion()); + uint32_t numDocs = tensorReader.getDocIdLimit(); + _refVector.reset(); + _refVector.unsafe_reserve(numDocs); + vespalib::Array<char> buffer(1024); + for (uint32_t lid = 0; lid < numDocs; ++lid) { + uint32_t tensorSize = tensorReader.getNextSize(); + if (tensorSize != 0) { + if (tensorSize > buffer.size()) { + buffer.resize(tensorSize + 1024); + } + tensorReader.readBlob(&buffer[0], tensorSize); + vespalib::nbostream source(&buffer[0], tensorSize); + EntryRef ref = _tensorStore.store_encoded_tensor(source); + _refVector.push_back(AtomicEntryRef(ref)); + } else { + EntryRef invalid; + _refVector.push_back(AtomicEntryRef(invalid)); + } + } + setNumDocs(numDocs); + setCommittedDocIdLimit(numDocs); + return true; +} + +std::unique_ptr<AttributeSaver> +TensorAttribute::onInitSave(vespalib::stringref fileName) +{ + vespalib::GenerationHandler::Guard guard(getGenerationHandler(). + takeGuard()); + return std::make_unique<TensorStoreSaver> + (std::move(guard), + this->createAttributeHeader(fileName), + getRefCopy(), + _tensorStore); +} + void TensorAttribute::update_tensor(DocId docId, const document::TensorUpdate &update, diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_attribute.h b/searchlib/src/vespa/searchlib/tensor/tensor_attribute.h index 411efcd8fea..b7bac35d1b7 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_attribute.h +++ b/searchlib/src/vespa/searchlib/tensor/tensor_attribute.h @@ -36,6 +36,8 @@ protected: void populate_state(vespalib::slime::Cursor& object) const; void populate_address_space_usage(AddressSpaceUsage& usage) const override; EntryRef acquire_entry_ref(DocId doc_id) const noexcept { return _refVector.acquire_elem_ref(doc_id).load_acquire(); } + bool onLoad(vespalib::Executor *executor) override; + std::unique_ptr<AttributeSaver> onInitSave(vespalib::stringref fileName) override; public: using RefCopyVector = vespalib::Array<EntryRef>; @@ -46,8 +48,8 @@ public: uint32_t clearDoc(DocId docId) override; void onCommit() override; void onUpdateStat() override; - void removeOldGenerations(generation_t firstUsed) override; - void onGenerationChange(generation_t generation) override; + void reclaim_memory(generation_t oldest_used_gen) override; + void before_inc_generation(generation_t current_gen) override; bool addDoc(DocId &docId) override; std::unique_ptr<vespalib::eval::Value> getEmptyTensor() const override; vespalib::eval::TypedCells extract_cells_ref(uint32_t docid) const override; diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.cpp b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.cpp index 22298354444..d13d3f24b5b 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.cpp +++ b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.cpp @@ -6,7 +6,7 @@ #include <vespa/eval/eval/value_codec.h> #include <vespa/eval/eval/value_type.h> #include <vespa/eval/streamed/streamed_value_view.h> -#include <vespa/vespalib/util/arrayref.h> +#include <vespa/vespalib/util/atomic.h> #include <vespa/vespalib/util/shared_string_repo.h> #include <algorithm> @@ -85,16 +85,25 @@ TensorBufferOperations::TensorBufferOperations(const vespalib::eval::ValueType& TensorBufferOperations::~TensorBufferOperations() = default; uint32_t -TensorBufferOperations::get_num_subspaces(ConstArrayRef<char> buf) const noexcept +TensorBufferOperations::get_num_subspaces_and_flag(ConstArrayRef<char> buf) const noexcept { assert(buf.size() >= get_num_subspaces_size()); - return *reinterpret_cast<const uint32_t*>(buf.data()); + const uint32_t& num_subspaces_and_flag_ref = *reinterpret_cast<const uint32_t*>(buf.data()); + return vespalib::atomic::load_ref_relaxed(num_subspaces_and_flag_ref); +} + +void +TensorBufferOperations::set_skip_reclaim_labels(ArrayRef<char> buf, uint32_t num_subspaces_and_flag) const noexcept +{ + uint32_t& num_subspaces_and_flag_ref = *reinterpret_cast<uint32_t*>(buf.data()); + vespalib::atomic::store_ref_relaxed(num_subspaces_and_flag_ref, (num_subspaces_and_flag | skip_reclaim_labels_mask)); } void TensorBufferOperations::store_tensor(ArrayRef<char> buf, const vespalib::eval::Value& tensor) { uint32_t num_subspaces = tensor.index().size(); + assert(num_subspaces <= num_subspaces_mask); auto labels_end_offset = get_labels_offset() + get_labels_mem_size(num_subspaces); auto cells_size = num_subspaces * _dense_subspace_size; auto cells_mem_size = cells_size * _cell_mem_size; // Size measured in bytes @@ -148,23 +157,26 @@ TensorBufferOperations::make_fast_view(ConstArrayRef<char> buf, const vespalib:: } void -TensorBufferOperations::copied_labels(ConstArrayRef<char> buf) const +TensorBufferOperations::copied_labels(ArrayRef<char> buf) const { - auto num_subspaces = get_num_subspaces(buf); - ConstArrayRef<string_id> labels(reinterpret_cast<const string_id*>(buf.data() + get_labels_offset()), num_subspaces * _num_mapped_dimensions); - for (auto& label : labels) { - SharedStringRepo::unsafe_copy(label); // Source buffer has an existing ref + auto num_subspaces_and_flag = get_num_subspaces_and_flag(buf); + if (!get_skip_reclaim_labels(num_subspaces_and_flag)) { + set_skip_reclaim_labels(buf, num_subspaces_and_flag); } } void TensorBufferOperations::reclaim_labels(ArrayRef<char> buf) const { - auto num_subspaces = get_num_subspaces(buf); + auto num_subspaces_and_flag = get_num_subspaces_and_flag(buf); + if (get_skip_reclaim_labels(num_subspaces_and_flag)) { + return; + } + set_skip_reclaim_labels(buf, num_subspaces_and_flag); + auto num_subspaces = get_num_subspaces(num_subspaces_and_flag); ArrayRef<string_id> labels(reinterpret_cast<string_id*>(buf.data() + get_labels_offset()), num_subspaces * _num_mapped_dimensions); for (auto& label : labels) { SharedStringRepo::unsafe_reclaim(label); - label = string_id(); // Clear label to avoid double reclaim } } diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.h b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.h index 18c7bc84ab2..7b6d089f8f2 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.h +++ b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_operations.h @@ -4,15 +4,12 @@ #include <vespa/eval/eval/cell_type.h> #include <vespa/vespalib/datastore/aligner.h> +#include <vespa/vespalib/util/arrayref.h> #include <vespa/vespalib/util/string_id.h> #include <cstddef> #include <memory> -namespace vespalib { -template <typename T> class ArrayRef; -template <typename T> class ConstArrayRef; -class nbostream; -} +namespace vespalib { class nbostream; } namespace vespalib::eval { struct Value; @@ -27,7 +24,10 @@ namespace search::tensor { * * Layout of buffer is: * - * num_subspaces - number of subspaces + * num_subspaces_and_flag + * 31 least significant bits is num_subspaces - number of subspaces + * 1 bit to signal that reclaim_labels should be a noop (i.e. buffer has been copied + * as part of compaction or fallback copy (due to datastore buffer fallback resize)). * labels[num_subspaces * _num_mapped_dimensions] - array of labels for sparse dimensions * padding - to align start of cells * cells[num_subspaces * _dense_subspaces_size] - array of tensor cell values @@ -50,6 +50,8 @@ class TensorBufferOperations static constexpr size_t CELLS_ALIGNMENT = 16; static constexpr size_t CELLS_ALIGNMENT_MEM_SIZE_MIN = 32; + static constexpr uint32_t num_subspaces_mask = ((1u << 31) - 1); + static constexpr uint32_t skip_reclaim_labels_mask = (1u << 31); static constexpr size_t get_num_subspaces_size() noexcept { return sizeof(uint32_t); } static constexpr size_t get_labels_offset() noexcept { return get_num_subspaces_size(); } @@ -65,7 +67,17 @@ class TensorBufferOperations size_t get_cells_offset(uint32_t num_subspaces, auto aligner) const noexcept { return aligner.align(get_labels_offset() + get_labels_mem_size(num_subspaces)); } - uint32_t get_num_subspaces(vespalib::ConstArrayRef<char> buf) const noexcept; + uint32_t get_num_subspaces_and_flag(vespalib::ConstArrayRef<char> buf) const noexcept; + void set_skip_reclaim_labels(vespalib::ArrayRef<char> buf, uint32_t num_subspaces_and_flag) const noexcept; + static uint32_t get_num_subspaces(uint32_t num_subspaces_and_flag) noexcept { + return num_subspaces_and_flag & num_subspaces_mask; + } + static bool get_skip_reclaim_labels(uint32_t num_subspaces_and_flag) noexcept { + return (num_subspaces_and_flag & skip_reclaim_labels_mask) != 0; + } + uint32_t get_num_subspaces(vespalib::ConstArrayRef<char> buf) const noexcept { + return get_num_subspaces(get_num_subspaces_and_flag(buf)); + } public: size_t get_array_size(uint32_t num_subspaces) const noexcept { auto cells_mem_size = get_cells_mem_size(num_subspaces); @@ -81,9 +93,9 @@ public: void store_tensor(vespalib::ArrayRef<char> buf, const vespalib::eval::Value& tensor); std::unique_ptr<vespalib::eval::Value> make_fast_view(vespalib::ConstArrayRef<char> buf, const vespalib::eval::ValueType& tensor_type) const; - // Increase reference counts for labels after copying tensor buffer - void copied_labels(vespalib::ConstArrayRef<char> buf) const; - // Decrease reference counts for labels and invalidate them + // Mark that reclaim_labels should be skipped for old buffer after copying tensor buffer + void copied_labels(vespalib::ArrayRef<char> buf) const; + // Decrease reference counts for labels and set skip flag unless skip flag is set. void reclaim_labels(vespalib::ArrayRef<char> buf) const; // Serialize stored tensor to target (used when saving attribute) void encode_stored_tensor(vespalib::ConstArrayRef<char> buf, const vespalib::eval::ValueType& type, vespalib::nbostream& target) const; diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.cpp b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.cpp index eff6ac9f374..09d9ac7dd31 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.cpp +++ b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.cpp @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "tensor_buffer_store.h" +#include <vespa/document/util/serializableexceptions.h> #include <vespa/eval/eval/value_codec.h> #include <vespa/eval/streamed/streamed_value_builder_factory.h> #include <vespa/vespalib/datastore/array_store.hpp> @@ -10,6 +11,7 @@ #include <vespa/vespalib/datastore/datastore.hpp> #include <vespa/vespalib/util/size_literals.h> +using document::DeserializeException; using vespalib::alloc::MemoryAllocator; using vespalib::datastore::CompactionContext; using vespalib::datastore::CompactionStrategy; @@ -46,15 +48,14 @@ TensorBufferStore::holdTensor(EntryRef ref) } EntryRef -TensorBufferStore::move(EntryRef ref) +TensorBufferStore::move_on_compact(EntryRef ref) { if (!ref.valid()) { return EntryRef(); } - auto buf = _array_store.get(ref); + auto buf = _array_store.get_writable(ref); auto new_ref = _array_store.add(buf); _ops.copied_labels(buf); - _array_store.remove(ref); return new_ref; } @@ -90,6 +91,9 @@ TensorBufferStore::store_encoded_tensor(vespalib::nbostream &encoded) { const auto &factory = StreamedValueBuilderFactory::get(); auto val = vespalib::eval::decode_value(encoded, factory); + if (!encoded.empty()) { + throw DeserializeException("Leftover bytes deserializing tensor attribute value.", VESPA_STRLOC); + } return store_tensor(*val); } diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.h b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.h index 585bbd7a0c3..1b5520233e1 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.h +++ b/searchlib/src/vespa/searchlib/tensor/tensor_buffer_store.h @@ -27,13 +27,13 @@ public: TensorBufferStore(const vespalib::eval::ValueType& tensor_type, std::shared_ptr<vespalib::alloc::MemoryAllocator> allocator, uint32_t max_small_subspaces_type_id); ~TensorBufferStore(); void holdTensor(EntryRef ref) override; - EntryRef move(EntryRef ref) override; + EntryRef move_on_compact(EntryRef ref) override; vespalib::MemoryUsage update_stat(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; std::unique_ptr<vespalib::datastore::ICompactionContext> start_compact(const vespalib::datastore::CompactionStrategy& compaction_strategy) override; - EntryRef store_tensor(const vespalib::eval::Value &tensor); - EntryRef store_encoded_tensor(vespalib::nbostream &encoded); - std::unique_ptr<vespalib::eval::Value> get_tensor(EntryRef ref) const; - bool encode_stored_tensor(EntryRef ref, vespalib::nbostream &target) const; + EntryRef store_tensor(const vespalib::eval::Value& tensor) override; + EntryRef store_encoded_tensor(vespalib::nbostream& encoded) override; + std::unique_ptr<vespalib::eval::Value> get_tensor(EntryRef ref) const override; + bool encode_stored_tensor(EntryRef ref, vespalib::nbostream& target) const override; }; } diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.cpp b/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.cpp index a8399d9ddeb..791662caed7 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.cpp +++ b/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.cpp @@ -5,6 +5,7 @@ #include <vespa/eval/eval/fast_value.h> #include <vespa/eval/eval/value_codec.h> #include <vespa/eval/eval/value.h> +#include <vespa/vespalib/objects/nbostream.h> using document::DeserializeException; using vespalib::eval::FastValueBuilderFactory; @@ -25,10 +26,4 @@ std::unique_ptr<Value> deserialize_tensor(vespalib::nbostream &buffer) } } -std::unique_ptr<Value> deserialize_tensor(const void *data, size_t size) -{ - vespalib::nbostream wrapStream(data, size); - return deserialize_tensor(wrapStream); -} - } // namespace diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.h b/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.h index 7d1ede29167..18b9731b30b 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.h +++ b/searchlib/src/vespa/searchlib/tensor/tensor_deserialize.h @@ -1,12 +1,13 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#include <vespa/eval/eval/value.h> -#include <vespa/vespalib/objects/nbostream.h> +#pragma once -namespace search::tensor { +#include <memory> -extern std::unique_ptr<vespalib::eval::Value> -deserialize_tensor(const void *data, size_t size); +namespace vespalib { class nbostream; } +namespace vespalib::eval { struct Value; } + +namespace search::tensor { extern std::unique_ptr<vespalib::eval::Value> deserialize_tensor(vespalib::nbostream &stream); diff --git a/searchlib/src/vespa/searchlib/tensor/tensor_store.h b/searchlib/src/vespa/searchlib/tensor/tensor_store.h index 90bc82c4fde..53551bc48fa 100644 --- a/searchlib/src/vespa/searchlib/tensor/tensor_store.h +++ b/searchlib/src/vespa/searchlib/tensor/tensor_store.h @@ -8,6 +8,7 @@ #include <vespa/vespalib/datastore/i_compactable.h> #include <vespa/vespalib/util/generationhandler.h> +namespace vespalib { class nbostream; } namespace vespalib::datastore { struct ICompactionContext; } namespace vespalib::eval { struct Value; } @@ -41,18 +42,23 @@ public: virtual std::unique_ptr<vespalib::datastore::ICompactionContext> start_compact(const vespalib::datastore::CompactionStrategy& compaction_strategy) = 0; + virtual EntryRef store_tensor(const vespalib::eval::Value& tensor) = 0; + virtual EntryRef store_encoded_tensor(vespalib::nbostream& encoded) = 0; + virtual std::unique_ptr<vespalib::eval::Value> get_tensor(EntryRef ref) const = 0; + virtual bool encode_stored_tensor(EntryRef ref, vespalib::nbostream& target) const = 0; + // Inherit doc from DataStoreBase - void trimHoldLists(generation_t usedGen) { - _store.trimHoldLists(usedGen); + void reclaim_memory(generation_t oldest_used_gen) { + _store.reclaim_memory(oldest_used_gen); } // Inherit doc from DataStoreBase - void transferHoldLists(generation_t generation) { - _store.transferHoldLists(generation); + void assign_generation(generation_t current_gen) { + _store.assign_generation(current_gen); } - void clearHoldLists() { - _store.clearHoldLists(); + void reclaim_all_memory() { + _store.reclaim_all_memory(); } vespalib::MemoryUsage getMemoryUsage() const { diff --git a/searchlib/src/vespa/searchlib/tensor/streamed_value_saver.cpp b/searchlib/src/vespa/searchlib/tensor/tensor_store_saver.cpp index 4c188bb3370..0963e79b0dd 100644 --- a/searchlib/src/vespa/searchlib/tensor/streamed_value_saver.cpp +++ b/searchlib/src/vespa/searchlib/tensor/tensor_store_saver.cpp @@ -1,7 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#include "streamed_value_saver.h" -#include "tensor_buffer_store.h" +#include "tensor_store_saver.h" +#include "tensor_store.h" #include <vespa/searchlib/attribute/iattributesavetarget.h> #include <vespa/searchlib/util/bufferwriter.h> @@ -11,21 +11,21 @@ using vespalib::GenerationHandler; namespace search::tensor { -StreamedValueSaver:: -StreamedValueSaver(GenerationHandler::Guard &&guard, - const attribute::AttributeHeader &header, - RefCopyVector &&refs, - const TensorBufferStore &tensorStore) +TensorStoreSaver:: +TensorStoreSaver(GenerationHandler::Guard &&guard, + const attribute::AttributeHeader &header, + RefCopyVector &&refs, + const TensorStore &tensorStore) : AttributeSaver(std::move(guard), header), _refs(std::move(refs)), _tensorStore(tensorStore) { } -StreamedValueSaver::~StreamedValueSaver() = default; +TensorStoreSaver::~TensorStoreSaver() = default; bool -StreamedValueSaver::onSave(IAttributeSaveTarget &saveTarget) +TensorStoreSaver::onSave(IAttributeSaveTarget &saveTarget) { auto datWriter = saveTarget.datWriter().allocBufferWriter(); const uint32_t docIdLimit(_refs.size()); diff --git a/searchlib/src/vespa/searchlib/tensor/streamed_value_saver.h b/searchlib/src/vespa/searchlib/tensor/tensor_store_saver.h index 0ce864769f7..a4bf6e07519 100644 --- a/searchlib/src/vespa/searchlib/tensor/streamed_value_saver.h +++ b/searchlib/src/vespa/searchlib/tensor/tensor_store_saver.h @@ -7,12 +7,10 @@ namespace search::tensor { -class TensorBufferStore; - /* * Class for saving a tensor attribute. */ -class StreamedValueSaver : public AttributeSaver +class TensorStoreSaver : public AttributeSaver { public: using RefCopyVector = TensorAttribute::RefCopyVector; @@ -20,16 +18,16 @@ private: using GenerationHandler = vespalib::GenerationHandler; RefCopyVector _refs; - const TensorBufferStore &_tensorStore; + const TensorStore& _tensorStore; bool onSave(IAttributeSaveTarget &saveTarget) override; public: - StreamedValueSaver(GenerationHandler::Guard &&guard, - const attribute::AttributeHeader &header, - RefCopyVector &&refs, - const TensorBufferStore &tensorStore); + TensorStoreSaver(GenerationHandler::Guard &&guard, + const attribute::AttributeHeader &header, + RefCopyVector &&refs, + const TensorStore &tensorStore); - virtual ~StreamedValueSaver(); + virtual ~TensorStoreSaver(); }; } // namespace search::tensor diff --git a/searchlib/src/vespa/searchlib/test/CMakeLists.txt b/searchlib/src/vespa/searchlib/test/CMakeLists.txt index ed884a46217..2eb207d0d43 100644 --- a/searchlib/src/vespa/searchlib/test/CMakeLists.txt +++ b/searchlib/src/vespa/searchlib/test/CMakeLists.txt @@ -2,12 +2,14 @@ vespa_add_library(searchlib_test SOURCES document_weight_attribute_helper.cpp + doc_builder.cpp imported_attribute_fixture.cpp initrange.cpp make_attribute_map_lookup_node.cpp mock_attribute_context.cpp mock_attribute_manager.cpp searchiteratorverifier.cpp + string_field_builder.cpp $<TARGET_OBJECTS:searchlib_test_fakedata> $<TARGET_OBJECTS:searchlib_searchlib_test_diskindex> $<TARGET_OBJECTS:searchlib_test_gtest_migration> diff --git a/searchlib/src/vespa/searchlib/test/doc_builder.cpp b/searchlib/src/vespa/searchlib/test/doc_builder.cpp new file mode 100644 index 00000000000..2312bf1d6bf --- /dev/null +++ b/searchlib/src/vespa/searchlib/test/doc_builder.cpp @@ -0,0 +1,117 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "doc_builder.h" +#include <vespa/document/datatype/documenttype.h> +#include <vespa/document/fieldvalue/arrayfieldvalue.h> +#include <vespa/document/fieldvalue/document.h> +#include <vespa/document/fieldvalue/mapfieldvalue.h> +#include <vespa/document/fieldvalue/structfieldvalue.h> +#include <vespa/document/fieldvalue/weightedsetfieldvalue.h> +#include <vespa/document/repo/documenttyperepo.h> +#include <vespa/document/repo/document_type_repo_factory.h> +#include <vespa/document/repo/configbuilder.h> +#include <cassert> + +using document::ArrayFieldValue; +using document::DataType; +using document::Document; +using document::DocumentId; +using document::DocumentTypeRepo; +using document::DocumentTypeRepoFactory; +using document::MapFieldValue; +using document::StructFieldValue; +using document::WeightedSetFieldValue; + +namespace search::test { + +namespace { + +DocumenttypesConfig +get_document_types_config(DocBuilder::AddFieldsType add_fields) +{ + using namespace document::config_builder; + DocumenttypesConfigBuilderHelper builder; + Struct header("searchdocument.header"); + add_fields(header); + builder.document(42, "searchdocument", + header, + Struct("searchdocument.body")); + return builder.config(); +} + +} + +DocBuilder::DocBuilder() + : DocBuilder([](auto&) noexcept {}) +{ +} + +DocBuilder::DocBuilder(AddFieldsType add_fields) + : _document_types_config(std::make_shared<const DocumenttypesConfig>(get_document_types_config(add_fields))), + _repo(DocumentTypeRepoFactory::make(*_document_types_config)), + _document_type(_repo->getDocumentType("searchdocument")) +{ +} + +DocBuilder::~DocBuilder() = default; + + +std::unique_ptr<Document> +DocBuilder::make_document(vespalib::string document_id) +{ + auto doc = std::make_unique<Document>(get_document_type(), DocumentId(document_id)); + doc->setRepo(get_repo()); + return doc; +} + +const DataType& +DocBuilder::get_data_type(const vespalib::string &name) const +{ + const DataType *type = _repo->getDataType(*_document_type, name); + assert(type); + return *type; +} + +ArrayFieldValue +DocBuilder::make_array(vespalib::stringref field_name) +{ + auto& field = _document_type->getField(field_name); + auto& field_type = field.getDataType(); + assert(field_type.isArray()); + return {field_type}; +} +MapFieldValue +DocBuilder::make_map(vespalib::stringref field_name) +{ + auto& field = _document_type->getField(field_name); + auto& field_type = field.getDataType(); + assert(field_type.isMap()); + return {field_type}; + +} + +WeightedSetFieldValue +DocBuilder::make_wset(vespalib::stringref field_name) +{ + auto& field = _document_type->getField(field_name); + auto& field_type = field.getDataType(); + assert(field_type.isWeightedSet()); + return {field_type}; +} + +StructFieldValue +DocBuilder::make_struct(vespalib::stringref field_name) +{ + auto& field = _document_type->getField(field_name); + auto& field_type = field.getDataType(); + assert(field_type.isStructured()); + return {field_type}; +} + +StructFieldValue +DocBuilder::make_url() +{ + return {get_data_type("url")}; +} + +} diff --git a/searchlib/src/vespa/searchlib/index/empty_doc_builder.h b/searchlib/src/vespa/searchlib/test/doc_builder.h index d4b54359f87..75dbc30a0fb 100644 --- a/searchlib/src/vespa/searchlib/index/empty_doc_builder.h +++ b/searchlib/src/vespa/searchlib/test/doc_builder.h @@ -2,35 +2,50 @@ #pragma once +#include <vespa/document/config/documenttypes_config_fwd.h> #include <vespa/vespalib/stllike/string.h> #include <functional> #include <memory> namespace document { +class ArrayFieldValue; class DataType; class Document; class DocumentType; class DocumentTypeRepo; +class MapFieldValue; +class StructFieldValue; +class WeightedSetFieldValue; } +namespace document::config::internal { class InternalDocumenttypesType; } namespace document::config_builder { struct Struct; } -namespace search::index { +namespace search::test { /* * Class used to make empty search documents. */ -class EmptyDocBuilder { +class DocBuilder { + using DocumenttypesConfig = const document::config::internal::InternalDocumenttypesType; + std::shared_ptr<const DocumenttypesConfig> _document_types_config; std::shared_ptr<const document::DocumentTypeRepo> _repo; const document::DocumentType* _document_type; public: using AddFieldsType = std::function<void(document::config_builder::Struct&)>; - explicit EmptyDocBuilder(AddFieldsType add_fields); - ~EmptyDocBuilder(); + DocBuilder(); + explicit DocBuilder(AddFieldsType add_fields); + ~DocBuilder(); const document::DocumentTypeRepo& get_repo() const noexcept { return *_repo; } std::shared_ptr<const document::DocumentTypeRepo> get_repo_sp() const noexcept { return _repo; } const document::DocumentType& get_document_type() const noexcept { return *_document_type; } std::unique_ptr<document::Document> make_document(vespalib::string document_id); const document::DataType &get_data_type(const vespalib::string &name) const; + const DocumenttypesConfig& get_documenttypes_config() const noexcept { return *_document_types_config; } + document::ArrayFieldValue make_array(vespalib::stringref field_name); + document::MapFieldValue make_map(vespalib::stringref field_name); + document::WeightedSetFieldValue make_wset(vespalib::stringref field_name); + document::StructFieldValue make_struct(vespalib::stringref field_name); + document::StructFieldValue make_url(); }; } diff --git a/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.cpp b/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.cpp index 2597aec3dd7..e918c523fcf 100644 --- a/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.cpp +++ b/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.cpp @@ -12,12 +12,14 @@ #include <vespa/vespalib/btree/btreenodeallocator.hpp> #include <vespa/vespalib/btree/btreenodestore.hpp> #include <vespa/vespalib/btree/btreeroot.hpp> +#include <vespa/vespalib/datastore/compaction_strategy.h> #include <vespa/log/log.h> LOG_SETUP(".fakememtreeocc"); using search::fef::TermFieldMatchData; using search::fef::TermFieldMatchDataPosition; +using vespalib::datastore::CompactionStrategy; namespace search::fakedata { @@ -167,9 +169,9 @@ FakeMemTreeOccMgr::freeze() void -FakeMemTreeOccMgr::transferHoldLists() +FakeMemTreeOccMgr::assign_generation() { - _allocator.transferHoldLists(_generationHandler.getCurrentGeneration()); + _allocator.assign_generation(_generationHandler.getCurrentGeneration()); } void @@ -180,9 +182,9 @@ FakeMemTreeOccMgr::incGeneration() void -FakeMemTreeOccMgr::trimHoldLists() +FakeMemTreeOccMgr::reclaim_memory() { - _allocator.trimHoldLists(_generationHandler.getFirstUsedGeneration()); + _allocator.reclaim_memory(_generationHandler.get_oldest_used_generation()); } @@ -190,9 +192,9 @@ void FakeMemTreeOccMgr::sync() { freeze(); - transferHoldLists(); + assign_generation(); incGeneration(); - trimHoldLists(); + reclaim_memory(); } @@ -280,7 +282,9 @@ FakeMemTreeOccMgr::compactTrees() { // compact full trees by calling incremental compaction methods in a loop - std::vector<uint32_t> toHold = _allocator.startCompact(); + // Use a compaction strategy that will compact all active buffers + auto compaction_strategy = CompactionStrategy::make_compact_all_active_buffers_strategy(); + auto compacting_buffers = _allocator.start_compact_worst(compaction_strategy); for (uint32_t wordIdx = 0; wordIdx < _postingIdxs.size(); ++wordIdx) { PostingIdx &pidx(*_postingIdxs[wordIdx].get()); Tree &tree = pidx._tree; @@ -291,7 +295,7 @@ FakeMemTreeOccMgr::compactTrees() itr.moveNextLeafNode(); } } - _allocator.finishCompact(toHold); + compacting_buffers->finish(); sync(); } diff --git a/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h b/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h index d0a75930ed5..290ba1cf140 100644 --- a/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h +++ b/searchlib/src/vespa/searchlib/test/fakedata/fakememtreeocc.h @@ -94,9 +94,9 @@ public: ~FakeMemTreeOccMgr(); void freeze(); - void transferHoldLists(); + void assign_generation(); void incGeneration(); - void trimHoldLists(); + void reclaim_memory(); void sync(); void add(uint32_t wordIdx, index::DocIdAndFeatures &features) override; void remove(uint32_t wordIdx, uint32_t docId) override; diff --git a/searchlib/src/vespa/searchlib/test/string_field_builder.cpp b/searchlib/src/vespa/searchlib/test/string_field_builder.cpp new file mode 100644 index 00000000000..1510a306875 --- /dev/null +++ b/searchlib/src/vespa/searchlib/test/string_field_builder.cpp @@ -0,0 +1,140 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "string_field_builder.h" +#include "doc_builder.h" +#include <vespa/document/annotation/annotation.h> +#include <vespa/document/annotation/span.h> +#include <vespa/document/annotation/spanlist.h> +#include <vespa/document/annotation/spantree.h> +#include <vespa/document/fieldvalue/stringfieldvalue.h> +#include <vespa/fastlib/text/unicodeutil.h> +#include <vespa/vespalib/text/utf8.h> + +#include <cassert> + +using document::Annotation; +using document::AnnotationType; +using document::FixedTypeRepo; +using document::StringFieldValue; +using document::Span; +using document::SpanList; +using document::SpanNode; +using document::SpanTree; +using vespalib::Utf8Reader; +using vespalib::Utf8Writer; + +namespace search::test { + +namespace { + +const vespalib::string SPANTREE_NAME("linguistics"); + +} + +StringFieldBuilder::StringFieldBuilder(const DocBuilder& doc_builder) + : _value(), + _span_start(0u), + _span_list(nullptr), + _span_tree(), + _last_span(nullptr), + _url_mode(false), + _repo(doc_builder.get_repo(), doc_builder.get_document_type()) +{ +} + +StringFieldBuilder::~StringFieldBuilder() = default; + +void +StringFieldBuilder::start_annotate() +{ + auto span_list_up = std::make_unique<SpanList>(); + _span_list = span_list_up.get(); + _span_tree = std::make_unique<SpanTree>(SPANTREE_NAME, std::move(span_list_up)); +} + +void +StringFieldBuilder::add_span() +{ + assert(_value.size() > _span_start); + const SpanNode &span = _span_list->add(std::make_unique<Span>(_span_start, _value.size() - _span_start)); + _last_span = &span; + _span_start = _value.size(); +} + +StringFieldBuilder& +StringFieldBuilder::token(const vespalib::string& val, bool is_word) +{ + if (val.empty()) { + return *this; + } + if (!_span_tree) { + start_annotate(); + } + _span_start = _value.size(); + _value.append(val); + add_span(); + if (is_word) { + _span_tree->annotate(*_last_span, *AnnotationType::TERM); + } + return *this; +} + +StringFieldBuilder& +StringFieldBuilder::alt_word(const vespalib::string& val) +{ + assert(_last_span != nullptr); + _span_tree->annotate(*_last_span, + Annotation(*AnnotationType::TERM, + std::make_unique<StringFieldValue>(val))); + return *this; +} + +StringFieldBuilder& +StringFieldBuilder::tokenize(const vespalib::string& val) +{ + Utf8Reader reader(val); + vespalib::string token_buffer; + Utf8Writer writer(token_buffer); + uint32_t c = 0u; + bool old_word = false; + + while (reader.hasMore()) { + c = reader.getChar(); + bool new_word = Fast_UnicodeUtil::IsWordChar(c) || + (_url_mode && (c == '-' || c == '_')); + if (old_word != new_word) { + if (!token_buffer.empty()) { + token(token_buffer, old_word); + token_buffer.clear(); + } + old_word = new_word; + } + writer.putChar(c); + } + if (!token_buffer.empty()) { + token(token_buffer, old_word); + } + return *this; +} + + +document::StringFieldValue +StringFieldBuilder::build() +{ + StringFieldValue value(_value); + // Also drop all spans no annotation for now + if (_span_tree && _span_tree->numAnnotations() > 0u) { + StringFieldValue::SpanTrees trees; + trees.emplace_back(std::move(_span_tree)); + value.setSpanTrees(trees, _repo); + } else { + _span_tree.reset(); + } + _span_list = nullptr; + _last_span = nullptr; + _span_start = 0u; + _value.clear(); + return value; +} + +} diff --git a/searchlib/src/vespa/searchlib/test/string_field_builder.h b/searchlib/src/vespa/searchlib/test/string_field_builder.h new file mode 100644 index 00000000000..94c2bfc2fe8 --- /dev/null +++ b/searchlib/src/vespa/searchlib/test/string_field_builder.h @@ -0,0 +1,45 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/stllike/string.h> +#include <vespa/document/repo/fixedtyperepo.h> +#include <memory> + +namespace document { +class SpanList; +struct SpanNode; +class SpanTree; +class StringFieldValue; +} + +namespace search::test { + +class DocBuilder; + +/* + * Helper class to build annotated string field. + */ +class StringFieldBuilder { + vespalib::string _value; + size_t _span_start; + document::SpanList* _span_list; // owned by _span_tree + std::unique_ptr<document::SpanTree> _span_tree; + const document::SpanNode* _last_span; + bool _url_mode; + const document::FixedTypeRepo _repo; + void start_annotate(); + void add_span(); +public: + StringFieldBuilder(const DocBuilder& doc_builder); + ~StringFieldBuilder(); + StringFieldBuilder& url_mode(bool url_mode_) noexcept { _url_mode = url_mode_; return *this; } + StringFieldBuilder& token(const vespalib::string& val, bool is_word); + StringFieldBuilder& word(const vespalib::string& val) { return token(val, true); } + StringFieldBuilder& space() { return token(" ", false); } + StringFieldBuilder& tokenize(const vespalib::string& val); + StringFieldBuilder& alt_word(const vespalib::string& val); + document::StringFieldValue build(); +}; + +} diff --git a/searchsummary/src/tests/docsummary/annotation_converter/annotation_converter_test.cpp b/searchsummary/src/tests/docsummary/annotation_converter/annotation_converter_test.cpp index 753ae8d9044..17a7457711d 100644 --- a/searchsummary/src/tests/docsummary/annotation_converter/annotation_converter_test.cpp +++ b/searchsummary/src/tests/docsummary/annotation_converter/annotation_converter_test.cpp @@ -4,6 +4,7 @@ #include <vespa/document/annotation/span.h> #include <vespa/document/annotation/spanlist.h> #include <vespa/document/annotation/spantree.h> +#include <vespa/document/datatype/annotationtype.h> #include <vespa/document/fieldvalue/stringfieldvalue.h> #include <vespa/document/repo/configbuilder.h> #include <vespa/document/repo/fixedtyperepo.h> @@ -16,6 +17,7 @@ #include <vespa/vespalib/stllike/asciistream.h> using document::Annotation; +using document::AnnotationType; using document::DocumentType; using document::DocumentTypeRepo; using document::Span; @@ -25,7 +27,6 @@ using document::StringFieldValue; using search::docsummary::AnnotationConverter; using search::docsummary::IJuniperConverter; using search::linguistics::SPANTREE_NAME; -using search::linguistics::TERM; using vespalib::Slime; using vespalib::slime::SlimeInserter; @@ -95,9 +96,9 @@ AnnotationConverterTest::make_annotated_string() auto span_list_up = std::make_unique<SpanList>(); auto span_list = span_list_up.get(); auto tree = std::make_unique<SpanTree>(SPANTREE_NAME, std::move(span_list_up)); - tree->annotate(span_list->add(std::make_unique<Span>(0, 3)), *TERM); + tree->annotate(span_list->add(std::make_unique<Span>(0, 3)), *AnnotationType::TERM); tree->annotate(span_list->add(std::make_unique<Span>(4, 3)), - Annotation(*TERM, std::make_unique<StringFieldValue>("baz"))); + Annotation(*AnnotationType::TERM, std::make_unique<StringFieldValue>("baz"))); StringFieldValue value("foo bar"); set_span_tree(value, std::move(tree)); return value; @@ -110,8 +111,8 @@ AnnotationConverterTest::make_annotated_chinese_string() auto span_list = span_list_up.get(); auto tree = std::make_unique<SpanTree>(SPANTREE_NAME, std::move(span_list_up)); // These chinese characters each use 3 bytes in their UTF8 encoding. - tree->annotate(span_list->add(std::make_unique<Span>(0, 15)), *TERM); - tree->annotate(span_list->add(std::make_unique<Span>(15, 9)), *TERM); + tree->annotate(span_list->add(std::make_unique<Span>(0, 15)), *AnnotationType::TERM); + tree->annotate(span_list->add(std::make_unique<Span>(15, 9)), *AnnotationType::TERM); StringFieldValue value("我就是那个大灰狼"); set_span_tree(value, std::move(tree)); return value; diff --git a/searchsummary/src/vespa/searchsummary/docsummary/annotation_converter.cpp b/searchsummary/src/vespa/searchsummary/docsummary/annotation_converter.cpp index b36a2f8383e..f4594cba4f4 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/annotation_converter.cpp +++ b/searchsummary/src/vespa/searchsummary/docsummary/annotation_converter.cpp @@ -7,6 +7,7 @@ #include <vespa/document/annotation/annotation.h> #include <vespa/document/annotation/spantree.h> #include <vespa/document/annotation/spantreevisitor.h> +#include <vespa/document/datatype/annotationtype.h> #include <vespa/document/fieldvalue/stringfieldvalue.h> #include <vespa/juniper/juniper_separators.h> #include <vespa/vespalib/stllike/asciistream.h> @@ -15,6 +16,7 @@ using document::AlternateSpanList; using document::Annotation; +using document::AnnotationType; using document::FieldValue; using document::SimpleSpanList; using document::Span; @@ -139,7 +141,7 @@ AnnotationConverter::handleIndexingTerms(const StringFieldValue& value) // For now, skip any composite spans. const auto *span = dynamic_cast<const Span*>(annotation.getSpanNode()); if ((span != nullptr) && annotation.valid() && - (annotation.getType() == *linguistics::TERM)) { + (annotation.getType() == *AnnotationType::TERM)) { terms.push_back(std::make_pair(getSpan(*span), annotation.getFieldValue())); } diff --git a/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.cpp b/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.cpp index 8a847a90ddb..4b4cb2d9602 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.cpp +++ b/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.cpp @@ -1,27 +1,9 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "linguisticsannotation.h" -#include <vespa/document/datatype/primitivedatatype.h> - -using document::AnnotationType; -using document::DataType; -using document::PrimitiveDataType; -using vespalib::string; namespace search::linguistics { -namespace { -AnnotationType makeType(int id, string name, const DataType &type) { - AnnotationType annotation_type(id, name); - annotation_type.setDataType(type); - return annotation_type; -} - -const PrimitiveDataType STRING_OBJ(DataType::T_STRING); -AnnotationType TERM_OBJ(makeType(1, "term", STRING_OBJ)); -} // namespace - -const string SPANTREE_NAME("linguistics"); -const AnnotationType *const TERM(&TERM_OBJ); +const vespalib::string SPANTREE_NAME("linguistics"); } diff --git a/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.h b/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.h index d27c0da151f..e6395fe5b68 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.h +++ b/searchsummary/src/vespa/searchsummary/docsummary/linguisticsannotation.h @@ -2,11 +2,10 @@ #pragma once -#include <vespa/document/datatype/annotationtype.h> +#include <vespa/vespalib/stllike/string.h> namespace search::linguistics { extern const vespalib::string SPANTREE_NAME; -extern const document::AnnotationType *const TERM; } diff --git a/security-utils/pom.xml b/security-utils/pom.xml index 71920327fbb..a6f0040509c 100644 --- a/security-utils/pom.xml +++ b/security-utils/pom.xml @@ -24,7 +24,7 @@ <!-- compile scope --> <dependency> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>bcpkix-jdk18on</artifactId> <scope>compile</scope> </dependency> <dependency> diff --git a/security-utils/src/main/java/com/yahoo/security/HKDF.java b/security-utils/src/main/java/com/yahoo/security/HKDF.java new file mode 100644 index 00000000000..3aff89d71c2 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/HKDF.java @@ -0,0 +1,221 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Objects; + +/** + * Implementation of RFC-5869 HMAC-based Extract-and-Expand Key Derivation Function (HKDF). + * + * <p>The HKDF is initialized ("extracted") from a (non-secret) salt and a secret key. + * From this, any number of secret keys can be derived ("expanded") deterministically.</p> + * + * <p>When multiple keys are to be derived from the same initial keying/salting material, + * each separate key should use a distinct "context" in the {@link #expand(int, byte[])} + * call. This ensures that there exists a domain separation between the keys. + * Using the same context as another key on a HKDF initialized with the same salt+key + * results in the exact same derived key material as that key.</p> + * + * <p>This implementation only offers HMAC-SHA256-based key derivation.</p> + * + * @see <a href="https://tools.ietf.org/html/rfc5869">RFC-5869</a> + * @see <a href="https://en.wikipedia.org/wiki/HKDF">HKDF on Wikipedia</a> + * + * @author vekterli + */ +public class HKDF { + + private static final int HASH_LEN = 32; // Fixed output size of HMAC-SHA256. Corresponds to HashLen in the spec + private static final byte[] EMPTY_BYTES = new byte[0]; + private static final byte[] ALL_ZEROS_SALT = new byte[HASH_LEN]; + public static final int MAX_OUTPUT_SIZE = 255 * HASH_LEN; + + private final byte[] pseudoRandomKey; // Corresponds to "PRK" in spec + + private HKDF(byte[] pseudoRandomKey) { + this.pseudoRandomKey = pseudoRandomKey; + } + + private static Mac createHmacSha256() { + try { + return Mac.getInstance("HmacSHA256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private static SecretKeySpec hmacKeyFrom(byte[] rawKey) { + return new SecretKeySpec(rawKey, "HmacSHA256"); + } + + private static Mac createKeyedHmacSha256(byte[] rawKey) { + var hmac = createHmacSha256(); + try { + hmac.init(hmacKeyFrom(rawKey)); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } + return hmac; + } + + private static void validateExtractionParams(byte[] salt, byte[] ikm) { + Objects.requireNonNull(salt); + Objects.requireNonNull(ikm); + if (ikm.length == 0) { + throw new IllegalArgumentException("HKDF extraction IKM array can not be empty"); + } + if (salt.length == 0) { + throw new IllegalArgumentException("HKDF extraction salt array can not be empty"); + } + } + + /** + * Creates and returns a new HKDF instance extracted from the given salt and key. + * + * <p>Both the salt and input key value may be of arbitrary size, but it is recommended + * to have both be at least 16 bytes in size.</p> + * + * @param salt a non-secret salt value. Should ideally be high entropy and functionally + * "as if random". May not be empty, use {@link #unsaltedExtractedFrom(byte[])} + * if unsalted extraction is desired (though this is not recommended). + * @param ikm secret initial Input Keying Material value. + * @return a new HDFK instance ready for deriving keys based on the salt and IKM. + */ + public static HKDF extractedFrom(byte[] salt, byte[] ikm) { + validateExtractionParams(salt, ikm); + /* + RFC-5869, Step 2.2, Extract: + + HKDF-Extract(salt, IKM) -> PRK + + Options: + Hash a hash function; HashLen denotes the length of the + hash function output in octets + + Inputs: + salt optional salt value (a non-secret random value); + if not provided, it is set to a string of HashLen zeros. + IKM input keying material + + Output: + PRK a pseudorandom key (of HashLen octets) + + The output PRK is calculated as follows: + + PRK = HMAC-Hash(salt, IKM) + */ + var mac = createKeyedHmacSha256(salt); // Note: HDFK is initially keyed on the salt, _not_ on ikm! + mac.update(ikm); + return new HKDF(/*PRK = */ mac.doFinal()); + } + + /** + * Creates and returns a new <em>unsalted</em> HKDF instance extracted from the given key. + * + * <p>Prefer using the salted {@link #extractedFrom(byte[], byte[])} method if possible.</p> + * + * @param ikm secret initial Input Keying Material value. + * @return a new HDFK instance ready for deriving keys based on the IKM and an all-zero salt. + */ + public static HKDF unsaltedExtractedFrom(byte[] ikm) { + return extractedFrom(ALL_ZEROS_SALT, ikm); + } + + /** + * Derives a key with a given number of bytes for a particular context. The returned + * key is always deterministic for a given unique context and a HKDF initialized with + * a specific salt+IKM pair. + * + * <p>Thread safety: multiple threads can safely call <code>expand()</code> simultaneously + * on the same HKDF object.</p> + * + * @param wantedBytes Positive number of output bytes. Must be less than or equal to {@link #MAX_OUTPUT_SIZE} + * @param context Context for key derivation. Derivation is deterministic for a given context. + * Note: this maps to the "info" field in RFC-5869. + * @return A byte buffer of size wantedBytes filled with derived key material + */ + public byte[] expand(int wantedBytes, byte[] context) { + Objects.requireNonNull(context); + verifyWantedBytesWithinBounds(wantedBytes); + return expandImpl(wantedBytes, context); + } + + /** + * Derives a key with a given number of bytes. The returned key is always deterministic + * for a HKDF initialized with a specific salt+IKM pair. + * + * <p>If more than one key is to be derived, use {@link #expand(int, byte[])}</p> + * + * <p>Thread safety: multiple threads can safely call <code>expand()</code> simultaneously + * on the same HKDF object.</p> + * + * @param wantedBytes Positive number of output bytes. Must be less than or equal to {@link #MAX_OUTPUT_SIZE} + * @return A byte buffer of size wantedBytes filled with derived key material + */ + public byte[] expand(int wantedBytes) { + return expand(wantedBytes, EMPTY_BYTES); + } + + private void verifyWantedBytesWithinBounds(int wantedBytes) { + if (wantedBytes <= 0) { + throw new IllegalArgumentException("Requested negative or zero number of HKDF output bytes"); + } + if (wantedBytes > MAX_OUTPUT_SIZE) { + throw new IllegalArgumentException("Too many requested HKDF output bytes (max %d, got %d)" + .formatted(MAX_OUTPUT_SIZE, wantedBytes)); + } + } + + private byte[] expandImpl(int wantedBytes, byte[] context) { + /* + RFC-5869, Step 2.3, Expand: + + HKDF-Expand(PRK, info, L) -> OKM + + Inputs: + PRK a pseudorandom key of at least HashLen octets + (usually, the output from the extract step) + info optional context and application specific information + (can be a zero-length string) + L length of output keying material in octets + (<= 255*HashLen) + + Output: + OKM output keying material (of L octets) + + The output OKM is calculated as follows: + + N = ceil(L/HashLen) + T = T(1) | T(2) | T(3) | ... | T(N) + OKM = first L octets of T + + where: + T(0) = empty string (zero length) + T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) + T(2) = HMAC-Hash(PRK, T(1) | info | 0x02) + T(3) = HMAC-Hash(PRK, T(2) | info | 0x03) + ... + */ + var prkHmac = createKeyedHmacSha256(pseudoRandomKey); + int blocks = (wantedBytes / HASH_LEN) + ((wantedBytes % HASH_LEN) != 0 ? 1 : 0); // N + var buffer = ByteBuffer.allocate(blocks * HASH_LEN); // T + byte[] lastBlock = EMPTY_BYTES; // initially T(0) + for (int i = 0; i < blocks; ++i) { + prkHmac.update(lastBlock); + prkHmac.update(context); + prkHmac.update((byte)(i + 1)); // Number of blocks shall never exceed 255 + // HMAC instance can be reused across doFinal() calls; resets back to initially keyed state. + lastBlock = prkHmac.doFinal(); + buffer.put(lastBlock); + } + buffer.flip(); + byte[] outputKeyingMaterial = new byte[wantedBytes]; // OKM + buffer.get(outputKeyingMaterial); + return outputKeyingMaterial; + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java new file mode 100644 index 00000000000..237c4976c7c --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/SealedSharedKey.java @@ -0,0 +1,73 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import java.nio.ByteBuffer; +import java.util.Base64; + +/** + * A SealedSharedKey represents the public part of a secure one-way ephemeral key exchange. + * + * It is "sealed" in the sense that it is expected to be computationally infeasible + * for anyone to derive the correct shared key from the sealed key without holding + * the correct private key. + * + * A SealedSharedKey can be converted to--and from--an opaque string token representation. + * This token representation is expected to be used as a convenient serialization + * form when communicating shared keys. + */ +public record SealedSharedKey(int keyId, byte[] eciesPayload, byte[] iv) { + + /** Current encoding version of opaque sealed key tokens. Must be less than 256. */ + public static final int CURRENT_TOKEN_VERSION = 1; + + private static final int ECIES_AES_IV_LENGTH = SharedKeyGenerator.ECIES_AES_CBC_IV_BITS / 8; + + /** + * Creates an opaque URL-safe string token that contains enough information to losslessly + * reconstruct the SealedSharedKey instance when passed verbatim to fromTokenString(). + */ + public String toTokenString() { + if (keyId >= (1 << 24)) { + throw new IllegalArgumentException("Key id is too large to be encoded"); + } + if (iv.length != ECIES_AES_IV_LENGTH) { + throw new IllegalStateException("Expected a %d byte IV, got %d bytes".formatted(ECIES_AES_IV_LENGTH, iv.length)); + } + + ByteBuffer encoded = ByteBuffer.allocate(4 + ECIES_AES_IV_LENGTH + eciesPayload.length); + encoded.putInt((CURRENT_TOKEN_VERSION << 24) | keyId); + encoded.put(iv); + encoded.put(eciesPayload); + encoded.flip(); + + byte[] encBytes = new byte[encoded.remaining()]; + encoded.get(encBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(encBytes); + } + + /** + * Attempts to unwrap a SealedSharedKey opaque token representation that was previously + * created by a call to toTokenString(). + */ + public static SealedSharedKey fromTokenString(String tokenString) { + byte[] rawTokenBytes = Base64.getUrlDecoder().decode(tokenString); + if (rawTokenBytes.length < 4) { + throw new IllegalArgumentException("Decoded token too small to contain a header"); + } + ByteBuffer decoded = ByteBuffer.wrap(rawTokenBytes); + int versionAndKeyId = decoded.getInt(); + int version = versionAndKeyId >>> 24; + if (version != CURRENT_TOKEN_VERSION) { + throw new IllegalArgumentException("Token had unexpected version. Expected %d, was %d" + .formatted(CURRENT_TOKEN_VERSION, version)); + } + byte[] iv = new byte[ECIES_AES_IV_LENGTH]; + decoded.get(iv); + byte[] eciesPayload = new byte[decoded.remaining()]; + decoded.get(eciesPayload); + + int keyId = versionAndKeyId & 0xffffff; + return new SealedSharedKey(keyId, eciesPayload, iv); + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/SecretSharedKey.java b/security-utils/src/main/java/com/yahoo/security/SecretSharedKey.java new file mode 100644 index 00000000000..3e90711d57f --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/SecretSharedKey.java @@ -0,0 +1,24 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import javax.crypto.SecretKey; + +/** + * A SecretSharedKey represents a pairing of both the secret and public parts of + * a secure one-way ephemeral key exchange. + * + * The underlying SealedSharedKey may be made public, generally as a token. + * + * It should not come as a surprise that the underlying SecretKey must NOT be + * made public. + */ +public record SecretSharedKey(SecretKey secretKey, SealedSharedKey sealedSharedKey) { + + // Explicitly override toString to ensure we can't leak any SecretKey contents + // via an implicitly generated method. Only print the sealed key (which is entirely public). + @Override + public String toString() { + return "SharedSecretKey(sealed: %s)".formatted(sealedSharedKey.toTokenString()); + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java new file mode 100644 index 00000000000..07e8243ec09 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java @@ -0,0 +1,129 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import org.bouncycastle.jcajce.provider.util.BadBlockException; +import org.bouncycastle.jce.spec.IESParameterSpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; + +/** + * Implements both the sender and receiver sides of a secure, anonymous one-way + * key generation and exchange protocol implemented using ECIES; a hybrid crypto + * scheme built around elliptic curves. + * + * A shared key, once generated, may have its sealed component sent over a public + * channel without revealing anything about the underlying secret key. Only a + * recipient holding the private key corresponding to the public used for shared + * key creation may derive the same secret key as the sender. + * + * Every generated key is globally unique (with extremely high probability). + * + * The secret key is intended to be used <em>only once</em>. It MUST NOT be used to + * produce more than a single ciphertext. Using the secret key to produce multiple + * ciphertexts completely breaks the security model due to using a fixed Initialization + * Vector (IV). + */ +public class SharedKeyGenerator { + + private static final int AES_GCM_KEY_BITS = 256; + private static final int AES_GCM_AUTH_TAG_BITS = 128; + private static final String AES_GCM_ALGO_SPEC = "AES/GCM/NoPadding"; + private static final String ECIES_CIPHER_NAME = "ECIESwithSHA256andAES-CBC"; + protected static final int ECIES_AES_CBC_IV_BITS = 128; + private static final int ECIES_HMAC_BITS = 256; + private static final int ECIES_AES_KEY_BITS = 256; + private static final SecureRandom SHARED_CSPRNG = new SecureRandom(); + + public static SecretSharedKey generateForReceiverPublicKey(PublicKey receiverPublicKey, int keyId) { + try { + var keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(AES_GCM_KEY_BITS, SHARED_CSPRNG); + var secretKey = keyGen.generateKey(); + + var cipher = Cipher.getInstance(ECIES_CIPHER_NAME, BouncyCastleProviderHolder.getInstance()); + byte[] iv = new byte[ECIES_AES_CBC_IV_BITS / 8]; + SHARED_CSPRNG.nextBytes(iv); + var iesParamSpec = new IESParameterSpec(null, null, ECIES_HMAC_BITS, ECIES_AES_KEY_BITS, iv); + + cipher.init(Cipher.ENCRYPT_MODE, receiverPublicKey, iesParamSpec); + byte[] eciesPayload = cipher.doFinal(secretKey.getEncoded()); + + var sealedSharedKey = new SealedSharedKey(keyId, eciesPayload, iv); + return new SecretSharedKey(secretKey, sealedSharedKey); + } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException + | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + public static SecretSharedKey fromSealedKey(SealedSharedKey sealedKey, PrivateKey receiverPrivateKey) { + try { + var cipher = Cipher.getInstance(ECIES_CIPHER_NAME, BouncyCastleProviderHolder.getInstance()); + var iesParamSpec = new IESParameterSpec(null, null, ECIES_HMAC_BITS, ECIES_AES_KEY_BITS, sealedKey.iv()); + cipher.init(Cipher.DECRYPT_MODE, receiverPrivateKey, iesParamSpec); + byte[] secretKey = cipher.doFinal(sealedKey.eciesPayload()); + + return new SecretSharedKey(new SecretKeySpec(secretKey, "AES"), sealedKey); + } catch (BadBlockException e) { + throw new IllegalArgumentException("Token integrity check failed; token is either corrupt or was " + + "generated for a different public key"); + } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException + | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + // A given key+IV pair can only be used for one single encryption session, ever. + // Since our keys are intended to be inherently single-use, we can satisfy that + // requirement even with a fixed IV. This avoids the need for explicitly including + // the IV with the token, and also avoids tying the encryption to a particular + // token recipient (which would be the case if the IV were deterministically derived + // from the recipient key and ephemeral ECDH public key), as that would preclude + // support for delegated key forwarding. + private static byte[] fixed96BitIvForSingleUseKey() { + // Nothing up my sleeve! + return new byte[] { 'h', 'e', 'r', 'e', 'B', 'd', 'r', 'a', 'g', 'o', 'n', 's' }; + } + + private static Cipher makeAesGcmCipher(SecretSharedKey secretSharedKey, int cipherMode) { + try { + var cipher = Cipher.getInstance(AES_GCM_ALGO_SPEC); + var gcmSpec = new GCMParameterSpec(AES_GCM_AUTH_TAG_BITS, fixed96BitIvForSingleUseKey()); + cipher.init(cipherMode, secretSharedKey.secretKey(), gcmSpec); + return cipher; + } catch (NoSuchAlgorithmException | NoSuchPaddingException + | InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + /** + * Creates an AES-GCM Cipher that can be used to encrypt arbitrary plaintext. + * + * The given secret key MUST NOT be used to encrypt more than one plaintext. + */ + public static Cipher makeAesGcmEncryptionCipher(SecretSharedKey secretSharedKey) { + return makeAesGcmCipher(secretSharedKey, Cipher.ENCRYPT_MODE); + } + + /** + * Creates an AES-GCM Cipher that can be used to decrypt ciphertext that was previously + * encrypted with the given secret key. + */ + public static Cipher makeAesGcmDecryptionCipher(SecretSharedKey secretSharedKey) { + return makeAesGcmCipher(secretSharedKey, Cipher.DECRYPT_MODE); + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/SideChannelSafe.java b/security-utils/src/main/java/com/yahoo/security/SideChannelSafe.java new file mode 100644 index 00000000000..1f160d94c6a --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/SideChannelSafe.java @@ -0,0 +1,54 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +/** + * Utility functions for comparing the contents of arrays without leaking information about the + * data contained within them via timing side-channels. This is done by avoiding any branches + * that depend on the array elements themselves. This inherently means that all operations have + * both an upper and a lower bound in processing time that is O(n) for an array of size n, as there + * can be no early exits. + * + * @author vekterli + */ +public class SideChannelSafe { + + /** + * @return true iff all bytes in the array are zero. An empty array always returns false + * since it technically can't contain any zeros at all. + */ + public static boolean allZeros(byte[] buf) { + if (buf.length == 0) { + return false; + } + byte accu = 0; + for (byte b : buf) { + accu |= b; + } + return (accu == 0); + } + + /** + * Compare two byte arrays without the use of data-dependent branching that may leak information + * about the contents of either of the arrays. + * + * <strong>Important:</strong> the <em>length</em> of the arrays is not considered secret, and + * will be leaked if arrays of differing sizes are given. + * + * @param lhs first array of bytes to compare + * @param rhs second array of bytes to compare + * @return true iff both arrays have the same size and are element-wise identical + */ + public static boolean arraysEqual(byte[] lhs, byte[] rhs) { + if (lhs.length != rhs.length) { + return false; + } + // Only use constant time bitwise ops. `accu` will be non-zero if at least one bit + // differed in any byte compared between the two arrays. + byte accu = 0; + for (int i = 0; i < lhs.length; ++i) { + accu |= (lhs[i] ^ rhs[i]); + } + return (accu == 0); + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java b/security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java index c01de58987c..e184d982790 100644 --- a/security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java +++ b/security-utils/src/main/java/com/yahoo/security/SubjectAlternativeName.java @@ -2,7 +2,7 @@ package com.yahoo.security; import org.bouncycastle.asn1.ASN1Encodable; -import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.ASN1IA5String; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.GeneralName; @@ -60,7 +60,7 @@ public class SubjectAlternativeName { case GeneralName.rfc822Name: case GeneralName.dNSName: case GeneralName.uniformResourceIdentifier: - return DERIA5String.getInstance(name).getString(); + return ASN1IA5String.getInstance(name).getString(); case GeneralName.directoryName: return X500Name.getInstance(name).toString(); case GeneralName.iPAddress: diff --git a/security-utils/src/test/java/com/yahoo/security/HKDFTest.java b/security-utils/src/test/java/com/yahoo/security/HKDFTest.java new file mode 100644 index 00000000000..bf000cbf8d2 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/HKDFTest.java @@ -0,0 +1,298 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * HKDF tests that ensure that the output of our own implementation matches the test + * vectors given in <a href="https://tools.ietf.org/html/rfc5869">RFC-5869</a>. + * + * We don't expose the internal PRK (pseudo-random key) value of the HKDF itself, + * so we don't test it explicitly. The actual OKM (output keying material) inherently + * depends on it, so its correctness is verified transitively. + * + * @author vekterli + */ +public class HKDFTest { + + private static byte[] fromHex(String hex) { + return Hex.decode(hex); + } + + private static String toHex(byte[] bytes) { + return Hex.toHexString(bytes); + } + + private static byte[] sha256DigestOf(byte[]... buffers) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + for (byte[] buf : buffers) { + digest.update(buf); + } + return digest.digest(); + } catch (NoSuchAlgorithmException e) { + // SHA-256 should always be present, so this should never be reached in practice + throw new RuntimeException(e); + } + } + + /* + A.1. Test Case 1 + + Basic test case with SHA-256 + + Hash = SHA-256 + IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 octets) + salt = 0x000102030405060708090a0b0c (13 octets) + info = 0xf0f1f2f3f4f5f6f7f8f9 (10 octets) + L = 42 + + PRK = 0x077709362c2e32df0ddc3f0dc47bba63 + 90b6c73bb50f9c3122ec844ad7c2b3e5 (32 octets) + OKM = 0x3cb25f25faacd57a90434f64d0362f2a + 2d2d0a90cf1a5a4c5db02d56ecc4c5bf + 34007208d5b887185865 (42 octets) + */ + @Test + void rfc_5869_test_vector_case_1() { + var ikm = fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + var salt = fromHex("000102030405060708090a0b0c"); + var info = fromHex("f0f1f2f3f4f5f6f7f8f9"); + + var hkdf = HKDF.extractedFrom(salt, ikm); + var okm = hkdf.expand(42, info); + assertEquals("3cb25f25faacd57a90434f64d0362f2a" + + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + + "34007208d5b887185865", + toHex(okm)); + } + + @Test + void rfc_5869_test_vector_case_1_block_boundary_edge_cases() { + var ikm = fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + var salt = fromHex("000102030405060708090a0b0c"); + var info = fromHex("f0f1f2f3f4f5f6f7f8f9"); + + var hkdf = HKDF.extractedFrom(salt, ikm); + var okm = hkdf.expand(31, info); // One less than block size + assertEquals("3cb25f25faacd57a90434f64d0362f2a" + + "2d2d0a90cf1a5a4c5db02d56ecc4c5", + toHex(okm)); + + okm = hkdf.expand(32, info); // Exactly equal to block size + assertEquals("3cb25f25faacd57a90434f64d0362f2a" + + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf", + toHex(okm)); + + okm = hkdf.expand(33, info); // One more than block size + assertEquals("3cb25f25faacd57a90434f64d0362f2a" + + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + + "34", + toHex(okm)); + } + + /* + A.2. Test Case 2 + + Test with SHA-256 and longer inputs/outputs + + Hash = SHA-256 + IKM = 0x000102030405060708090a0b0c0d0e0f + 101112131415161718191a1b1c1d1e1f + 202122232425262728292a2b2c2d2e2f + 303132333435363738393a3b3c3d3e3f + 404142434445464748494a4b4c4d4e4f (80 octets) + salt = 0x606162636465666768696a6b6c6d6e6f + 707172737475767778797a7b7c7d7e7f + 808182838485868788898a8b8c8d8e8f + 909192939495969798999a9b9c9d9e9f + a0a1a2a3a4a5a6a7a8a9aaabacadaeaf (80 octets) + info = 0xb0b1b2b3b4b5b6b7b8b9babbbcbdbebf + c0c1c2c3c4c5c6c7c8c9cacbcccdcecf + d0d1d2d3d4d5d6d7d8d9dadbdcdddedf + e0e1e2e3e4e5e6e7e8e9eaebecedeeef + f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff (80 octets) + L = 82 + + PRK = 0x06a6b88c5853361a06104c9ceb35b45c + ef760014904671014a193f40c15fc244 (32 octets) + OKM = 0xb11e398dc80327a1c8e7f78c596a4934 + 4f012eda2d4efad8a050cc4c19afa97c + 59045a99cac7827271cb41c65e590e09 + da3275600c2f09b8367793a9aca3db71 + cc30c58179ec3e87c14c01d5c1f3434f + 1d87 (82 octets) + */ + @Test + void rfc_5869_test_vector_case_2() { + var ikm = fromHex("000102030405060708090a0b0c0d0e0f" + + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e2f" + + "303132333435363738393a3b3c3d3e3f" + + "404142434445464748494a4b4c4d4e4f"); + var salt = fromHex("606162636465666768696a6b6c6d6e6f" + + "707172737475767778797a7b7c7d7e7f" + + "808182838485868788898a8b8c8d8e8f" + + "909192939495969798999a9b9c9d9e9f" + + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"); + var info = fromHex("b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" + + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" + + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + + var hkdf = HKDF.extractedFrom(salt, ikm); + var okm = hkdf.expand(82, info); + assertEquals("b11e398dc80327a1c8e7f78c596a4934" + + "4f012eda2d4efad8a050cc4c19afa97c" + + "59045a99cac7827271cb41c65e590e09" + + "da3275600c2f09b8367793a9aca3db71" + + "cc30c58179ec3e87c14c01d5c1f3434f" + + "1d87", + toHex(okm)); + } + + /* + A.3. Test Case 3 + + Test with SHA-256 and zero-length salt/info + + Hash = SHA-256 + IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 octets) + salt = (0 octets) + info = (0 octets) + L = 42 + + PRK = 0x19ef24a32c717b167f33a91d6f648bdf + 96596776afdb6377ac434c1c293ccb04 (32 octets) + OKM = 0x8da4e775a563c18f715f802a063c5a31 + b8a11f5c5ee1879ec3454e5f3c738d2d + 9d201395faa4b61a96c8 (42 octets) + */ + @Test + void rfc_5869_test_vector_case_3() { + var ikm = fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + var info = new byte[0]; + + // We don't allow empty salt to the salted factory function, so this is equivalent. + var hkdf = HKDF.unsaltedExtractedFrom(ikm); + var okm = hkdf.expand(42, info); + var expectedOkm = "8da4e775a563c18f715f802a063c5a31" + + "b8a11f5c5ee1879ec3454e5f3c738d2d" + + "9d201395faa4b61a96c8"; + assertEquals(expectedOkm, toHex(okm)); + + // expand() without explicit context should return as if an empty context array was passed + okm = hkdf.expand(42); + assertEquals(expectedOkm, toHex(okm)); + } + + @Test + void requested_key_size_is_bounded_and_checked() { + var ikm = fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + var salt = fromHex("000102030405060708090a0b0c"); + var hkdf = HKDF.extractedFrom(salt, ikm); + + assertThrows(IllegalArgumentException.class, () -> hkdf.expand(-1)); // Can't request negative output size + + assertThrows(IllegalArgumentException.class, () -> hkdf.expand(0)); // Need at least 1 key byte + + assertThrows(IllegalArgumentException.class, () -> hkdf.expand(HKDF.MAX_OUTPUT_SIZE + 1)); // 1 too large + } + + @Test + void missing_salt_to_salted_factory_function_throws_exception() { + var ikm = fromHex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); + assertThrows(NullPointerException.class, () -> HKDF.extractedFrom(null, ikm)); + assertThrows(IllegalArgumentException.class, () -> HKDF.extractedFrom(new byte[0], ikm)); + } + + @Test + void ikm_can_not_be_null_or_empty() { + var salt = fromHex("000102030405060708090a0b0c"); + assertThrows(NullPointerException.class, () -> HKDF.extractedFrom(salt, null)); + assertThrows(IllegalArgumentException.class, () -> HKDF.extractedFrom(salt, new byte[0])); + assertThrows(NullPointerException.class, () -> HKDF.unsaltedExtractedFrom(null)); + assertThrows(IllegalArgumentException.class, () -> HKDF.unsaltedExtractedFrom(new byte[0])); + } + + // + // Subset of Wycheproof test vectors for specific named edge cases + // From https://github.com/google/wycheproof/blob/master/testvectors/hkdf_sha256_test.json + // + + @Test + void maximal_output_size() { + var ikm = fromHex("bdd9c30b5fab7f22d859db774779b41cc124daf3ce872f6e80951c0edd8f8214"); + var salt = fromHex("90983ed74912c6173d0f7cf8164b525361b89bda04d085341a057bde9083b5af"); + var info = fromHex("e6483e923d37e4ba"); + + var hkdf = HKDF.extractedFrom(salt, ikm); + assertEquals(8160, HKDF.MAX_OUTPUT_SIZE); + var okm = hkdf.expand(HKDF.MAX_OUTPUT_SIZE, info); + // To avoid shoving an 8K sized hex string into the source code, check against the pre-hashed + // value of the expected OKM output. It's hashes all the way down! + var expectedOkmSha256Digest = "c17ce0403e133570191dd1d2ca46f6b62623d62e4f0def8de23a51d65d40a009"; + var okmDigest = sha256DigestOf(okm); + assertEquals(expectedOkmSha256Digest, toHex(okmDigest)); + } + + @Test + void output_collision_for_different_salts() { + var ikm = fromHex("5943c65bc33bf05a205b04be8ae0ab2e"); + var info = fromHex("be082f301a03f87787a80fbea88941214d50c42b"); + var hkdf = HKDF.unsaltedExtractedFrom(ikm); + + var okm = hkdf.expand(32, info); + var expectedOkm = "e7f384df2eae32addabd068a758dec84ed7fcfd87a5fcceb37b70c51422d7387"; + assertEquals(expectedOkm, toHex(okm)); + + var salt = fromHex("0000000000000000000000000000000000000000000000000000000000000000"); + hkdf = HKDF.extractedFrom(salt, ikm); + okm = hkdf.expand(32, info); + assertEquals(expectedOkm, toHex(okm)); + } + + @Test + void salt_longer_than_block_size_is_equivalent_to_hash_of_the_salt() { + var ikm = fromHex("624a5b59c2be55cbe29ea90c0020a7e8c60f2501"); + var info = fromHex("5447e595250d02165aae3e61fa90313e25509a7b"); + var salts = List.of("c737d7278df1ec7c0a549ce964abd51c3df1d3584d49e77208cd3f9f5bbfb32e", + "1a08959149f4b073bcd902c9bc4ed0324c21c95590773afc77037d610b9584806aeeeda8b5" + + "d588d0cd79e7c12211b8e394067516ce12946d61111a52042b539353"); + var expectedOkm = "d45c3909269f4b5f9de1fb2eeb0593a7cb9175c8835aba37e0ee0c4cb3bd87c4"; + for (var salt : salts) { + var hkdf = HKDF.extractedFrom(fromHex(salt), ikm); + var okm = hkdf.expand(32, info); + assertEquals(expectedOkm, toHex(okm)); + } + } + + @Test + void salt_shorter_than_the_block_size_is_padded_with_zeros() { + var ikm = fromHex("5943c65bc33bf05a205b04be8ae0ab2e"); + var info = fromHex("be082f301a03f87787a80fbea88941214d50c42b"); + var expectedOkm = "43e371354001617abb70454751059625ef1a64e0f818469c2f886b27140a0166"; + var salts = List.of("e69dcaad55fb0536", + "e69dcaad55fb05360000000000000000", + "e69dcaad55fb053600000000000000000000000000000000", + "e69dcaad55fb0536000000000000000000000000000000000000000000000000", + "e69dcaad55fb05360000000000000000000000000000000000000000000000000000000000000000", + "e69dcaad55fb053600000000000000000000000000000000000000000000000000000000000000000000000000000000", + "e69dcaad55fb0536000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + for (var salt : salts) { + var hkdf = HKDF.extractedFrom(fromHex(salt), ikm); + var okm = hkdf.expand(32, info); + assertEquals(expectedOkm, toHex(okm), "Failed for salt %s".formatted(salt)); + } + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java new file mode 100644 index 00000000000..26a506015c5 --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java @@ -0,0 +1,136 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.Test; + +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SharedKeyTest { + + @Test + void generated_secret_key_is_256_bit_aes() { + var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var shared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1); + var secret = shared.secretKey(); + assertEquals(secret.getAlgorithm(), "AES"); + assertEquals(secret.getEncoded().length, 32); + } + + @Test + void sealed_shared_key_can_be_exchanged_via_token_and_computes_identical_secret_key_at_receiver() { + var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1); + var publicToken = myShared.sealedSharedKey().toTokenString(); + + var theirSealed = SealedSharedKey.fromTokenString(publicToken); + var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverKeyPair.getPrivate()); + + assertArrayEquals(myShared.secretKey().getEncoded(), theirShared.secretKey().getEncoded()); + } + + @Test + void token_v1_representation_is_stable() { + var receiverPrivate = KeyUtils.fromPemEncodedPrivateKey( + """ + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIO+CkAccoU9jPjX64mwU54Ar9DNZSLBBTYRSINerSW8EoAoGCCqGSM49 + AwEHoUQDQgAE3FA2VSuOn0vVhtQgNe13H2UE0Vx5A41demyX8nkHTCO4BDXSEPca + vejY7YaVcNSvFUbzDvia51X4pxbr1pe56g== + -----END EC PRIVATE KEY----- + """); + var receiverPublic = KeyUtils.fromPemEncodedPublicKey( + """ + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3FA2VSuOn0vVhtQgNe13H2UE0Vx5 + A41demyX8nkHTCO4BDXSEPcavejY7YaVcNSvFUbzDvia51X4pxbr1pe56g== + -----END PUBLIC KEY----- + """ + ); + var receiverKeyPair = new KeyPair(receiverPublic, receiverPrivate); + + // Token generated for the above receiver public key, with the below expected shared secret (in hex) + var publicToken = "AQAAAUfvuJpugUV3knQXwyP7afgEpDXT4JxaF-x7Ykirty2iwUqJv5UsGx78is5Vu4Mdln_mOVbAUv4dj" + + "da7hvzKYNC3IpSMjFrTQ8ab-bEkMpc5tjss_Z7DaJzY4fUlw31Lhx39BMB5yQX0pVLMdFGp5F-_8z8CE" + + "-7d9lkCDP9hPKiD77besjrBt_mEBadCd4oNONqc6zzhuQj4O5T9k_RC5VRV"; + var expectedSharedSecret = "64e01295e736cb827e86cf0281385d5a0dcca217ec1b59f6609a06e2e9debf78"; + + var theirSealed = SealedSharedKey.fromTokenString(publicToken); + var theirShared = SharedKeyGenerator.fromSealedKey(theirSealed, receiverKeyPair.getPrivate()); + + assertEquals(expectedSharedSecret, Hex.toHexString(theirShared.secretKey().getEncoded())); + } + + @Test + void unrelated_private_key_cannot_decrypt_shared_secret_key() { + var aliceKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var eveKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var bobShared = SharedKeyGenerator.generateForReceiverPublicKey(aliceKeyPair.getPublic(), 1); + assertThrows(IllegalArgumentException.class, // TODO consider distinct exception class + () -> SharedKeyGenerator.fromSealedKey(bobShared.sealedSharedKey(), eveKeyPair.getPrivate())); + } + + @Test + void token_carries_key_id_as_metadata() { + int keyId = 12345; + var keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(keyPair.getPublic(), keyId); + var publicToken = myShared.sealedSharedKey().toTokenString(); + var theirShared = SealedSharedKey.fromTokenString(publicToken); + assertEquals(theirShared.keyId(), keyId); + } + + static byte[] streamEncryptString(String data, SecretSharedKey secretSharedKey) throws IOException { + var cipher = SharedKeyGenerator.makeAesGcmEncryptionCipher(secretSharedKey); + var outStream = new ByteArrayOutputStream(); + try (var cipherStream = new CipherOutputStream(outStream, cipher)) { + cipherStream.write(data.getBytes(StandardCharsets.UTF_8)); + cipherStream.flush(); + } + return outStream.toByteArray(); + } + + static String streamDecryptString(byte[] encrypted, SecretSharedKey secretSharedKey) throws IOException { + var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(secretSharedKey); + var inStream = new ByteArrayInputStream(encrypted); + var total = ByteBuffer.allocate(encrypted.length); // Assume decrypted form can't be _longer_ + byte[] tmp = new byte[8]; // short buf to test chunking + try (var cipherStream = new CipherInputStream(inStream, cipher)) { + while (true) { + int read = cipherStream.read(tmp); + if (read == -1) { + break; + } + total.put(tmp, 0, read); + } + } + total.flip(); + byte[] strBytes = new byte[total.remaining()]; + total.get(strBytes); + return new String(strBytes, StandardCharsets.UTF_8); + } + + @Test + void can_create_symmetric_ciphers_from_shared_secret_key_and_public_keys() throws Exception { + var receiverKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC); + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), 1); + + String terrifyingSecret = "birds are not real D:"; + byte[] encrypted = streamEncryptString(terrifyingSecret, myShared); + String decrypted = streamDecryptString(encrypted, myShared); + assertEquals(terrifyingSecret, decrypted); + } + +} diff --git a/security-utils/src/test/java/com/yahoo/security/SideChannelSafeTest.java b/security-utils/src/test/java/com/yahoo/security/SideChannelSafeTest.java new file mode 100644 index 00000000000..7a66ed6eb7f --- /dev/null +++ b/security-utils/src/test/java/com/yahoo/security/SideChannelSafeTest.java @@ -0,0 +1,38 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * _Functional_ test of side channel safe utility functions. Testing that they're actually + * (probably) side channel safe would be too flaky since it's inherently timing-dependent. + */ +public class SideChannelSafeTest { + + @Test + void all_zeros_checks_length_and_array_contents() { + assertFalse(SideChannelSafe.allZeros(new byte[0])); + assertFalse(SideChannelSafe.allZeros(new byte[]{ 1 })); + assertTrue(SideChannelSafe.allZeros(new byte[]{ 0 })); + assertFalse(SideChannelSafe.allZeros(new byte[]{ 0, 0, 127, 0 })); + assertFalse(SideChannelSafe.allZeros(new byte[]{ 0, 0, -1, 0 })); + assertTrue(SideChannelSafe.allZeros(new byte[]{ 0, 0, 0 })); + } + + @Test + void arrays_equal_checks_length_and_array_contents() { + assertTrue(SideChannelSafe.arraysEqual(new byte[0], new byte[0])); + assertFalse(SideChannelSafe.arraysEqual(new byte[] { 0 }, new byte[0])); + assertFalse(SideChannelSafe.arraysEqual(new byte[0], new byte[]{ 0 })); + assertTrue(SideChannelSafe.arraysEqual(new byte[] { 0, 0, 0 }, new byte[] { 0, 0, 0 })); + assertTrue(SideChannelSafe.arraysEqual(new byte[] { 0x7, 0xe }, new byte[] { 0x7, 0xe })); + assertFalse(SideChannelSafe.arraysEqual(new byte[] { 0xe, 0x7 }, new byte[] { 0x7, 0xe })); + assertFalse(SideChannelSafe.arraysEqual(new byte[] { -1, 127 }, new byte[] { 127, -1 })); + assertFalse(SideChannelSafe.arraysEqual(new byte[] { -1, -1, 1 }, new byte[] { -1, -1, 2 })); + assertFalse(SideChannelSafe.arraysEqual(new byte[] { 0, -1, 1 }, new byte[] { 0, -1, 3 })); + } + +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/health/ApplicationHealthMonitor.java b/service-monitor/src/main/java/com/yahoo/vespa/service/health/ApplicationHealthMonitor.java index a209b4111ed..b7bf023eebd 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/health/ApplicationHealthMonitor.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/health/ApplicationHealthMonitor.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; * @author hakon */ class ApplicationHealthMonitor implements ServiceStatusProvider, AutoCloseable { + private final ApplicationId applicationId; private final StateV1HealthModel healthModel; @@ -86,4 +87,5 @@ class ApplicationHealthMonitor implements ServiceStatusProvider, AutoCloseable { monitors.clear(); } } + } diff --git a/storage/src/vespa/storage/bucketdb/generic_btree_bucket_database.hpp b/storage/src/vespa/storage/bucketdb/generic_btree_bucket_database.hpp index 9db36e96fc0..125882f7fe7 100644 --- a/storage/src/vespa/storage/bucketdb/generic_btree_bucket_database.hpp +++ b/storage/src/vespa/storage/bucketdb/generic_btree_bucket_database.hpp @@ -38,14 +38,14 @@ void GenericBTreeBucketDatabase<DataStoreTraitsT>::commit_tree_changes() { _tree.getAllocator().freeze(); auto current_gen = _generation_handler.getCurrentGeneration(); - _store.transferHoldLists(current_gen); - _tree.getAllocator().transferHoldLists(current_gen); + _store.assign_generation(current_gen); + _tree.getAllocator().assign_generation(current_gen); _generation_handler.incGeneration(); - auto used_gen = _generation_handler.getFirstUsedGeneration(); - _store.trimHoldLists(used_gen); - _tree.getAllocator().trimHoldLists(used_gen); + auto used_gen = _generation_handler.get_oldest_used_generation(); + _store.reclaim_memory(used_gen); + _tree.getAllocator().reclaim_memory(used_gen); } template <typename DataStoreTraitsT> diff --git a/valgrind-suppressions.txt b/valgrind-suppressions.txt index 63fd7857e76..241c7ef32f7 100644 --- a/valgrind-suppressions.txt +++ b/valgrind-suppressions.txt @@ -18,6 +18,16 @@ NPTL keeps a cache of thread stacks, and metadata for thread local storage is not freed for threads in that cache Memcheck:Leak fun:calloc + fun:UnknownInlinedFun + fun:allocate_dtv + fun:_dl_allocate_tls + fun:allocate_stack + fun:pthread_create@@GLIBC_2.2.5 +} +{ + NPTL keeps a cache of thread stacks, and metadata for thread local storage is not freed for threads in that cache + Memcheck:Leak + fun:calloc fun:allocate_dtv fun:_dl_allocate_tls fun:allocate_stack diff --git a/vespa-athenz/pom.xml b/vespa-athenz/pom.xml index 55482dd1fed..758a99b38b9 100644 --- a/vespa-athenz/pom.xml +++ b/vespa-athenz/pom.xml @@ -92,7 +92,7 @@ </exclusion> <exclusion> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>*</artifactId> </exclusion> <!--Exclude all Jackson bundles provided by JDisc --> <exclusion> @@ -148,7 +148,7 @@ </exclusion> <exclusion> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>*</artifactId> </exclusion> <!--Exclude all Jackson bundles provided by JDisc --> <exclusion> diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java index d5b772e5bab..aaf9038208f 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/DefaultZmsClient.java @@ -205,6 +205,18 @@ public class DefaultZmsClient extends ClientBase implements ZmsClient { } @Override + public List<AthenzDomain> getDomainListByAccount(String account) { + HttpUriRequest request = RequestBuilder.get() + .setUri(zmsUrl.resolve("domain")) + .addParameter("account", account) + .build(); + return execute(request, response -> { + DomainListResponseEntity result = readEntity(response, DomainListResponseEntity.class); + return result.domains.stream().map(AthenzDomain::new).collect(toList()); + }); + } + + @Override public boolean hasAccess(AthenzResourceName resource, String action, AthenzIdentity identity) { URI uri = zmsUrl.resolve(String.format("access/%s/%s?principal=%s", action, resource.toResourceNameString(), identity.getFullName())); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java index 0dd0d30200c..be4c6c7ba3b 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/client/zms/ZmsClient.java @@ -50,6 +50,8 @@ public interface ZmsClient extends Closeable { List<AthenzDomain> getDomainList(String prefix); + List<AthenzDomain> getDomainListByAccount(String id); + boolean hasAccess(AthenzResourceName resource, String action, AthenzIdentity identity); void createPolicy(AthenzDomain athenzDomain, String athenzPolicy); diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java index a032b23bfb3..0415bca1670 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identity/SiaIdentityProvider.java @@ -70,6 +70,10 @@ public class SiaIdentityProvider extends AbstractComponent implements ServiceIde @Override public Path certificatePath() { return certificateFile; } @Override public Path privateKeyPath() { return privateKeyFile; } + public SSLContext createIdentitySslContextWithTrustStore(Path trustStoreFile) { + return createIdentitySslContext(keyManager, trustStoreFile); + } + private static SSLContext createIdentitySslContext(AutoReloadingX509KeyManager keyManager, Path trustStoreFile) { return new SslContextBuilder() .withTrustStore(trustStoreFile) diff --git a/vespa-feed-client/pom.xml b/vespa-feed-client/pom.xml index 8b7b82573c4..1cc2f2adee1 100644 --- a/vespa-feed-client/pom.xml +++ b/vespa-feed-client/pom.xml @@ -21,7 +21,7 @@ </dependency> <dependency> <groupId>org.bouncycastle</groupId> - <artifactId>bcpkix-jdk15on</artifactId> + <artifactId>bcpkix-jdk18on</artifactId> <scope>compile</scope> </dependency> <dependency> diff --git a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java index aaf67f6b8ea..ce85b7d6f32 100644 --- a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java +++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/AbstractVespaMojo.java @@ -43,6 +43,9 @@ public abstract class AbstractVespaMojo extends AbstractMojo { @Parameter(property = "instance") protected String instance; + @Parameter(property = "tags") + protected String tags; + @Parameter(property = "apiKey") protected String apiKey; diff --git a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java index 556af8b6f85..377975b3d01 100644 --- a/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java +++ b/vespa-maven-plugin/src/main/java/ai/vespa/hosted/plugin/EffectiveServicesMojo.java @@ -3,6 +3,7 @@ package ai.vespa.hosted.plugin; import com.yahoo.config.application.XmlPreProcessor; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; @@ -44,16 +45,17 @@ public class EffectiveServicesMojo extends AbstractVespaDeploymentMojo { ZoneId zone = zoneOf(environment, region); Path output = Paths.get(outputDirectory).resolve("services-" + zone.environment().value() + "-" + zone.region().value() + ".xml"); - Files.write(output, effectiveServices(services, zone, InstanceName.from(instance)).getBytes(StandardCharsets.UTF_8)); + Files.write(output, effectiveServices(services, zone, InstanceName.from(instance), Tags.fromString(tags)).getBytes(StandardCharsets.UTF_8)); getLog().info("Effective services for " + zone + " written to " + output); } - static String effectiveServices(File servicesFile, ZoneId zone, InstanceName instance) throws Exception { + static String effectiveServices(File servicesFile, ZoneId zone, InstanceName instance, Tags tags) throws Exception { Document processedServicesXml = new XmlPreProcessor(servicesFile.getParentFile(), servicesFile, instance, zone.environment(), - zone.region()) + zone.region(), + tags) .run(); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); diff --git a/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java b/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java index 3cb08f5f2b6..fb7376b13cb 100644 --- a/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java +++ b/vespa-maven-plugin/src/test/java/ai/vespa/hosted/plugin/EffectiveServicesMojoTest.java @@ -2,6 +2,7 @@ package ai.vespa.hosted.plugin; import com.yahoo.config.provision.InstanceName; +import com.yahoo.config.provision.Tags; import com.yahoo.config.provision.zone.ZoneId; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -26,21 +27,21 @@ class EffectiveServicesMojoTest { @DisplayName("when zone matches environment-only directive") void devServices() throws Exception { assertEquals(Files.readString(Paths.get("src/test/resources/effective-services/dev.xml")), - effectiveServices(servicesFile, ZoneId.from("dev", "us-east-3"), InstanceName.defaultName())); + effectiveServices(servicesFile, ZoneId.from("dev", "us-east-3"), InstanceName.defaultName(), Tags.empty())); } @Test @DisplayName("when zone matches region-and-environment directive") void prodUsEast3() throws Exception { assertEquals(Files.readString(Paths.get("src/test/resources/effective-services/prod_us-east-3.xml")), - effectiveServices(servicesFile, ZoneId.from("prod", "us-east-3"), InstanceName.defaultName())); + effectiveServices(servicesFile, ZoneId.from("prod", "us-east-3"), InstanceName.defaultName(), Tags.empty())); } @Test @DisplayName("when zone doesn't match any directives") void prodUsWest1Services() throws Exception { assertEquals(Files.readString(Paths.get("src/test/resources/effective-services/prod_us-west-1.xml")), - effectiveServices(servicesFile, ZoneId.from("prod", "us-west-1"), InstanceName.defaultName())); + effectiveServices(servicesFile, ZoneId.from("prod", "us-west-1"), InstanceName.defaultName(), Tags.empty())); } } diff --git a/vespabase/CMakeLists.txt b/vespabase/CMakeLists.txt index bf7ea3dfcc7..ce19dbb56b3 100644 --- a/vespabase/CMakeLists.txt +++ b/vespabase/CMakeLists.txt @@ -29,6 +29,7 @@ install(DIRECTORY DESTINATION var/db/vespa) install(DIRECTORY DESTINATION var/db/vespa/config_server) install(DIRECTORY DESTINATION var/db/vespa/config_server/serverdb) install(DIRECTORY DESTINATION var/db/vespa/config_server/serverdb/tenants) +install(DIRECTORY DESTINATION var/db/vespa/download) install(DIRECTORY DESTINATION var/db/vespa/filedistribution) install(DIRECTORY DESTINATION var/db/vespa/index) install(DIRECTORY DESTINATION var/db/vespa/search) diff --git a/vespabase/src/rhel-prestart.sh b/vespabase/src/rhel-prestart.sh index ff28b31ca2b..79a8e61848c 100755 --- a/vespabase/src/rhel-prestart.sh +++ b/vespabase/src/rhel-prestart.sh @@ -116,6 +116,7 @@ fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa/config_server fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa/config_server/serverdb fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa/config_server/serverdb/tenants +fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa/download fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa/filedistribution fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa/index fixdir ${VESPA_USER} ${VESPA_GROUP} 755 var/db/vespa/logcontrol diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java index c72bc1ef4c5..66154ec1c28 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java @@ -12,7 +12,6 @@ import com.yahoo.container.core.documentapi.VespaDocumentAccess; import com.yahoo.container.jdisc.ContentChannelOutputStream; import com.yahoo.document.Document; import com.yahoo.document.DocumentId; -import com.yahoo.document.DocumentOperation; import com.yahoo.document.DocumentPut; import com.yahoo.document.DocumentRemove; import com.yahoo.document.DocumentTypeManager; @@ -26,6 +25,7 @@ import com.yahoo.document.idstring.IdIdString; import com.yahoo.document.json.DocumentOperationType; import com.yahoo.document.json.JsonReader; import com.yahoo.document.json.JsonWriter; +import com.yahoo.document.json.ParsedDocumentOperation; import com.yahoo.document.restapi.DocumentOperationExecutorConfig; import com.yahoo.document.select.parser.ParseException; import com.yahoo.documentapi.AckToken; @@ -38,6 +38,7 @@ import com.yahoo.documentapi.ProgressToken; import com.yahoo.documentapi.Response.Outcome; import com.yahoo.documentapi.Result; import com.yahoo.documentapi.VisitorControlHandler; +import com.yahoo.documentapi.VisitorControlSession; import com.yahoo.documentapi.VisitorDataHandler; import com.yahoo.documentapi.VisitorParameters; import com.yahoo.documentapi.VisitorSession; @@ -67,6 +68,7 @@ import com.yahoo.restapi.Path; import com.yahoo.search.query.ParameterParser; import com.yahoo.text.Text; import com.yahoo.vespa.config.content.AllClustersBucketSpacesConfig; +import com.yahoo.vespa.http.server.Headers; import com.yahoo.vespa.http.server.MetricNames; import com.yahoo.yolean.Exceptions; import com.yahoo.yolean.Exceptions.RunnableThrowingIOException; @@ -215,7 +217,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { this.operations = new ConcurrentLinkedDeque<>(); long resendDelayMS = SystemTimer.adjustTimeoutByDetectedHz(Duration.ofMillis(executorConfig.resendDelayMillis())).toMillis(); - // TODO: Here it would be better do have dedicated threads with different wait depending on blocked or empty. + // TODO: Here it would be better to have dedicated threads with different wait depending on blocked or empty. this.dispatcher.scheduleWithFixedDelay(this::dispatchEnqueued, resendDelayMS, resendDelayMS, MILLISECONDS); this.visitDispatcher.scheduleWithFixedDelay(this::dispatchVisitEnqueued, resendDelayMS, resendDelayMS, MILLISECONDS); } @@ -238,7 +240,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { MILLISECONDS); Path requestPath = Path.withoutValidation(request.getUri()); // No segment validation here, as document IDs can be anything. - for (String path : handlers.keySet()) + for (String path : handlers.keySet()) { if (requestPath.matches(path)) { Map<Method, Handler> methods = handlers.get(path); if (methods.containsKey(request.getMethod())) @@ -249,6 +251,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { methodNotAllowed(request, methods.keySet(), responseHandler); } + } notFound(request, handlers.keySet(), responseHandler); } catch (IllegalArgumentException e) { @@ -398,10 +401,10 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { parameters.setFieldSet(DocIdOnly.NAME); String type = path.documentType().orElseThrow(() -> new IllegalStateException("Document type must be specified for mass updates")); IdIdString dummyId = new IdIdString("dummy", type, "", ""); - DocumentUpdate update = parser.parseUpdate(in, dummyId.toString()); - update.setCondition(new TestAndSetCondition(requireProperty(request, SELECTION))); + ParsedDocumentOperation update = parser.parseUpdate(in, dummyId.toString()); + update.operation().setCondition(new TestAndSetCondition(requireProperty(request, SELECTION))); return () -> { - visitAndUpdate(request, parameters, handler, update, cluster.name()); + visitAndUpdate(request, parameters, update.fullyApplied(), handler, (DocumentUpdate)update.operation(), cluster.name()); return true; // VisitorSession has its own throttle handling. }; }); @@ -448,21 +451,21 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private ContentChannel postDocument(HttpRequest request, DocumentPath path, ResponseHandler rawHandler) { ResponseHandler handler = new MeasuringResponseHandler(rawHandler, com.yahoo.documentapi.metrics.DocumentOperationType.PUT, clock.instant()); if (getProperty(request, DRY_RUN, booleanParser).orElse(false)) { - handleFeedOperation(path, handler, new com.yahoo.documentapi.Response(-1)); + handleFeedOperation(path, true, handler, new com.yahoo.documentapi.Response(-1)); return ignoredContent; } return new ForwardingContentChannel(in -> { enqueueAndDispatch(request, handler, () -> { - DocumentPut put = parser.parsePut(in, path.id().toString()); - getProperty(request, CONDITION).map(TestAndSetCondition::new).ifPresent(put::setCondition); + ParsedDocumentOperation put = parser.parsePut(in, path.id().toString()); + getProperty(request, CONDITION).map(TestAndSetCondition::new).ifPresent(c -> put.operation().setCondition(c)); DocumentOperationParameters parameters = parametersFromRequest(request, ROUTE) .withResponseHandler(response -> { outstanding.decrementAndGet(); updatePutMetrics(response.outcome()); - handleFeedOperation(path, handler, response); + handleFeedOperation(path, put.fullyApplied(), handler, response); }); - return () -> dispatchOperation(() -> asyncSession.put(put, parameters)); + return () -> dispatchOperation(() -> asyncSession.put((DocumentPut)put.operation(), parameters)); }); }); } @@ -470,20 +473,21 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private ContentChannel putDocument(HttpRequest request, DocumentPath path, ResponseHandler rawHandler) { ResponseHandler handler = new MeasuringResponseHandler(rawHandler, com.yahoo.documentapi.metrics.DocumentOperationType.UPDATE, clock.instant()); if (getProperty(request, DRY_RUN, booleanParser).orElse(false)) { - handleFeedOperation(path, handler, new com.yahoo.documentapi.Response(-1)); + handleFeedOperation(path, true, handler, new com.yahoo.documentapi.Response(-1)); return ignoredContent; } return new ForwardingContentChannel(in -> { enqueueAndDispatch(request, handler, () -> { - DocumentUpdate update = parser.parseUpdate(in, path.id().toString()); + ParsedDocumentOperation parsed = parser.parseUpdate(in, path.id().toString()); + DocumentUpdate update = (DocumentUpdate)parsed.operation(); getProperty(request, CONDITION).map(TestAndSetCondition::new).ifPresent(update::setCondition); getProperty(request, CREATE, booleanParser).ifPresent(update::setCreateIfNonExistent); DocumentOperationParameters parameters = parametersFromRequest(request, ROUTE) .withResponseHandler(response -> { outstanding.decrementAndGet(); updateUpdateMetrics(response.outcome(), update.getCreateIfNonExistent()); - handleFeedOperation(path, handler, response); + handleFeedOperation(path, parsed.fullyApplied(), handler, response); }); return () -> dispatchOperation(() -> asyncSession.update(update, parameters)); }); @@ -493,7 +497,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private ContentChannel deleteDocument(HttpRequest request, DocumentPath path, ResponseHandler rawHandler) { ResponseHandler handler = new MeasuringResponseHandler(rawHandler, com.yahoo.documentapi.metrics.DocumentOperationType.REMOVE, clock.instant()); if (getProperty(request, DRY_RUN, booleanParser).orElse(false)) { - handleFeedOperation(path, handler, new com.yahoo.documentapi.Response(-1)); + handleFeedOperation(path, true, handler, new com.yahoo.documentapi.Response(-1)); return ignoredContent; } @@ -504,7 +508,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { .withResponseHandler(response -> { outstanding.decrementAndGet(); updateRemoveMetrics(response.outcome()); - handleFeedOperation(path, handler, response); + handleFeedOperation(path, true, handler, response); }); return () -> dispatchOperation(() -> asyncSession.remove(remove, parameters)); }); @@ -659,10 +663,16 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { return response; } - /** Commits a response with the given status code and some default headers, and writes whatever content is buffered. */ synchronized void commit(int status) throws IOException { + commit(status, true); + } + + /** Commits a response with the given status code and some default headers, and writes whatever content is buffered. */ + synchronized void commit(int status, boolean fullyApplied) throws IOException { Response response = new Response(status); - response.headers().addAll(Map.of("Content-Type", List.of("application/json; charset=UTF-8"))); + response.headers().add("Content-Type", List.of("application/json; charset=UTF-8")); + if (! fullyApplied) + response.headers().add(Headers.IGNORED_FIELDS, "true"); try { channel = handler.handleResponse(response); buffer.connectTo(channel); @@ -1023,16 +1033,16 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { this.manager = new DocumentTypeManager(config); } - DocumentPut parsePut(InputStream inputStream, String docId) { - return (DocumentPut) parse(inputStream, docId, DocumentOperationType.PUT); + ParsedDocumentOperation parsePut(InputStream inputStream, String docId) { + return parse(inputStream, docId, DocumentOperationType.PUT); } - DocumentUpdate parseUpdate(InputStream inputStream, String docId) { - return (DocumentUpdate) parse(inputStream, docId, DocumentOperationType.UPDATE); + ParsedDocumentOperation parseUpdate(InputStream inputStream, String docId) { + return parse(inputStream, docId, DocumentOperationType.UPDATE); } - private DocumentOperation parse(InputStream inputStream, String docId, DocumentOperationType operation) { - return new JsonReader(manager, inputStream, jsonFactory).readSingleDocument(operation, docId); + private ParsedDocumentOperation parse(InputStream inputStream, String docId, DocumentOperationType operation) { + return new JsonReader(manager, inputStream, jsonFactory).readOperation(operation, docId); } } @@ -1041,7 +1051,11 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { void onSuccess(Document document, JsonResponse response) throws IOException; } - private static void handle(DocumentPath path, HttpRequest request, ResponseHandler handler, com.yahoo.documentapi.Response response, SuccessCallback callback) { + private static void handle(DocumentPath path, + HttpRequest request, + ResponseHandler handler, + com.yahoo.documentapi.Response response, + SuccessCallback callback) { try (JsonResponse jsonResponse = JsonResponse.create(path, handler, request)) { jsonResponse.writeTrace(response.getTrace()); if (response.isSuccess()) @@ -1049,25 +1063,18 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { else { jsonResponse.writeMessage(response.getTextMessage()); switch (response.outcome()) { - case NOT_FOUND: - jsonResponse.commit(Response.Status.NOT_FOUND); - break; - case CONDITION_FAILED: - jsonResponse.commit(Response.Status.PRECONDITION_FAILED); - break; - case INSUFFICIENT_STORAGE: - jsonResponse.commit(Response.Status.INSUFFICIENT_STORAGE); - break; - case TIMEOUT: - jsonResponse.commit(Response.Status.GATEWAY_TIMEOUT); - break; - case ERROR: + case NOT_FOUND -> jsonResponse.commit(Response.Status.NOT_FOUND); + case CONDITION_FAILED -> jsonResponse.commit(Response.Status.PRECONDITION_FAILED); + case INSUFFICIENT_STORAGE -> jsonResponse.commit(Response.Status.INSUFFICIENT_STORAGE); + case TIMEOUT -> jsonResponse.commit(Response.Status.GATEWAY_TIMEOUT); + case ERROR -> { log.log(FINE, () -> "Exception performing document operation: " + response.getTextMessage()); jsonResponse.commit(Response.Status.BAD_GATEWAY); - break; - default: + } + default -> { log.log(WARNING, "Unexpected document API operation outcome '" + response.outcome() + "' " + response.getTextMessage()); jsonResponse.commit(Response.Status.BAD_GATEWAY); + } } } } @@ -1076,8 +1083,11 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } } - private static void handleFeedOperation(DocumentPath path, ResponseHandler handler, com.yahoo.documentapi.Response response) { - handle(path, null, handler, response, (document, jsonResponse) -> jsonResponse.commit(Response.Status.OK)); + private static void handleFeedOperation(DocumentPath path, + boolean fullyApplied, + ResponseHandler handler, + com.yahoo.documentapi.Response response) { + handle(path, null, handler, response, (document, jsonResponse) -> jsonResponse.commit(Response.Status.OK, fullyApplied)); } private void updatePutMetrics(Outcome outcome) { @@ -1166,6 +1176,8 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { StringJoiner::merge) .toString()); + getProperty(request, TRACELEVEL, integerParser).ifPresent(parameters::setTraceLevel); + getProperty(request, CONTINUATION).map(ProgressToken::fromSerializedString).ifPresent(parameters::setResumeToken); parameters.setPriority(DocumentProtocol.Priority.NORMAL_4); @@ -1188,7 +1200,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private interface VisitCallback { /** Called at the start of response rendering. */ - default void onStart(JsonResponse response) throws IOException { } + default void onStart(JsonResponse response, boolean fullyApplied) throws IOException { } /** Called for every document received from backend visitors—must call the ack for these to proceed. */ default void onDocument(JsonResponse response, Document document, Runnable ack, Consumer<String> onError) { } @@ -1199,25 +1211,26 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private void visitAndDelete(HttpRequest request, VisitorParameters parameters, ResponseHandler handler, TestAndSetCondition condition, String route) { - visitAndProcess(request, parameters, handler, route, (id, operationParameters) -> { + visitAndProcess(request, parameters, true, handler, route, (id, operationParameters) -> { DocumentRemove remove = new DocumentRemove(id); remove.setCondition(condition); return asyncSession.remove(remove, operationParameters); }); } - private void visitAndUpdate(HttpRequest request, VisitorParameters parameters, ResponseHandler handler, - DocumentUpdate protoUpdate, String route) { - visitAndProcess(request, parameters, handler, route, (id, operationParameters) -> { + private void visitAndUpdate(HttpRequest request, VisitorParameters parameters, boolean fullyApplied, + ResponseHandler handler, DocumentUpdate protoUpdate, String route) { + visitAndProcess(request, parameters, fullyApplied, handler, route, (id, operationParameters) -> { DocumentUpdate update = new DocumentUpdate(protoUpdate); update.setId(id); return asyncSession.update(update, operationParameters); }); } - private void visitAndProcess(HttpRequest request, VisitorParameters parameters, ResponseHandler handler, + private void visitAndProcess(HttpRequest request, VisitorParameters parameters, boolean fullyApplied, + ResponseHandler handler, String route, BiFunction<DocumentId, DocumentOperationParameters, Result> operation) { - visit(request, parameters, false, handler, new VisitCallback() { + visit(request, parameters, false, fullyApplied, handler, new VisitCallback() { @Override public void onDocument(JsonResponse response, Document document, Runnable ack, Consumer<String> onError) { DocumentOperationParameters operationParameters = parameters().withRoute(route) .withResponseHandler(operationResponse -> { @@ -1255,10 +1268,10 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } private void visitAndWrite(HttpRequest request, VisitorParameters parameters, ResponseHandler handler, boolean streamed) { - visit(request, parameters, streamed, handler, new VisitCallback() { - @Override public void onStart(JsonResponse response) throws IOException { + visit(request, parameters, streamed, true, handler, new VisitCallback() { + @Override public void onStart(JsonResponse response, boolean fullyApplied) throws IOException { if (streamed) - response.commit(Response.Status.OK); + response.commit(Response.Status.OK, fullyApplied); response.writeDocumentsArrayStart(); } @@ -1288,18 +1301,23 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } private void visitWithRemote(HttpRequest request, VisitorParameters parameters, ResponseHandler handler) { - visit(request, parameters, false, handler, new VisitCallback() { }); + visit(request, parameters, false, true, handler, new VisitCallback() { }); } @SuppressWarnings("fallthrough") - private void visit(HttpRequest request, VisitorParameters parameters, boolean streaming, ResponseHandler handler, VisitCallback callback) { + private void visit(HttpRequest request, VisitorParameters parameters, boolean streaming, boolean fullyApplied, ResponseHandler handler, VisitCallback callback) { try { JsonResponse response = JsonResponse.create(request, handler); Phaser phaser = new Phaser(2); // Synchronize this thread (dispatch) with the visitor callback thread. AtomicReference<String> error = new AtomicReference<>(); // Set if error occurs during processing of visited documents. - callback.onStart(response); + callback.onStart(response, fullyApplied); VisitorControlHandler controller = new VisitorControlHandler() { final ScheduledFuture<?> abort = streaming ? visitDispatcher.schedule(this::abort, request.getTimeout(MILLISECONDS), MILLISECONDS) : null; + final AtomicReference<VisitorSession> session = new AtomicReference<>(); + @Override public void setSession(VisitorControlSession session) { // Workaround for broken session API ಠ_ಠ + super.setSession(session); + if (session instanceof VisitorSession visitorSession) this.session.set(visitorSession); + } @Override public void onDone(CompletionCode code, String message) { super.onDone(code, message); loggingException(() -> { @@ -1309,6 +1327,9 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { if (getVisitorStatistics() != null) response.writeDocumentCount(getVisitorStatistics().getDocumentsVisited()); + if (session.get() != null) + response.writeTrace(session.get().getTrace()); + int status = Response.Status.BAD_GATEWAY; switch (code) { case TIMEOUT: @@ -1332,7 +1353,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { response.writeMessage(error.get() != null ? error.get() : message != null ? message : "Visiting failed"); } if ( ! streaming) - response.commit(status); + response.commit(status, fullyApplied); } }); if (abort != null) abort.cancel(false); // Avoid keeping scheduled future alive if this completes in any other fashion. @@ -1340,7 +1361,6 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { phaser.arriveAndAwaitAdvance(); // We may get here while dispatching thread is still putting us in the map. visits.remove(this).destroy(); }); - } }; if (parameters.getRemoteDataHandler() == null) { diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/ClientFeederV3.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/ClientFeederV3.java index 8ea9234009d..438248f31a7 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/ClientFeederV3.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/ClientFeederV3.java @@ -4,7 +4,6 @@ package com.yahoo.vespa.http.server; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.document.DocumentTypeManager; -import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.ReferencedResource; import com.yahoo.jdisc.ResourceReference; @@ -53,13 +52,12 @@ class ClientFeederV3 { private final AtomicInteger ongoingRequests = new AtomicInteger(0); private final String hostName; - ClientFeederV3( - ReferencedResource<SharedSourceSession> sourceSession, - FeedReaderFactory feedReaderFactory, - DocumentTypeManager docTypeManager, - String clientId, - Metric metric, - ReplyHandler feedReplyHandler) { + ClientFeederV3(ReferencedResource<SharedSourceSession> sourceSession, + FeedReaderFactory feedReaderFactory, + DocumentTypeManager docTypeManager, + String clientId, + Metric metric, + ReplyHandler feedReplyHandler) { this.sourceSession = sourceSession; this.clientId = clientId; this.feedReplyHandler = feedReplyHandler; @@ -220,10 +218,7 @@ class ClientFeederV3 { // This is a bit hard to set up while testing, so we accept that things are not perfect. if (sourceSession.getResource().session() != null) { - metric.set( - MetricNames.PENDING, - Double.valueOf(sourceSession.getResource().session().getPendingCount()), - null); + metric.set(MetricNames.PENDING, (double) sourceSession.getResource().session().getPendingCount(), null); } DocumentOperationMessageV3 message = DocumentOperationMessageV3.create(operation, operationId, metric); @@ -231,7 +226,7 @@ class ClientFeederV3 { // typical end of feed return null; } - metric.add(MetricNames.NUM_OPERATIONS, 1, null /*metricContext*/); + metric.add(MetricNames.NUM_OPERATIONS, 1, null); log(Level.FINE, "Successfully deserialized document id: ", message.getOperationId()); return message; } @@ -241,14 +236,6 @@ class ClientFeederV3 { if (settings.traceLevel != null) { msg.getMessage().getTrace().setLevel(settings.traceLevel); } - if (settings.priority != null) { - try { - DocumentProtocol.Priority priority = DocumentProtocol.Priority.valueOf(settings.priority); - } - catch (IllegalArgumentException i) { - log.severe(i.getMessage()); - } - } } private void setRoute(DocumentOperationMessageV3 msg, FeederSettings settings) { @@ -272,7 +259,7 @@ class ClientFeederV3 { if (now.plusSeconds(1).isAfter(prevOpsPerSecTime)) { Duration duration = Duration.between(now, prevOpsPerSecTime); double opsPerSec = operationsForOpsPerSec / (duration.toMillis() / 1000.); - metric.set(MetricNames.OPERATIONS_PER_SEC, opsPerSec, null /*metricContext*/); + metric.set(MetricNames.OPERATIONS_PER_SEC, opsPerSec, null); operationsForOpsPerSec = 1.0d; prevOpsPerSecTime = now; } else { @@ -280,4 +267,5 @@ class ClientFeederV3 { } } } + } diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/DocumentOperationMessageV3.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/DocumentOperationMessageV3.java index 25bf5815907..a12fe4efa8b 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/DocumentOperationMessageV3.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/DocumentOperationMessageV3.java @@ -14,8 +14,6 @@ import com.yahoo.vespaxmlparser.FeedOperation; /** * Keeps an operation with its message. * - * This implementation is based on V2, but the code is restructured. - * * @author dybis */ class DocumentOperationMessageV3 { @@ -66,13 +64,13 @@ class DocumentOperationMessageV3 { static DocumentOperationMessageV3 create(FeedOperation operation, String operationId, Metric metric) { switch (operation.getType()) { case DOCUMENT: - metric.add(MetricNames.NUM_PUTS, 1, null /*metricContext*/); + metric.add(MetricNames.NUM_PUTS, 1, null); return newPutMessage(operation, operationId); case REMOVE: - metric.add(MetricNames.NUM_REMOVES, 1, null /*metricContext*/); + metric.add(MetricNames.NUM_REMOVES, 1, null); return newRemoveMessage(operation, operationId); case UPDATE: - metric.add(MetricNames.NUM_UPDATES, 1, null /*metricContext*/); + metric.add(MetricNames.NUM_UPDATES, 1, null); return newUpdateMessage(operation, operationId); default: // typical end of feed diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandler.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandler.java index 74665d60a04..3c1f376b4eb 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandler.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandler.java @@ -29,14 +29,14 @@ import java.util.regex.Pattern; import java.util.zip.GZIPInputStream; /** - * Accept feeds from outside of the Vespa cluster. + * Accept feeds from outside the Vespa cluster. * * @author Steinar Knutsen */ public class FeedHandler extends ThreadedHttpRequestHandler { protected final ReplyHandler feedReplyHandler; - private static final List<Integer> serverSupportedVersions = Collections.unmodifiableList(Arrays.asList(3)); + private static final List<Integer> serverSupportedVersions = List.of(3); private static final Pattern USER_AGENT_PATTERN = Pattern.compile("vespa-http-client \\((.+)\\)"); private final FeedHandlerV3 feedHandlerV3; private final DocumentApiMetrics metricsHelper; @@ -144,4 +144,5 @@ public class FeedHandler extends ThreadedHttpRequestHandler { } @Override protected void destroy() { feedHandlerV3.destroy(); } + } diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandlerV3.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandlerV3.java index f9ae04623e6..4de3eebec2d 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandlerV3.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeedHandlerV3.java @@ -24,9 +24,8 @@ import java.util.logging.Level; import java.util.logging.Logger; /** - * This code is based on v2 code, however, in v3, one client has one ClientFeederV3 shared between all client threads. - * The new API has more logic for shutting down cleanly as the server is more likely to be upgraded. - * The code is restructured a bit. + * One client has one ClientFeederV3 shared between all client threads. + * Contains logic for shutting down cleanly as the server is upgraded. * * @author dybis */ @@ -60,7 +59,7 @@ public class FeedHandlerV3 extends ThreadedHttpRequestHandler { } // TODO: If this is set up to run without first invoking the old FeedHandler code, we should - // verify the version header first. This is done in the old code. + // verify the version header first. @Override public HttpResponse handle(HttpRequest request) { String clientId = clientId(request); @@ -70,7 +69,7 @@ public class FeedHandlerV3 extends ThreadedHttpRequestHandler { SourceSessionParams sourceSessionParams = sourceSessionParams(request); clientFeederByClientId.put(clientId, new ClientFeederV3(retainSource(sessionCache, sourceSessionParams), - new FeedReaderFactory(true), //TODO make error debugging configurable + new FeedReaderFactory(true), // TODO: Make error debugging configurable docTypeManager, clientId, metric, diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeederSettings.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeederSettings.java index 9bb8a58d6f6..a8175a48a39 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeederSettings.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/FeederSettings.java @@ -17,14 +17,12 @@ public class FeederSettings { public final boolean drain; // TODO: Implement drain=true public final Route route; public final FeedParams.DataFormat dataFormat; - public final String priority; public final Integer traceLevel; public FeederSettings(HttpRequest request) { this.drain = Optional.ofNullable(request.getHeader(Headers.DRAIN)).map(Boolean::parseBoolean).orElse(false); this.route = Optional.ofNullable(request.getHeader(Headers.ROUTE)).map(Route::parse).orElse(DEFAULT_ROUTE); this.dataFormat = Optional.ofNullable(request.getHeader(Headers.DATA_FORMAT)).map(FeedParams.DataFormat::valueOf).orElse(FeedParams.DataFormat.JSON_UTF8); - this.priority = request.getHeader(Headers.PRIORITY); this.traceLevel = Optional.ofNullable(request.getHeader(Headers.TRACE_LEVEL)).map(Integer::valueOf).orElse(null); } diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/Headers.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/Headers.java index 16bff38af4b..657c22ba7ee 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/Headers.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/Headers.java @@ -6,7 +6,7 @@ package com.yahoo.vespa.http.server; * * @author Steinar Knutsen */ -final class Headers { +public final class Headers { private Headers() { } @@ -23,7 +23,6 @@ final class Headers { // This value can be used to route the request to a specific server when using // several servers. It is a random value that is the same for the whole session. public static final String SHARDING_KEY = "X-Yahoo-Feed-Sharding-Key"; - public static final String PRIORITY = "X-Yahoo-Feed-Priority"; public static final String TRACE_LEVEL = "X-Yahoo-Feed-Trace-Level"; public static final int HTTP_NOT_ACCEPTABLE = 406; @@ -34,4 +33,8 @@ final class Headers { public static final String HOSTNAME = "X-Yahoo-Hostname"; public static final String SILENTUPGRADE = "X-Yahoo-Silent-Upgrade"; + // A response header present and set to "true" onlynif any fields of a document operation were ignored + // because they were not declared in the target document type. + public static final String IGNORED_FIELDS = "X-Vespa-Ignored-Fields"; + } diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/StreamReaderV3.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/StreamReaderV3.java index c2c6d00fa25..3d82919d503 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/StreamReaderV3.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/StreamReaderV3.java @@ -10,17 +10,15 @@ import com.yahoo.vespaxmlparser.FeedReader; import java.io.IOException; import java.io.InputStream; import java.util.Optional; -import java.util.logging.Logger; import java.util.zip.GZIPInputStream; /** * This code is based on v2 code, but restructured so stream reading code is in one dedicated class. + * * @author dybis */ public class StreamReaderV3 { - protected static final Logger log = Logger.getLogger(StreamReaderV3.class.getName()); - private final FeedReaderFactory feedReaderFactory; private final DocumentTypeManager docTypeManager; @@ -30,15 +28,11 @@ public class StreamReaderV3 { } public FeedOperation getNextOperation(InputStream requestInputStream, FeederSettings settings) throws Exception { - FeedOperation op = null; - int length = readByteLength(requestInputStream); - try (InputStream limitedInputStream = new ByteLimitedInputStream(requestInputStream, length)){ FeedReader reader = feedReaderFactory.createReader(limitedInputStream, docTypeManager, settings.dataFormat); - op = reader.read(); + return reader.read(); } - return op; } public Optional<String> getNextOperationId(InputStream requestInputStream) throws IOException { @@ -48,7 +42,7 @@ public class StreamReaderV3 { if (c == 32) { break; } - idBuf.append((char) c); //it's ASCII + idBuf.append((char) c); // it's ASCII } if (idBuf.length() == 0) { return Optional.empty(); @@ -63,7 +57,7 @@ public class StreamReaderV3 { if (c == 10) { break; } - lenBuf.append((char) c); //it's ASCII + lenBuf.append((char) c); // it's ASCII } if (lenBuf.length() == 0) { throw new IllegalStateException("Operation length missing."); @@ -71,9 +65,8 @@ public class StreamReaderV3 { return Integer.valueOf(lenBuf.toString(), 16); } - public static InputStream unzipStreamIfNeeded(final HttpRequest httpRequest) - throws IOException { - final String contentEncodingHeader = httpRequest.getHeader("content-encoding"); + public static InputStream unzipStreamIfNeeded(final HttpRequest httpRequest) throws IOException { + String contentEncodingHeader = httpRequest.getHeader("content-encoding"); if ("gzip".equals(contentEncodingHeader)) { return new GZIPInputStream(httpRequest.getData()); } else { diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/package-info.java b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/package-info.java index ea01137d9af..3678a0b9fac 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/package-info.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/vespa/http/server/package-info.java @@ -1,6 +1,6 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. /** - * Server side of programmatic API for feeding into Vespa from outside of the + * Server side of programmatic API for feeding into Vespa from outside the * clusters. Not a public API, not meant for direct use. */ @com.yahoo.api.annotations.PackageMarker diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java index 7f77ce9d0d5..973d0a98b24 100644 --- a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java +++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java @@ -113,7 +113,8 @@ public class DocumentV1ApiTest { .maxThrottled(2) .resendDelayMillis(1 << 30) .build(); - final DocumentmanagerConfig docConfig = Deriver.getDocumentManagerConfig("src/test/cfg/music.sd").build(); + final DocumentmanagerConfig docConfig = Deriver.getDocumentManagerConfig("src/test/cfg/music.sd") + .ignoreundefinedfields(true).build(); final DocumentTypeManager manager = new DocumentTypeManager(docConfig); final Document doc1 = new Document(manager.getDocumentType("music"), "id:space:music::one"); final Document doc2 = new Document(manager.getDocumentType("music"), "id:space:music:n=1:two"); @@ -207,6 +208,12 @@ public class DocumentV1ApiTest { // GET at root is a visit. Numeric parameters have an upper bound. access.expect(tokens); + Trace visitorTrace = new Trace(9); + visitorTrace.trace(7, "Tracy Chapman", false); + visitorTrace.getRoot().addChild(new TraceNode().setStrict(false) + .addChild("Fast Car") + .addChild("Baby Can I Hold You")); + access.visitorTrace = visitorTrace; access.expect(parameters -> { assertEquals("content", parameters.getRoute().toString()); assertEquals("default", parameters.getBucketSpace()); @@ -215,6 +222,7 @@ public class DocumentV1ApiTest { assertEquals("[id]", parameters.getFieldSet()); assertEquals("(all the things)", parameters.getDocumentSelection()); assertEquals(6000, parameters.getSessionTimeoutMs()); + assertEquals(9, parameters.getTraceLevel()); // Put some documents in the response parameters.getLocalDataHandler().onMessage(new PutDocumentMessage(new DocumentPut(doc1)), tokens.get(0)); parameters.getLocalDataHandler().onMessage(new PutDocumentMessage(new DocumentPut(doc2)), tokens.get(1)); @@ -226,32 +234,43 @@ public class DocumentV1ApiTest { parameters.getControlHandler().onDone(VisitorControlHandler.CompletionCode.TIMEOUT, "timeout is OK"); }); response = driver.sendRequest("http://localhost/document/v1?cluster=content&bucketSpace=default&wantedDocumentCount=1025&concurrency=123" + - "&selection=all%20the%20things&fieldSet=[id]&timeout=6"); - assertSameJson("{" + - " \"pathId\": \"/document/v1\"," + - " \"documents\": [" + - " {" + - " \"id\": \"id:space:music::one\"," + - " \"fields\": {" + - " \"artist\": \"Tom Waits\", " + - " \"embedding\": { \"type\": \"tensor(x[3])\", \"values\": [1.0,2.0,3.0] } " + - " }" + - " }," + - " {" + - " \"id\": \"id:space:music:n=1:two\"," + - " \"fields\": {" + - " \"artist\": \"Asa-Chan & Jun-Ray\", " + - " \"embedding\": { \"type\": \"tensor(x[3])\", \"values\": [4.0,5.0,6.0] } " + - " }" + - " }," + - " {" + - " \"id\": \"id:space:music:g=a:three\"," + - " \"fields\": {}" + - " }" + - " ]," + - " \"documentCount\": 3" + - "}", response.readAll()); + "&selection=all%20the%20things&fieldSet=[id]&timeout=6&tracelevel=9"); + assertSameJson(""" + { + "pathId": "/document/v1", + "documents": [ + { + "id": "id:space:music::one", + "fields": { + "artist": "Tom Waits",\s + "embedding": { "type": "tensor(x[3])", "values": [1.0,2.0,3.0] }\s + } + }, + { + "id": "id:space:music:n=1:two", + "fields": { + "artist": "Asa-Chan & Jun-Ray",\s + "embedding": { "type": "tensor(x[3])", "values": [4.0,5.0,6.0] }\s + } + }, + { + "id": "id:space:music:g=a:three", + "fields": {} + } + ], + "documentCount": 3, + "trace": [ + { "message": "Tracy Chapman" }, + { + "fork": [ + { "message": "Fast Car" }, + { "message": "Baby Can I Hold You" } + ] + } + ] + }""", response.readAll()); assertEquals(200, response.getStatus()); + access.visitorTrace = null; // GET at root is a visit. Streaming mode can be specified with &stream=true access.expect(tokens); @@ -330,6 +349,7 @@ public class DocumentV1ApiTest { " \"message\": \"failure?\"" + "}", response.readAll()); assertEquals(200, response.getStatus()); + assertNull(response.getResponse().headers().get("X-Vespa-Ignored-Fields")); // POST with namespace and document type is a restricted visit with a required destination cluster ("destinationCluster") access.expect(parameters -> { @@ -376,13 +396,15 @@ public class DocumentV1ApiTest { response = driver.sendRequest("http://localhost/document/v1/space/music/docid?selection=true&cluster=content&timeChunk=10", PUT, "{" + " \"fields\": {" + - " \"artist\": { \"assign\": \"Lisa Ekdahl\" }" + + " \"artist\": { \"assign\": \"Lisa Ekdahl\" }, " + + " \"nonexisting\": { \"assign\": \"Ignored\" }" + " }" + "}"); assertSameJson("{" + " \"pathId\": \"/document/v1/space/music/docid\"" + "}", response.readAll()); assertEquals(200, response.getStatus()); + assertEquals("true", response.getResponse().headers().get("X-Vespa-Ignored-Fields").get(0).toString()); // PUT with namespace, document type and group is also a restricted visit which requires a cluster. access.expect(parameters -> { @@ -907,6 +929,7 @@ public class DocumentV1ApiTest { private final AtomicReference<Consumer<VisitorParameters>> expectations = new AtomicReference<>(); private final Set<AckToken> outstanding = new CopyOnWriteArraySet<>(); private final MockAsyncSession session = new MockAsyncSession(); + private Trace visitorTrace; MockDocumentAccess(DocumentmanagerConfig config) { super(new DocumentAccessParams().setDocumentmanagerConfig(config)); @@ -932,7 +955,7 @@ public class DocumentV1ApiTest { } @Override public boolean isDone() { return false; } @Override public ProgressToken getProgress() { return null; } - @Override public Trace getTrace() { return null; } + @Override public Trace getTrace() { return visitorTrace; } @Override public boolean waitUntilDone(long timeoutMs) { return false; } @Override public void ack(AckToken token) { assertTrue(outstanding.remove(token)); } @Override public void abort() { } diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerV3Test.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerV3Test.java index 5b8b5b1827f..dbbe664c9f8 100644 --- a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerV3Test.java +++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerV3Test.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class FeedHandlerV3Test { + final CollectingMetric metric = new CollectingMetric(); private final Executor simpleThreadpool = Executors.newCachedThreadPool(); @@ -101,7 +102,6 @@ public class FeedHandlerV3Test { request.getJDiscRequest().headers().add(Headers.DATA_FORMAT, FeedParams.DataFormat.JSON_UTF8.name()); request.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000"); request.getJDiscRequest().headers().add(Headers.CLIENT_ID, "client123"); - request.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST"); request.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4"); request.getJDiscRequest().headers().add(Headers.DRAIN, "true"); return request; diff --git a/vespaclient-core/src/main/java/com/yahoo/feedapi/Feeder.java b/vespaclient-core/src/main/java/com/yahoo/feedapi/Feeder.java index cd888b11d64..d8b69bd4a85 100644 --- a/vespaclient-core/src/main/java/com/yahoo/feedapi/Feeder.java +++ b/vespaclient-core/src/main/java/com/yahoo/feedapi/Feeder.java @@ -52,9 +52,8 @@ public abstract class Feeder { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); - message = "(no message) " + sw.toString(); + message = "(no message) " + sw; } - addError("ERROR: " + message); } @@ -84,17 +83,9 @@ public abstract class Feeder { if (createIfNonExistent && op.getDocumentUpdate() != null) { op.getDocumentUpdate().setCreateIfNonExistent(true); } - - // Done feeding. - if (op.getType() == FeedOperation.Type.INVALID) { - break; - } else { - sender.sendOperation(op); - } - } catch (XMLStreamException e) { - addException(e); - break; - } catch (NullPointerException e) { + if (op.getType() == FeedOperation.Type.INVALID) break; // Done feeding + sender.sendOperation(op); + } catch (XMLStreamException | NullPointerException e) { addException(e); break; } catch (Exception e) { diff --git a/vespaclient-java/src/main/java/com/yahoo/vespavisit/VdsVisit.java b/vespaclient-java/src/main/java/com/yahoo/vespavisit/VdsVisit.java index 42c23c4a961..9a5df96b705 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespavisit/VdsVisit.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespavisit/VdsVisit.java @@ -25,6 +25,7 @@ import org.apache.commons.cli.Options; import java.io.*; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.stream.Collectors; @@ -649,7 +650,7 @@ public class VdsVisit { out.println("Adding the following library specific parameters:"); for (Map.Entry<String, byte[]> entry : params.getLibraryParameters().entrySet()) { out.println(" " + entry.getKey() + " = " + - new String(entry.getValue(), Charset.forName("utf-8"))); + new String(entry.getValue(), StandardCharsets.UTF_8)); } } if (params.getPriority() != DocumentProtocol.Priority.NORMAL_3) { diff --git a/vespajlib/src/main/java/com/yahoo/io/GrowableByteBuffer.java b/vespajlib/src/main/java/com/yahoo/io/GrowableByteBuffer.java index cf728d69d18..479375d6b74 100644 --- a/vespajlib/src/main/java/com/yahoo/io/GrowableByteBuffer.java +++ b/vespajlib/src/main/java/com/yahoo/io/GrowableByteBuffer.java @@ -90,7 +90,7 @@ public class GrowableByteBuffer implements Comparable<GrowableByteBuffer> { //ByteBuffers and keep track of global position etc., much like //GrowableBufferOutputStream does it. - protected void grow(int newSize) { + public void grow(int newSize) { //create new buffer: ByteBuffer newByteBuf; if (buffer.isDirect()) { @@ -104,7 +104,7 @@ public class GrowableByteBuffer implements Comparable<GrowableByteBuffer> { //copy old contents and set correct position: int oldPos = buffer.position(); newByteBuf.position(0); - buffer.position(0); + buffer.flip(); newByteBuf.put(buffer); newByteBuf.position(oldPos); diff --git a/vespajlib/src/main/java/com/yahoo/tensor/functions/Join.java b/vespajlib/src/main/java/com/yahoo/tensor/functions/Join.java index 4ec5b196dbc..1009177761b 100644 --- a/vespajlib/src/main/java/com/yahoo/tensor/functions/Join.java +++ b/vespajlib/src/main/java/com/yahoo/tensor/functions/Join.java @@ -304,7 +304,7 @@ public class Join<NAMETYPE extends Name> extends PrimitiveTensorFunction<NAMETYP for (Iterator<Tensor.Cell> bIterator = b.cellIterator(); bIterator.hasNext(); ) { Map.Entry<TensorAddress, Double> bCell = bIterator.next(); TensorAddress combinedAddress = joinAddresses(aCell.getKey(), aToIndexes, - bCell.getKey(), bToIndexes, joinedType); + bCell.getKey(), bToIndexes, joinedType); if (combinedAddress == null) continue; // not combinable builder.cell(combinedAddress, combinator.applyAsDouble(aCell.getValue(), bCell.getValue())); } @@ -347,7 +347,7 @@ public class Join<NAMETYPE extends Name> extends PrimitiveTensorFunction<NAMETYP TensorAddress partialCommonAddress = partialCommonAddress(bCell, bIndexesInCommon); for (Tensor.Cell aCell : aCellsByCommonAddress.getOrDefault(partialCommonAddress, Collections.emptyList())) { TensorAddress combinedAddress = joinAddresses(aCell.getKey(), aIndexesInJoined, - bCell.getKey(), bIndexesInJoined, joinedType); + bCell.getKey(), bIndexesInJoined, joinedType); if (combinedAddress == null) continue; // not combinable double combinedValue = swapTensors ? combinator.applyAsDouble(bCell.getValue(), aCell.getValue()) : diff --git a/vespajlib/src/main/java/com/yahoo/tensor/serialization/TypedBinaryFormat.java b/vespajlib/src/main/java/com/yahoo/tensor/serialization/TypedBinaryFormat.java index eddb90ea84c..28cbf4ef64b 100644 --- a/vespajlib/src/main/java/com/yahoo/tensor/serialization/TypedBinaryFormat.java +++ b/vespajlib/src/main/java/com/yahoo/tensor/serialization/TypedBinaryFormat.java @@ -2,7 +2,6 @@ package com.yahoo.tensor.serialization; import com.yahoo.io.GrowableByteBuffer; -import com.yahoo.tensor.IndexedTensor; import com.yahoo.tensor.MixedTensor; import com.yahoo.tensor.Tensor; import com.yahoo.tensor.TensorType; @@ -34,9 +33,12 @@ public class TypedBinaryFormat { public static byte[] encode(Tensor tensor) { GrowableByteBuffer buffer = new GrowableByteBuffer(); + return asByteArray(encode(tensor, buffer)); + } + public static GrowableByteBuffer encode(Tensor tensor, GrowableByteBuffer buffer) { BinaryFormat encoder = getFormatEncoder(buffer, tensor); encoder.encode(buffer, tensor); - return asByteArray(buffer); + return buffer; } /** @@ -53,8 +55,8 @@ public class TypedBinaryFormat { } private static BinaryFormat getFormatEncoder(GrowableByteBuffer buffer, Tensor tensor) { - boolean hasMappedDimensions = tensor.type().dimensions().stream().anyMatch(d -> d.isMapped()); - boolean hasIndexedDimensions = tensor.type().dimensions().stream().anyMatch(d -> d.isIndexed()); + boolean hasMappedDimensions = tensor.type().dimensions().stream().anyMatch(TensorType.Dimension::isMapped); + boolean hasIndexedDimensions = tensor.type().dimensions().stream().anyMatch(TensorType.Dimension::isIndexed); boolean isMixed = hasMappedDimensions && hasIndexedDimensions; // TODO: Encoding as indexed if the implementation is mixed is not yet supported so use mixed format instead @@ -113,12 +115,11 @@ public class TypedBinaryFormat { private static void encodeValueType(GrowableByteBuffer buffer, TensorType.Value valueType) { switch (valueType) { - case DOUBLE: buffer.putInt1_4Bytes(DOUBLE_VALUE_TYPE); break; - case FLOAT: buffer.putInt1_4Bytes(FLOAT_VALUE_TYPE); break; - case BFLOAT16: buffer.putInt1_4Bytes(BFLOAT16_VALUE_TYPE); break; - case INT8: buffer.putInt1_4Bytes(INT8_VALUE_TYPE); break; - default: - throw new IllegalArgumentException("Attempt to encode unknown tensor value type: " + valueType); + case DOUBLE -> buffer.putInt1_4Bytes(DOUBLE_VALUE_TYPE); + case FLOAT -> buffer.putInt1_4Bytes(FLOAT_VALUE_TYPE); + case BFLOAT16 -> buffer.putInt1_4Bytes(BFLOAT16_VALUE_TYPE); + case INT8 -> buffer.putInt1_4Bytes(INT8_VALUE_TYPE); + default -> throw new IllegalArgumentException("Attempt to encode unknown tensor value type: " + valueType); } } diff --git a/vespajlib/src/main/java/com/yahoo/yolean/UncheckedInterruptedException.java b/vespajlib/src/main/java/com/yahoo/yolean/UncheckedInterruptedException.java index d3317b5fb26..934a1b17c70 100644 --- a/vespajlib/src/main/java/com/yahoo/yolean/UncheckedInterruptedException.java +++ b/vespajlib/src/main/java/com/yahoo/yolean/UncheckedInterruptedException.java @@ -17,7 +17,7 @@ public class UncheckedInterruptedException extends RuntimeException { this(cause.toString(), cause, restoreInterruptFlags); } - public UncheckedInterruptedException(String message, boolean restoreInterruptFlag) { this(message, null, false); } + public UncheckedInterruptedException(String message, boolean restoreInterruptFlag) { this(message, null, restoreInterruptFlag); } public UncheckedInterruptedException(String message, InterruptedException cause) { this(message, cause, false); } diff --git a/vespalib/CMakeLists.txt b/vespalib/CMakeLists.txt index 7aafb7c364e..6ecac23d5fa 100644 --- a/vespalib/CMakeLists.txt +++ b/vespalib/CMakeLists.txt @@ -42,6 +42,8 @@ vespa_define_module( src/tests/component src/tests/compress src/tests/compression + src/tests/coro/detached + src/tests/coro/lazy src/tests/cpu_usage src/tests/crc src/tests/crypto @@ -54,6 +56,7 @@ vespa_define_module( src/tests/data/smart_buffer src/tests/datastore/array_store src/tests/datastore/array_store_config + src/tests/datastore/buffer_stats src/tests/datastore/buffer_type src/tests/datastore/compact_buffer_candidates src/tests/datastore/datastore @@ -180,9 +183,9 @@ vespa_define_module( src/tests/util/bfloat16 src/tests/util/cgroup_resource_limits src/tests/util/file_area_freelist + src/tests/util/generation_hold_list src/tests/util/generationhandler src/tests/util/generationhandler_stress - src/tests/util/generation_holder src/tests/util/hamming src/tests/util/md5 src/tests/util/mmap_file_allocator @@ -201,9 +204,13 @@ vespa_define_module( src/tests/fastlib/text LIBS + src/vespa/fastlib/io + src/vespa/fastlib/text + src/vespa/fastlib/text/apps src/vespa/vespalib src/vespa/vespalib/btree src/vespa/vespalib/component + src/vespa/vespalib/coro src/vespa/vespalib/crypto src/vespa/vespalib/data src/vespa/vespalib/data/slime @@ -230,7 +237,4 @@ vespa_define_module( src/vespa/vespalib/time src/vespa/vespalib/trace src/vespa/vespalib/util - src/vespa/fastlib/io - src/vespa/fastlib/text - src/vespa/fastlib/text/apps ) diff --git a/vespalib/src/tests/btree/btree-stress/btree_stress_test.cpp b/vespalib/src/tests/btree/btree-stress/btree_stress_test.cpp index c68ff07491e..caed5c3543c 100644 --- a/vespalib/src/tests/btree/btree-stress/btree_stress_test.cpp +++ b/vespalib/src/tests/btree/btree-stress/btree_stress_test.cpp @@ -59,15 +59,13 @@ public: AtomicEntryRef add_relaxed(uint32_t value) { return AtomicEntryRef(add(value)); } void hold(const AtomicEntryRef& ref) { _store.holdElem(ref.load_relaxed(), 1); } EntryRef move(EntryRef ref); - void transfer_hold_lists(generation_t gen) { _store.transferHoldLists(gen); } - void trim_hold_lists(generation_t gen) { _store.trimHoldLists(gen); } + void assign_generation(generation_t current_gen) { _store.assign_generation(current_gen); } + void reclaim_memory(generation_t gen) { _store.reclaim_memory(gen); } uint32_t get(EntryRef ref) const { return _store.getEntry(ref); } uint32_t get_acquire(const AtomicEntryRef& ref) const { return get(ref.load_acquire()); } uint32_t get_relaxed(const AtomicEntryRef& ref) const { return get(ref.load_relaxed()); } std::unique_ptr<vespalib::datastore::CompactingBuffers> start_compact(); static constexpr bool is_indirect = true; - static uint32_t get_offset_bits() { return StoreRefType::offset_bits; } - static uint32_t get_num_buffers() { return StoreRefType::numBuffers(); } bool has_held_buffers() const noexcept { return _store.has_held_buffers(); } }; @@ -82,7 +80,7 @@ std::unique_ptr<vespalib::datastore::CompactingBuffers> RealIntStore::start_compact() { // Use a compaction strategy that will compact all active buffers - CompactionStrategy compaction_strategy(0.0, 0.0, get_num_buffers(), 1.0); + auto compaction_strategy = CompactionStrategy::make_compact_all_active_buffers_strategy(); CompactionSpec compaction_spec(true, false); return _store.start_compact_worst_buffers(compaction_spec, compaction_strategy); } @@ -120,8 +118,8 @@ public: static uint32_t add(uint32_t value) noexcept { return value; } static uint32_t add_relaxed(uint32_t value) noexcept { return value; } static void hold(uint32_t) noexcept { } - static void transfer_hold_lists(generation_t) noexcept { } - static void trim_hold_lists(generation_t) noexcept { } + static void assign_generation(generation_t) noexcept { } + static void reclaim_memory(generation_t) noexcept { } static uint32_t get(uint32_t value) noexcept { return value; } static uint32_t get_acquire(uint32_t value) noexcept { return value; } static uint32_t get_relaxed(uint32_t value) noexcept { return value; } @@ -276,15 +274,15 @@ Fixture<Params>::commit() auto &allocator = _tree.getAllocator(); allocator.freeze(); auto current_gen = _generationHandler.getCurrentGeneration(); - allocator.transferHoldLists(current_gen); - _keys.transfer_hold_lists(current_gen); - _values.transfer_hold_lists(current_gen); - allocator.transferHoldLists(_generationHandler.getCurrentGeneration()); + allocator.assign_generation(current_gen); + _keys.assign_generation(current_gen); + _values.assign_generation(current_gen); + allocator.assign_generation(_generationHandler.getCurrentGeneration()); _generationHandler.incGeneration(); - auto first_used_gen = _generationHandler.getFirstUsedGeneration(); - allocator.trimHoldLists(first_used_gen); - _keys.trim_hold_lists(first_used_gen); - _values.trim_hold_lists(first_used_gen); + auto oldest_used_gen = _generationHandler.get_oldest_used_generation(); + allocator.reclaim_memory(oldest_used_gen); + _keys.reclaim_memory(oldest_used_gen); + _values.reclaim_memory(oldest_used_gen); } template <typename Params> @@ -329,7 +327,7 @@ void Fixture<Params>::compact_tree() { // Use a compaction strategy that will compact all active buffers - CompactionStrategy compaction_strategy(0.0, 0.0, RefType::numBuffers(), 1.0); + auto compaction_strategy = CompactionStrategy::make_compact_all_active_buffers_strategy(); _tree.compact_worst(compaction_strategy); _writeItr = _tree.begin(); _compact_tree.track_compacted(); diff --git a/vespalib/src/tests/btree/btree_store/btree_store_test.cpp b/vespalib/src/tests/btree/btree_store/btree_store_test.cpp index 4da34c64ed9..0370b1ce2eb 100644 --- a/vespalib/src/tests/btree/btree_store/btree_store_test.cpp +++ b/vespalib/src/tests/btree/btree_store/btree_store_test.cpp @@ -31,9 +31,9 @@ protected: void inc_generation() { _store.freeze(); - _store.transferHoldLists(_gen_handler.getCurrentGeneration()); + _store.assign_generation(_gen_handler.getCurrentGeneration()); _gen_handler.incGeneration(); - _store.trimHoldLists(_gen_handler.getFirstUsedGeneration()); + _store.reclaim_memory(_gen_handler.get_oldest_used_generation()); } EntryRef add_sequence(int start_key, int end_key) diff --git a/vespalib/src/tests/btree/btree_test.cpp b/vespalib/src/tests/btree/btree_test.cpp index 92f55681c0f..f2896cb783c 100644 --- a/vespalib/src/tests/btree/btree_test.cpp +++ b/vespalib/src/tests/btree/btree_test.cpp @@ -163,9 +163,9 @@ void cleanup(GenerationHandler & g, ManagerType & m) { m.freeze(); - m.transferHoldLists(g.getCurrentGeneration()); + m.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - m.trimHoldLists(g.getFirstUsedGeneration()); + m.reclaim_memory(g.get_oldest_used_generation()); } template <typename ManagerType, typename NodeType> @@ -862,19 +862,21 @@ TEST_F(BTreeTest, require_that_we_can_insert_and_remove_from_tree) } // compact full tree by calling incremental compaction methods in a loop { + // Use a compaction strategy that will compact all active buffers + auto compaction_strategy = CompactionStrategy::make_compact_all_active_buffers_strategy(); MyTree::NodeAllocatorType &manager = tree.getAllocator(); - std::vector<uint32_t> toHold = manager.startCompact(); + auto compacting_buffers = manager.start_compact_worst(compaction_strategy); MyTree::Iterator itr = tree.begin(); tree.setRoot(itr.moveFirstLeafNode(tree.getRoot())); while (itr.valid()) { // LOG(info, "Leaf moved to %d", UNWRAP(itr.getKey())); itr.moveNextLeafNode(); } - manager.finishCompact(toHold); + compacting_buffers->finish(); manager.freeze(); - manager.transferHoldLists(g.getCurrentGeneration()); + manager.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - manager.trimHoldLists(g.getFirstUsedGeneration()); + manager.reclaim_memory(g.get_oldest_used_generation()); } // remove entries for (size_t i = 0; i < numEntries; ++i) { @@ -1104,9 +1106,9 @@ TEST_F(BTreeTest, require_that_memory_usage_is_calculated) EXPECT_TRUE(assertMemoryUsage(mu, tm.getMemoryUsage())); // trim hold lists - tm.transferHoldLists(gh.getCurrentGeneration()); + tm.assign_generation(gh.getCurrentGeneration()); gh.incGeneration(); - tm.trimHoldLists(gh.getFirstUsedGeneration()); + tm.reclaim_memory(gh.get_oldest_used_generation()); mu = vespalib::MemoryUsage(); mu.incAllocatedBytes(adjustAllocatedBytes(initialInternalNodes, sizeof(INode))); mu.incAllocatedBytes(adjustAllocatedBytes(initialLeafNodes, sizeof(LNode))); @@ -1280,9 +1282,9 @@ TEST_F(BTreeTest, require_that_small_nodes_works) s.clear(root); s.clearBuilder(); s.freeze(); - s.transferHoldLists(g.getCurrentGeneration()); + s.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - s.trimHoldLists(g.getFirstUsedGeneration()); + s.reclaim_memory(g.get_oldest_used_generation()); } namespace { @@ -1414,9 +1416,9 @@ TEST_F(BTreeTest, require_that_apply_works) s.clear(root); s.clearBuilder(); s.freeze(); - s.transferHoldLists(g.getCurrentGeneration()); + s.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - s.trimHoldLists(g.getFirstUsedGeneration()); + s.reclaim_memory(g.get_oldest_used_generation()); } class MyTreeTestIterator : public MyTree::Iterator @@ -1551,9 +1553,9 @@ inc_generation(GenerationHandler &g, Tree &t) { auto &s = t.getAllocator(); s.freeze(); - s.transferHoldLists(g.getCurrentGeneration()); + s.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - s.trimHoldLists(g.getFirstUsedGeneration()); + s.reclaim_memory(g.get_oldest_used_generation()); } template <typename Tree> diff --git a/vespalib/src/tests/btree/btreeaggregation_test.cpp b/vespalib/src/tests/btree/btreeaggregation_test.cpp index f4300499fcd..fb394df9861 100644 --- a/vespalib/src/tests/btree/btreeaggregation_test.cpp +++ b/vespalib/src/tests/btree/btreeaggregation_test.cpp @@ -15,6 +15,7 @@ #include <vespa/vespalib/btree/btreestore.hpp> #include <vespa/vespalib/btree/btreeaggregator.hpp> #include <vespa/vespalib/datastore/buffer_type.hpp> +#include <vespa/vespalib/datastore/compaction_strategy.h> #include <vespa/vespalib/test/btree/btree_printer.h> #include <vespa/vespalib/testkit/testapp.h> #include <vespa/vespalib/util/rand48.h> @@ -28,6 +29,7 @@ LOG_SETUP("btreeaggregation_test"); using vespalib::GenerationHandler; +using vespalib::datastore::CompactionStrategy; using vespalib::datastore::EntryRef; namespace vespalib::btree { @@ -270,9 +272,9 @@ void freezeTree(GenerationHandler &g, ManagerType &m) { m.freeze(); - m.transferHoldLists(g.getCurrentGeneration()); + m.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - m.trimHoldLists(g.getFirstUsedGeneration()); + m.reclaim_memory(g.get_oldest_used_generation()); } template <typename ManagerType> @@ -877,19 +879,21 @@ Test::requireThatWeCanInsertAndRemoveFromTree() } // compact full tree by calling incremental compaction methods in a loop { + // Use a compaction strategy that will compact all active buffers + auto compaction_strategy = CompactionStrategy::make_compact_all_active_buffers_strategy(); MyTree::NodeAllocatorType &manager = tree.getAllocator(); - std::vector<uint32_t> toHold = manager.startCompact(); + auto compacting_buffers = manager.start_compact_worst(compaction_strategy); MyTree::Iterator itr = tree.begin(); tree.setRoot(itr.moveFirstLeafNode(tree.getRoot())); while (itr.valid()) { // LOG(info, "Leaf moved to %d", UNWRAP(itr.getKey())); itr.moveNextLeafNode(); } - manager.finishCompact(toHold); + compacting_buffers->finish(); manager.freeze(); - manager.transferHoldLists(g.getCurrentGeneration()); + manager.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - manager.trimHoldLists(g.getFirstUsedGeneration()); + manager.reclaim_memory(g.get_oldest_used_generation()); } // remove entries for (size_t i = 0; i < numEntries; ++i) { @@ -1186,9 +1190,9 @@ Test::requireThatSmallNodesWorks() s.clear(root); s.clearBuilder(); s.freeze(); - s.transferHoldLists(g.getCurrentGeneration()); + s.assign_generation(g.getCurrentGeneration()); g.incGeneration(); - s.trimHoldLists(g.getFirstUsedGeneration()); + s.reclaim_memory(g.get_oldest_used_generation()); } void diff --git a/vespalib/src/tests/btree/frozenbtree_test.cpp b/vespalib/src/tests/btree/frozenbtree_test.cpp index 01748b9edeb..3471d5dc3df 100644 --- a/vespalib/src/tests/btree/frozenbtree_test.cpp +++ b/vespalib/src/tests/btree/frozenbtree_test.cpp @@ -114,7 +114,7 @@ FrozenBTreeTest::freeTree(bool verbose) static_cast<uint64_t>(_intTree->getUsedMemory()), static_cast<uint64_t>(_intTree->getHeldMemory())); _intTree->dropFrozen(); - _intTree->removeOldGenerations(_intTree->getGeneration() + 1); + _intTree->reclaim_memory(_intTree->getGeneration() + 1); LOG(info, "freeTree after unhold: %" PRIu64 " (%" PRIu64 " held)", static_cast<uint64_t>(_intTree->getUsedMemory()), @@ -134,9 +134,9 @@ FrozenBTreeTest::freeTree(bool verbose) (void) verbose; _tree->clear(*_allocator); _allocator->freeze(); - _allocator->transferHoldLists(_generationHandler->getCurrentGeneration()); + _allocator->assign_generation(_generationHandler->getCurrentGeneration()); _generationHandler->incGeneration(); - _allocator->trimHoldLists(_generationHandler->getFirstUsedGeneration()); + _allocator->reclaim_memory(_generationHandler->get_oldest_used_generation()); delete _tree; _tree = NULL; delete _allocator; @@ -425,7 +425,7 @@ FrozenBTreeTest::Main() EXPECT_TRUE(_tree->getFrozenView(*_allocator).empty()); _allocator->freeze(); EXPECT_FALSE(_tree->getFrozenView(*_allocator).empty()); - _allocator->transferHoldLists(_generationHandler->getCurrentGeneration()); + _allocator->assign_generation(_generationHandler->getCurrentGeneration()); lookupFrozenRandomValues(*_tree, *_allocator, _randomValues); traverseTreeIterator(*_tree, *_allocator, diff --git a/vespalib/src/tests/coro/detached/CMakeLists.txt b/vespalib/src/tests/coro/detached/CMakeLists.txt new file mode 100644 index 00000000000..237b8615fec --- /dev/null +++ b/vespalib/src/tests/coro/detached/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(vespalib_detached_test_app TEST + SOURCES + detached_test.cpp + DEPENDS + vespalib + GTest::GTest +) +vespa_add_test(NAME vespalib_detached_test_app COMMAND vespalib_detached_test_app) diff --git a/vespalib/src/tests/coro/detached/detached_test.cpp b/vespalib/src/tests/coro/detached/detached_test.cpp new file mode 100644 index 00000000000..f23d16cc75c --- /dev/null +++ b/vespalib/src/tests/coro/detached/detached_test.cpp @@ -0,0 +1,19 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/vespalib/coro/detached.h> +#include <vespa/vespalib/gtest/gtest.h> + +using vespalib::coro::Detached; + +Detached set_result(int &res, int value) { + res = value; + co_return; +} + +TEST(DetachedTest, call_detached_coroutine) { + int result = 0; + set_result(result, 42); + EXPECT_EQ(result, 42); +} + +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/vespalib/src/tests/coro/lazy/CMakeLists.txt b/vespalib/src/tests/coro/lazy/CMakeLists.txt new file mode 100644 index 00000000000..daa11eb3576 --- /dev/null +++ b/vespalib/src/tests/coro/lazy/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(vespalib_lazy_test_app TEST + SOURCES + lazy_test.cpp + DEPENDS + vespalib + GTest::GTest +) +vespa_add_test(NAME vespalib_lazy_test_app COMMAND vespalib_lazy_test_app) diff --git a/vespalib/src/tests/coro/lazy/lazy_test.cpp b/vespalib/src/tests/coro/lazy/lazy_test.cpp new file mode 100644 index 00000000000..b838152249e --- /dev/null +++ b/vespalib/src/tests/coro/lazy/lazy_test.cpp @@ -0,0 +1,121 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/vespalib/coro/lazy.h> +#include <vespa/vespalib/coro/sync_wait.h> +#include <vespa/vespalib/util/require.h> +#include <vespa/vespalib/gtest/gtest.h> +#include <mutex> + +#include <thread> + +using vespalib::coro::Lazy; +using vespalib::coro::sync_wait; + +std::mutex thread_lock; +std::vector<std::thread> threads; +struct JoinThreads { + ~JoinThreads() { + for (auto &thread: threads) { + thread.join(); + } + threads.clear(); + } +}; + +auto run_in_other_thread() { + struct awaiter { + bool await_ready() const noexcept { return false; } + void await_suspend(std::coroutine_handle<> handle) const { + auto guard = std::lock_guard(thread_lock); + threads.push_back(std::thread(handle)); + } + void await_resume() const noexcept {} + }; + return awaiter(); +} + +Lazy<int> make_lazy(int value) { + co_return value; +} + +Lazy<int> async_add_values(int a, int b) { + auto lazy_a = make_lazy(a); + auto lazy_b = make_lazy(b); + co_return (co_await lazy_a + co_await lazy_b); +} + +Lazy<int> async_sum(Lazy<int> a, Lazy<int> b) { + co_return (co_await a + co_await b); +} + +Lazy<std::unique_ptr<int>> move_only_int() { + co_return std::make_unique<int>(123); +} + +Lazy<int> extract_rvalue() { + auto res = co_await move_only_int(); + co_return *res; +} + +Lazy<int> will_throw() { + REQUIRE_FAILED("failed on purpose"); + co_return 123; +} + +template<typename T> +Lazy<T> forward_value(Lazy<T> value) { + co_return co_await std::move(value); +} + +template <typename T> +Lazy<T> switch_thread(Lazy<T> value) { + std::cerr << "switching from thread " << std::this_thread::get_id() << std::endl; + co_await run_in_other_thread(); + std::cerr << "........... to thread " << std::this_thread::get_id() << std::endl; + co_return co_await value; +} + +TEST(LazyTest, simple_lazy_value) { + auto lazy = make_lazy(42); + auto result = sync_wait(lazy); + EXPECT_EQ(result, 42); +} + +TEST(LazyTest, async_sum_of_async_values) { + auto lazy = async_add_values(10, 20); + auto result = sync_wait(lazy); + EXPECT_EQ(result, 30); +} + +TEST(LazyTest, async_sum_of_external_async_values) { + auto a = make_lazy(100); + auto b = make_lazy(200); + auto lazy = async_sum(std::move(a), std::move(b)); + auto result = sync_wait(lazy); + EXPECT_EQ(result, 300); +} + +TEST(LazyTest, extract_rvalue_from_lazy_in_coroutine) { + auto lazy = extract_rvalue(); + auto result = sync_wait(lazy); + EXPECT_EQ(result, 123); +} + +TEST(LazyTest, extract_rvalue_from_lazy_in_sync_wait) { + auto result = sync_wait(move_only_int()); + EXPECT_EQ(*result, 123); +} + +TEST(LazyTest, calculate_result_in_another_thread) { + JoinThreads thread_guard; + auto result = sync_wait(switch_thread(make_lazy(7))); + EXPECT_EQ(result, 7); +} + +TEST(LazyTest, exceptions_are_propagated) { + JoinThreads thread_guard; + auto lazy = switch_thread(forward_value(will_throw())); + EXPECT_THROW(sync_wait(lazy), vespalib::RequireFailedException); +} + +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/vespalib/src/tests/datastore/array_store/array_store_test.cpp b/vespalib/src/tests/datastore/array_store/array_store_test.cpp index 1e8632aee95..e9ea1a67156 100644 --- a/vespalib/src/tests/datastore/array_store/array_store_test.cpp +++ b/vespalib/src/tests/datastore/array_store/array_store_test.cpp @@ -20,7 +20,7 @@ using vespalib::alloc::MemoryAllocator; using vespalib::alloc::test::MemoryAllocatorObserver; using AllocStats = MemoryAllocatorObserver::Stats; -using BufferStats = vespalib::datastore::test::BufferStats; +using TestBufferStats = vespalib::datastore::test::BufferStats; using MemStats = vespalib::datastore::test::MemStats; namespace { @@ -98,16 +98,16 @@ struct ArrayStoreTest : public TestT } void assertBufferState(EntryRef ref, const MemStats& expStats) const { EXPECT_EQ(expStats._used, store.bufferState(ref).size()); - EXPECT_EQ(expStats._hold, store.bufferState(ref).getHoldElems()); - EXPECT_EQ(expStats._dead, store.bufferState(ref).getDeadElems()); + EXPECT_EQ(expStats._hold, store.bufferState(ref).stats().hold_elems()); + EXPECT_EQ(expStats._dead, store.bufferState(ref).stats().dead_elems()); } - void assert_buffer_stats(EntryRef ref, const BufferStats& exp_stats) const { + void assert_buffer_stats(EntryRef ref, const TestBufferStats& exp_stats) const { auto& state = store.bufferState(ref); EXPECT_EQ(exp_stats._used, state.size()); - EXPECT_EQ(exp_stats._hold, state.getHoldElems()); - EXPECT_EQ(exp_stats._dead, state.getDeadElems()); - EXPECT_EQ(exp_stats._extra_used, state.getExtraUsedBytes()); - EXPECT_EQ(exp_stats._extra_hold, state.getExtraHoldBytes()); + EXPECT_EQ(exp_stats._hold, state.stats().hold_elems()); + EXPECT_EQ(exp_stats._dead, state.stats().dead_elems()); + EXPECT_EQ(exp_stats._extra_used, state.stats().extra_used_bytes()); + EXPECT_EQ(exp_stats._extra_hold, state.stats().extra_hold_bytes()); } void assertMemoryUsage(const MemStats expStats) const { MemoryUsage act = store.getMemoryUsage(); @@ -123,7 +123,7 @@ struct ArrayStoreTest : public TestT void assert_ref_reused(const EntryVector& first, const EntryVector& second, bool should_reuse) { EntryRef ref1 = add(first); remove(ref1); - trimHoldLists(); + reclaim_memory(); EntryRef ref2 = add(second); EXPECT_EQ(should_reuse, (ref2 == ref1)); assertGet(ref2, second); @@ -136,9 +136,9 @@ struct ArrayStoreTest : public TestT } return EntryRef(); } - void trimHoldLists() { - store.transferHoldLists(generation++); - store.trimHoldLists(generation); + void reclaim_memory() { + store.assign_generation(generation++); + store.reclaim_memory(generation); } void compactWorst(bool compactMemory, bool compactAddressSpace) { CompactionSpec compaction_spec(compactMemory, compactAddressSpace); @@ -205,10 +205,10 @@ VESPA_GTEST_INSTANTIATE_TEST_SUITE_P(NumberStoreFreeListsDisabledMultiTest, TEST_P(NumberStoreTest, control_static_sizes) { #ifdef _LIBCPP_VERSION - EXPECT_EQ(464u, sizeof(store)); + EXPECT_EQ(472u, sizeof(store)); EXPECT_EQ(304u, sizeof(NumberStoreTest::ArrayStoreType::DataStoreType)); #else - EXPECT_EQ(496u, sizeof(store)); + EXPECT_EQ(504u, sizeof(store)); EXPECT_EQ(336u, sizeof(NumberStoreTest::ArrayStoreType::DataStoreType)); #endif EXPECT_EQ(112u, sizeof(NumberStoreTest::ArrayStoreType::SmallBufferType)); @@ -280,13 +280,13 @@ TEST_P(NumberStoreFreeListsDisabledTest, large_arrays_are_NOT_allocated_from_fre TEST_P(NumberStoreTest, track_size_of_large_array_allocations_with_free_lists_enabled) { EntryRef ref = add({1,2,3,4}); - assert_buffer_stats(ref, BufferStats().used(2).hold(0).dead(1).extra_used(16)); + assert_buffer_stats(ref, TestBufferStats().used(2).hold(0).dead(1).extra_used(16)); remove({1,2,3,4}); - assert_buffer_stats(ref, BufferStats().used(2).hold(1).dead(1).extra_hold(16).extra_used(16)); - trimHoldLists(); - assert_buffer_stats(ref, BufferStats().used(2).hold(0).dead(2).extra_used(0)); + assert_buffer_stats(ref, TestBufferStats().used(2).hold(1).dead(1).extra_hold(16).extra_used(16)); + reclaim_memory(); + assert_buffer_stats(ref, TestBufferStats().used(2).hold(0).dead(2).extra_used(0)); add({5,6,7,8,9}); - assert_buffer_stats(ref, BufferStats().used(2).hold(0).dead(1).extra_used(20)); + assert_buffer_stats(ref, TestBufferStats().used(2).hold(0).dead(1).extra_used(20)); } TEST_F(SmallOffsetNumberStoreTest, new_underlying_buffer_is_allocated_when_current_is_full) @@ -316,7 +316,7 @@ test_compaction(NumberStoreBasicTest &f) EntryRef size2Ref = f.add({2,2}); EntryRef size3Ref = f.add({3,3,3}); f.remove(f.add({5,5})); - f.trimHoldLists(); + f.reclaim_memory(); f.assertBufferState(size1Ref, MemStats().used(1).dead(0)); f.assertBufferState(size2Ref, MemStats().used(4).dead(2)); f.assertBufferState(size3Ref, MemStats().used(2).dead(1)); // Note: First element is reserved @@ -335,7 +335,7 @@ test_compaction(NumberStoreBasicTest &f) EXPECT_NE(size2BufferId, f.getBufferId(f.getEntryRef({2,2}))); f.assertGet(size2Ref, {2,2}); // Old ref should still point to data. EXPECT_TRUE(f.store.bufferState(size2Ref).isOnHold()); - f.trimHoldLists(); + f.reclaim_memory(); EXPECT_TRUE(f.store.bufferState(size2Ref).isFree()); } @@ -360,7 +360,7 @@ void testCompaction(NumberStoreTest &f, bool compactMemory, bool compactAddressS f.remove(f.add({5,5,5})); f.remove(f.add({6})); f.remove(f.add({7})); - f.trimHoldLists(); + f.reclaim_memory(); f.assertBufferState(size1Ref, MemStats().used(3).dead(2)); f.assertBufferState(size2Ref, MemStats().used(2).dead(0)); f.assertBufferState(size3Ref, MemStats().used(6).dead(3)); @@ -397,7 +397,7 @@ void testCompaction(NumberStoreTest &f, bool compactMemory, bool compactAddressS EXPECT_FALSE(f.store.bufferState(size1Ref).isOnHold()); } EXPECT_FALSE(f.store.bufferState(size2Ref).isOnHold()); - f.trimHoldLists(); + f.reclaim_memory(); if (compactMemory) { EXPECT_TRUE(f.store.bufferState(size3Ref).isFree()); } else { @@ -436,7 +436,7 @@ TEST_P(NumberStoreTest, used_onHold_and_dead_memory_usage_is_tracked_for_small_a assertMemoryUsage(exp.used(entrySize() * 3)); remove({1,2,3}); assertMemoryUsage(exp.hold(entrySize() * 3)); - trimHoldLists(); + reclaim_memory(); assertMemoryUsage(exp.holdToDead(entrySize() * 3)); } @@ -447,7 +447,7 @@ TEST_P(NumberStoreTest, used_onHold_and_dead_memory_usage_is_tracked_for_large_a assertMemoryUsage(exp.used(largeArraySize() + entrySize() * 4)); remove({1,2,3,4}); assertMemoryUsage(exp.hold(largeArraySize() + entrySize() * 4)); - trimHoldLists(); + reclaim_memory(); assertMemoryUsage(exp.decUsed(entrySize() * 4).decHold(largeArraySize() + entrySize() * 4). dead(largeArraySize())); } diff --git a/vespalib/src/tests/datastore/buffer_stats/CMakeLists.txt b/vespalib/src/tests/datastore/buffer_stats/CMakeLists.txt new file mode 100644 index 00000000000..2463f584133 --- /dev/null +++ b/vespalib/src/tests/datastore/buffer_stats/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(vespalib_datastore_buffer_stats_test_app TEST + SOURCES + buffer_stats_test.cpp + DEPENDS + vespalib + GTest::GTest +) +vespa_add_test(NAME vespalib_datastore_buffer_stats_test_app COMMAND vespalib_datastore_buffer_stats_test_app) diff --git a/vespalib/src/tests/datastore/buffer_stats/buffer_stats_test.cpp b/vespalib/src/tests/datastore/buffer_stats/buffer_stats_test.cpp new file mode 100644 index 00000000000..09b2590a5f3 --- /dev/null +++ b/vespalib/src/tests/datastore/buffer_stats/buffer_stats_test.cpp @@ -0,0 +1,34 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/vespalib/datastore/buffer_stats.h> +#include <vespa/vespalib/datastore/memory_stats.h> +#include <vespa/vespalib/gtest/gtest.h> + +using namespace vespalib::datastore; + +TEST(BufferStatsTest, buffer_stats_to_memory_stats) +{ + InternalBufferStats buf; + buf.set_alloc_elems(17); + buf.pushed_back(7); + buf.set_dead_elems(5); + buf.set_hold_elems(3); + buf.inc_extra_used_bytes(13); + buf.inc_extra_hold_bytes(11); + + MemoryStats mem; + constexpr size_t es = 8; + buf.add_to_mem_stats(es, mem); + + EXPECT_EQ(17, mem._allocElems); + EXPECT_EQ(7, mem._usedElems); + EXPECT_EQ(5, mem._deadElems); + EXPECT_EQ(3, mem._holdElems); + EXPECT_EQ(17 * es + 13, mem._allocBytes); + EXPECT_EQ(7 * es + 13, mem._usedBytes); + EXPECT_EQ(5 * es, mem._deadBytes); + EXPECT_EQ(3 * es + 11, mem._holdBytes); +} + +GTEST_MAIN_RUN_ALL_TESTS() + diff --git a/vespalib/src/tests/datastore/datastore/datastore_test.cpp b/vespalib/src/tests/datastore/datastore/datastore_test.cpp index 9522aa1e0dc..645871d3ef6 100644 --- a/vespalib/src/tests/datastore/datastore/datastore_test.cpp +++ b/vespalib/src/tests/datastore/datastore/datastore_test.cpp @@ -17,7 +17,6 @@ using vespalib::alloc::MemoryAllocator; class MyStore : public DataStore<int, EntryRefT<3, 2> > { private: using ParentType = DataStore<int, EntryRefT<3, 2> >; - using ParentType::_primary_buffer_ids; public: MyStore() {} explicit MyStore(std::unique_ptr<BufferType<int>> type) @@ -29,14 +28,11 @@ public: void holdElem(EntryRef ref, uint64_t len) { ParentType::holdElem(ref, len); } - void transferHoldLists(generation_t generation) { - ParentType::transferHoldLists(generation); + void assign_generation(generation_t current_gen) { + ParentType::assign_generation(current_gen); } - void trimElemHoldList(generation_t usedGen) override { - ParentType::trimElemHoldList(usedGen); - } - void incDead(EntryRef ref, uint64_t dead) { - ParentType::incDead(ref, dead); + void reclaim_entry_refs(generation_t oldest_used_gen) override { + ParentType::reclaim_entry_refs(oldest_used_gen); } void ensureBufferCapacity(size_t sizeNeeded) { ParentType::ensureBufferCapacity(0, sizeNeeded); @@ -47,7 +43,7 @@ public: void switch_primary_buffer() { ParentType::switch_primary_buffer(0, 0u); } - size_t primary_buffer_id() const { return _primary_buffer_ids[0]; } + size_t primary_buffer_id() const { return get_primary_buffer_id(0); } BufferState& get_active_buffer_state() { return ParentType::getBufferState(primary_buffer_id()); } @@ -55,7 +51,7 @@ public: using GrowthStats = std::vector<int>; -using BufferStats = std::vector<int>; +using BufferIds = std::vector<int>; constexpr float ALLOC_GROW_FACTOR = 0.4; constexpr size_t HUGE_PAGE_ARRAY_SIZE = (MemoryAllocator::HUGEPAGE_SIZE / sizeof(int)); @@ -124,8 +120,8 @@ public: ++i; } } - BufferStats getBuffers(size_t bufs) { - BufferStats buffers; + BufferIds getBuffers(size_t bufs) { + BufferIds buffers; while (buffers.size() < bufs) { RefType iRef = (_type.getArraySize() == 1) ? (_store.template allocator<DataType>(_typeId).alloc().ref) : @@ -143,8 +139,8 @@ public: using MyRef = MyStore::RefType; void -assertMemStats(const DataStoreBase::MemStats &exp, - const DataStoreBase::MemStats &act) +assertMemStats(const MemoryStats &exp, + const MemoryStats &act) { EXPECT_EQ(exp._allocElems, act._allocElems); EXPECT_EQ(exp._usedElems, act._usedElems); @@ -265,29 +261,29 @@ TEST(DataStoreTest, require_that_we_can_hold_and_trim_buffers) s.switch_primary_buffer(); EXPECT_EQ(1u, s.primary_buffer_id()); s.holdBuffer(0); // hold last buffer - s.transferHoldLists(10); + s.assign_generation(10); EXPECT_EQ(1u, MyRef(s.addEntry(2)).bufferId()); s.switch_primary_buffer(); EXPECT_EQ(2u, s.primary_buffer_id()); s.holdBuffer(1); // hold last buffer - s.transferHoldLists(20); + s.assign_generation(20); EXPECT_EQ(2u, MyRef(s.addEntry(3)).bufferId()); s.switch_primary_buffer(); EXPECT_EQ(3u, s.primary_buffer_id()); s.holdBuffer(2); // hold last buffer - s.transferHoldLists(30); + s.assign_generation(30); EXPECT_EQ(3u, MyRef(s.addEntry(4)).bufferId()); s.holdBuffer(3); // hold current buffer - s.transferHoldLists(40); + s.assign_generation(40); EXPECT_TRUE(s.getBufferState(0).size() != 0); EXPECT_TRUE(s.getBufferState(1).size() != 0); EXPECT_TRUE(s.getBufferState(2).size() != 0); EXPECT_TRUE(s.getBufferState(3).size() != 0); - s.trimHoldLists(11); + s.reclaim_memory(11); EXPECT_TRUE(s.getBufferState(0).size() == 0); EXPECT_TRUE(s.getBufferState(1).size() != 0); EXPECT_TRUE(s.getBufferState(2).size() != 0); @@ -296,7 +292,7 @@ TEST(DataStoreTest, require_that_we_can_hold_and_trim_buffers) s.switch_primary_buffer(); EXPECT_EQ(0u, s.primary_buffer_id()); EXPECT_EQ(0u, MyRef(s.addEntry(5)).bufferId()); - s.trimHoldLists(41); + s.reclaim_memory(41); EXPECT_TRUE(s.getBufferState(0).size() != 0); EXPECT_TRUE(s.getBufferState(1).size() == 0); EXPECT_TRUE(s.getBufferState(2).size() == 0); @@ -308,21 +304,21 @@ TEST(DataStoreTest, require_that_we_can_hold_and_trim_elements) MyStore s; MyRef r1 = s.addEntry(1); s.holdElem(r1, 1); - s.transferHoldLists(10); + s.assign_generation(10); MyRef r2 = s.addEntry(2); s.holdElem(r2, 1); - s.transferHoldLists(20); + s.assign_generation(20); MyRef r3 = s.addEntry(3); s.holdElem(r3, 1); - s.transferHoldLists(30); + s.assign_generation(30); EXPECT_EQ(1, s.getEntry(r1)); EXPECT_EQ(2, s.getEntry(r2)); EXPECT_EQ(3, s.getEntry(r3)); - s.trimElemHoldList(11); + s.reclaim_entry_refs(11); EXPECT_EQ(0, s.getEntry(r1)); EXPECT_EQ(2, s.getEntry(r2)); EXPECT_EQ(3, s.getEntry(r3)); - s.trimElemHoldList(31); + s.reclaim_entry_refs(31); EXPECT_EQ(0, s.getEntry(r1)); EXPECT_EQ(0, s.getEntry(r2)); EXPECT_EQ(0, s.getEntry(r3)); @@ -362,17 +358,17 @@ TEST(DataStoreTest, require_that_we_can_use_free_lists) s.enableFreeLists(); auto r1 = s.addEntry(1); s.holdElem(r1, 1); - s.transferHoldLists(10); + s.assign_generation(10); auto r2 = s.addEntry(2); expect_successive_refs(r1, r2); s.holdElem(r2, 1); - s.transferHoldLists(20); - s.trimElemHoldList(11); + s.assign_generation(20); + s.reclaim_entry_refs(11); auto r3 = s.addEntry(3); // reuse r1 EXPECT_EQ(r1, r3); auto r4 = s.addEntry(4); expect_successive_refs(r2, r4); - s.trimElemHoldList(21); + s.reclaim_entry_refs(21); auto r5 = s.addEntry(5); // reuse r2 EXPECT_EQ(r2, r5); auto r6 = s.addEntry(6); @@ -397,8 +393,8 @@ TEST(DataStoreTest, require_that_we_can_use_free_lists_with_raw_allocator) expect_successive_handles(h1, h2); s.holdElem(h1.ref, 3); s.holdElem(h2.ref, 3); - s.transferHoldLists(10); - s.trimElemHoldList(11); + s.assign_generation(10); + s.reclaim_entry_refs(11); auto h3 = allocator.alloc(3); // reuse h2.ref from free list EXPECT_EQ(h2, h3); @@ -414,7 +410,7 @@ TEST(DataStoreTest, require_that_we_can_use_free_lists_with_raw_allocator) TEST(DataStoreTest, require_that_memory_stats_are_calculated) { MyStore s; - DataStoreBase::MemStats m; + MemoryStats m; m._allocElems = MyRef::offsetSize(); m._usedElems = 1; // ref = 0 is reserved m._deadElems = 1; // ref = 0 is reserved @@ -429,16 +425,11 @@ TEST(DataStoreTest, require_that_memory_stats_are_calculated) m._usedElems++; assertMemStats(m, s.getMemStats()); - // inc dead - s.incDead(r, 1); - m._deadElems++; - assertMemStats(m, s.getMemStats()); - // hold buffer s.addEntry(20); s.addEntry(30); s.holdBuffer(r.bufferId()); - s.transferHoldLists(100); + s.assign_generation(100); m._usedElems += 2; m._holdElems = m._usedElems; m._deadElems = 0; @@ -455,7 +446,7 @@ TEST(DataStoreTest, require_that_memory_stats_are_calculated) m._freeBuffers--; // trim hold buffer - s.trimHoldLists(101); + s.reclaim_memory(101); m._allocElems -= MyRef::offsetSize(); m._usedElems = 1; m._deadElems = 0; @@ -466,7 +457,7 @@ TEST(DataStoreTest, require_that_memory_stats_are_calculated) { // increase extra used bytes auto prev_stats = s.getMemStats(); - s.get_active_buffer_state().incExtraUsedBytes(50); + s.get_active_buffer_state().stats().inc_extra_used_bytes(50); auto curr_stats = s.getMemStats(); EXPECT_EQ(prev_stats._allocBytes + 50, curr_stats._allocBytes); EXPECT_EQ(prev_stats._usedBytes + 50, curr_stats._usedBytes); @@ -474,7 +465,7 @@ TEST(DataStoreTest, require_that_memory_stats_are_calculated) { // increase extra hold bytes auto prev_stats = s.getMemStats(); - s.get_active_buffer_state().incExtraHoldBytes(30); + s.get_active_buffer_state().hold_elems(0, 30); auto curr_stats = s.getMemStats(); EXPECT_EQ(prev_stats._holdBytes + 30, curr_stats._holdBytes); } @@ -487,15 +478,14 @@ TEST(DataStoreTest, require_that_memory_usage_is_calculated) s.addEntry(20); s.addEntry(30); s.addEntry(40); - s.incDead(r, 1); s.holdBuffer(r.bufferId()); - s.transferHoldLists(100); + s.assign_generation(100); vespalib::MemoryUsage m = s.getMemoryUsage(); EXPECT_EQ(MyRef::offsetSize() * sizeof(int), m.allocatedBytes()); EXPECT_EQ(5 * sizeof(int), m.usedBytes()); EXPECT_EQ(0 * sizeof(int), m.deadBytes()); EXPECT_EQ(5 * sizeof(int), m.allocatedBytesOnHold()); - s.trimHoldLists(101); + s.reclaim_memory(101); } TEST(DataStoreTest, require_that_we_can_disable_elemement_hold_list) @@ -523,8 +513,8 @@ TEST(DataStoreTest, require_that_we_can_disable_elemement_hold_list) EXPECT_EQ(4 * sizeof(int), m.usedBytes()); EXPECT_EQ(2 * sizeof(int), m.deadBytes()); EXPECT_EQ(1 * sizeof(int), m.allocatedBytesOnHold()); - s.transferHoldLists(100); - s.trimHoldLists(101); + s.assign_generation(100); + s.reclaim_memory(101); } using IntGrowStore = GrowStore<int, EntryRefT<24>>; @@ -644,9 +634,9 @@ TEST(DataStoreTest, can_set_memory_allocator) s.switch_primary_buffer(); EXPECT_EQ(AllocStats(3, 0), stats); s.holdBuffer(0); - s.transferHoldLists(10); + s.assign_generation(10); EXPECT_EQ(AllocStats(3, 0), stats); - s.trimHoldLists(11); + s.reclaim_memory(11); EXPECT_EQ(AllocStats(3, 2), stats); } EXPECT_EQ(AllocStats(3, 3), stats); @@ -655,7 +645,7 @@ TEST(DataStoreTest, can_set_memory_allocator) namespace { void -assertBuffers(BufferStats exp_buffers, size_t num_arrays_for_new_buffer) +assertBuffers(BufferIds exp_buffers, size_t num_arrays_for_new_buffer) { EXPECT_EQ(exp_buffers, IntGrowStore(1, 1, 1024, num_arrays_for_new_buffer).getBuffers(exp_buffers.size())); } @@ -680,7 +670,7 @@ TEST(DataStoreTest, control_static_sizes) { namespace { -void test_free_element_to_held_buffer(bool direct, bool before_hold_buffer) +void test_free_element_to_held_buffer(bool before_hold_buffer) { MyStore s; auto ref = s.addEntry(1); @@ -689,45 +679,27 @@ void test_free_element_to_held_buffer(bool direct, bool before_hold_buffer) EXPECT_EQ(1u, s.primary_buffer_id()); if (before_hold_buffer) { - if (direct) { - s.freeElem(ref, 1); - } else { - s.holdElem(ref, 1); - } + s.holdElem(ref, 1); } s.holdBuffer(0); // hold last buffer if (!before_hold_buffer) { - if (direct) { - ASSERT_DEATH({ s.freeElem(ref, 1); }, "state.isOnHold\\(\\) && was_held"); - } else { - ASSERT_DEATH({ s.holdElem(ref, 1); }, "state.isActive\\(\\)"); - } + ASSERT_DEATH({ s.holdElem(ref, 1); }, "isActive\\(\\)"); } - s.transferHoldLists(100); - s.trimHoldLists(101); + s.assign_generation(100); + s.reclaim_memory(101); } } -TEST(DataStoreTest, free_to_active_then_held_buffer_is_ok) -{ - test_free_element_to_held_buffer(true, true); -} - TEST(DataStoreTest, hold_to_active_then_held_buffer_is_ok) { - test_free_element_to_held_buffer(false, true); + test_free_element_to_held_buffer(true); } #ifndef NDEBUG -TEST(DataStoreDeathTest, free_to_held_buffer_is_not_ok) -{ - test_free_element_to_held_buffer(true, false); -} - TEST(DataStoreDeathTest, hold_to_held_buffer_is_not_ok) { - test_free_element_to_held_buffer(false, false); + test_free_element_to_held_buffer(false); } #endif diff --git a/vespalib/src/tests/datastore/fixed_size_hash_map/fixed_size_hash_map_test.cpp b/vespalib/src/tests/datastore/fixed_size_hash_map/fixed_size_hash_map_test.cpp index 599cb209e6c..4f4c3ac94eb 100644 --- a/vespalib/src/tests/datastore/fixed_size_hash_map/fixed_size_hash_map_test.cpp +++ b/vespalib/src/tests/datastore/fixed_size_hash_map/fixed_size_hash_map_test.cpp @@ -88,13 +88,13 @@ DataStoreFixedSizeHashTest::~DataStoreFixedSizeHashTest() void DataStoreFixedSizeHashTest::commit() { - _store.transferHoldLists(_generation_handler.getCurrentGeneration()); - _hash_map->transfer_hold_lists(_generation_handler.getCurrentGeneration()); - _generation_holder.transferHoldLists(_generation_handler.getCurrentGeneration()); + _store.assign_generation(_generation_handler.getCurrentGeneration()); + _hash_map->assign_generation(_generation_handler.getCurrentGeneration()); + _generation_holder.assign_generation(_generation_handler.getCurrentGeneration()); _generation_handler.incGeneration(); - _store.trimHoldLists(_generation_handler.getFirstUsedGeneration()); - _hash_map->trim_hold_lists(_generation_handler.getFirstUsedGeneration()); - _generation_holder.trimHoldLists(_generation_handler.getFirstUsedGeneration()); + _store.reclaim_memory(_generation_handler.get_oldest_used_generation()); + _hash_map->reclaim_memory(_generation_handler.get_oldest_used_generation()); + _generation_holder.reclaim(_generation_handler.get_oldest_used_generation()); } size_t diff --git a/vespalib/src/tests/datastore/sharded_hash_map/sharded_hash_map_test.cpp b/vespalib/src/tests/datastore/sharded_hash_map/sharded_hash_map_test.cpp index 13f9ae251b6..4c3fe1756c5 100644 --- a/vespalib/src/tests/datastore/sharded_hash_map/sharded_hash_map_test.cpp +++ b/vespalib/src/tests/datastore/sharded_hash_map/sharded_hash_map_test.cpp @@ -73,8 +73,8 @@ public: } ~MyCompactable() override = default; - EntryRef move(EntryRef ref) override { - auto new_ref = _allocator.move(ref); + EntryRef move_on_compact(EntryRef ref) override { + auto new_ref = _allocator.move_on_compact(ref); _allocator.hold(ref); _new_refs.emplace_back(new_ref); return new_ref; @@ -168,11 +168,11 @@ DataStoreShardedHashTest::~DataStoreShardedHashTest() void DataStoreShardedHashTest::commit() { - _store.transferHoldLists(_generationHandler.getCurrentGeneration()); - _hash_map.transfer_hold_lists(_generationHandler.getCurrentGeneration()); + _store.assign_generation(_generationHandler.getCurrentGeneration()); + _hash_map.assign_generation(_generationHandler.getCurrentGeneration()); _generationHandler.incGeneration(); - _store.trimHoldLists(_generationHandler.getFirstUsedGeneration()); - _hash_map.trim_hold_lists(_generationHandler.getFirstUsedGeneration()); + _store.reclaim_memory(_generationHandler.get_oldest_used_generation()); + _hash_map.reclaim_memory(_generationHandler.get_oldest_used_generation()); } void @@ -395,7 +395,7 @@ TEST_F(DataStoreShardedHashTest, foreach_key_works) } } -TEST_F(DataStoreShardedHashTest, move_keys_works) +TEST_F(DataStoreShardedHashTest, move_keys_on_compact_works) { populate_sample_data(small_population); std::vector<EntryRef> refs; @@ -403,7 +403,7 @@ TEST_F(DataStoreShardedHashTest, move_keys_works) std::vector<EntryRef> new_refs; MyCompactable my_compactable(_allocator, new_refs); auto filter = make_entry_ref_filter<RefT>(false); - _hash_map.move_keys(my_compactable, filter); + _hash_map.move_keys_on_compact(my_compactable, filter); std::vector<EntryRef> verify_new_refs; _hash_map.foreach_key([&verify_new_refs](EntryRef ref) { verify_new_refs.emplace_back(ref); }); EXPECT_EQ(small_population, refs.size()); diff --git a/vespalib/src/tests/datastore/unique_store/unique_store_test.cpp b/vespalib/src/tests/datastore/unique_store/unique_store_test.cpp index 92bd5502406..48a0ecafbc6 100644 --- a/vespalib/src/tests/datastore/unique_store/unique_store_test.cpp +++ b/vespalib/src/tests/datastore/unique_store/unique_store_test.cpp @@ -21,10 +21,10 @@ enum class DictionaryType { BTREE, HASH, BTREE_AND_HASH }; using namespace vespalib::datastore; using vespalib::ArrayRef; using generation_t = vespalib::GenerationHandler::generation_t; -using vespalib::datastore::test::BufferStats; using vespalib::alloc::MemoryAllocator; using vespalib::alloc::test::MemoryAllocatorObserver; using AllocStats = MemoryAllocatorObserver::Stats; +using TestBufferStats = vespalib::datastore::test::BufferStats; template <typename UniqueStoreT> struct TestBaseValues { @@ -94,10 +94,10 @@ struct TestBase : public ::testing::Test { uint32_t getBufferId(EntryRef ref) const { return EntryRefType(ref).bufferId(); } - void assertBufferState(EntryRef ref, const BufferStats expStats) const { + void assertBufferState(EntryRef ref, const TestBufferStats expStats) const { EXPECT_EQ(expStats._used, store.bufferState(ref).size()); - EXPECT_EQ(expStats._hold, store.bufferState(ref).getHoldElems()); - EXPECT_EQ(expStats._dead, store.bufferState(ref).getDeadElems()); + EXPECT_EQ(expStats._hold, store.bufferState(ref).stats().hold_elems()); + EXPECT_EQ(expStats._dead, store.bufferState(ref).stats().dead_elems()); } void assertStoreContent() const { for (const auto &elem : refStore) { @@ -112,15 +112,15 @@ struct TestBase : public ::testing::Test { } return EntryRef(); } - void trimHoldLists() { + void reclaim_memory() { store.freeze(); - store.transferHoldLists(generation++); - store.trimHoldLists(generation); + store.assign_generation(generation++); + store.reclaim_memory(generation); } void compactWorst() { CompactionSpec compaction_spec(true, true); // Use a compaction strategy that will compact all active buffers - CompactionStrategy compaction_strategy(0.0, 0.0, EntryRefType::numBuffers(), 1.0); + auto compaction_strategy = CompactionStrategy::make_compact_all_active_buffers_strategy(); auto remapper = store.compact_worst(compaction_spec, compaction_strategy); std::vector<AtomicEntryRef> refs; for (const auto &elem : refStore) { @@ -320,9 +320,9 @@ TYPED_TEST(TestBase, elements_are_put_on_hold_when_value_is_removed) EntryRef ref = this->add(this->values()[0]); size_t reserved = this->get_reserved(ref); size_t array_size = this->get_array_size(ref); - this->assertBufferState(ref, BufferStats().used(array_size + reserved).hold(0).dead(reserved)); + this->assertBufferState(ref, TestBufferStats().used(array_size + reserved).hold(0).dead(reserved)); this->store.remove(ref); - this->assertBufferState(ref, BufferStats().used(array_size + reserved).hold(array_size).dead(reserved)); + this->assertBufferState(ref, TestBufferStats().used(array_size + reserved).hold(array_size).dead(reserved)); } TYPED_TEST(TestBase, elements_are_reference_counted) @@ -333,11 +333,11 @@ TYPED_TEST(TestBase, elements_are_reference_counted) // Note: The first buffer have the first element reserved -> we expect 2 elements used here. size_t reserved = this->get_reserved(ref); size_t array_size = this->get_array_size(ref); - this->assertBufferState(ref, BufferStats().used(array_size + reserved).hold(0).dead(reserved)); + this->assertBufferState(ref, TestBufferStats().used(array_size + reserved).hold(0).dead(reserved)); this->store.remove(ref); - this->assertBufferState(ref, BufferStats().used(array_size + reserved).hold(0).dead(reserved)); + this->assertBufferState(ref, TestBufferStats().used(array_size + reserved).hold(0).dead(reserved)); this->store.remove(ref); - this->assertBufferState(ref, BufferStats().used(array_size + reserved).hold(array_size).dead(reserved)); + this->assertBufferState(ref, TestBufferStats().used(array_size + reserved).hold(array_size).dead(reserved)); } TEST_F(SmallOffsetNumberTest, new_underlying_buffer_is_allocated_when_current_is_full) @@ -364,10 +364,10 @@ TYPED_TEST(TestBase, store_can_be_compacted) EntryRef val0Ref = this->add(this->values()[0]); EntryRef val1Ref = this->add(this->values()[1]); this->remove(this->add(this->values()[2])); - this->trimHoldLists(); + this->reclaim_memory(); size_t reserved = this->get_reserved(val0Ref); size_t array_size = this->get_array_size(val0Ref); - this->assertBufferState(val0Ref, BufferStats().used(reserved + 3 * array_size).dead(reserved + array_size)); + this->assertBufferState(val0Ref, TestBufferStats().used(reserved + 3 * array_size).dead(reserved + array_size)); uint32_t val1BufferId = this->getBufferId(val0Ref); EXPECT_EQ(2u, this->refStore.size()); @@ -381,7 +381,7 @@ TYPED_TEST(TestBase, store_can_be_compacted) this->assertGet(val0Ref, this->values()[0]); this->assertGet(val1Ref, this->values()[1]); EXPECT_TRUE(this->store.bufferState(val0Ref).isOnHold()); - this->trimHoldLists(); + this->reclaim_memory(); EXPECT_TRUE(this->store.bufferState(val0Ref).isFree()); this->assertStoreContent(); } @@ -396,7 +396,7 @@ TYPED_TEST(TestBase, store_can_be_instantiated_with_builder) EntryRef val1Ref = builder.mapEnumValueToEntryRef(2); size_t reserved = this->get_reserved(val0Ref); size_t array_size = this->get_array_size(val0Ref); - this->assertBufferState(val0Ref, BufferStats().used(2 * array_size + reserved).dead(reserved)); // Note: First element is reserved + this->assertBufferState(val0Ref, TestBufferStats().used(2 * array_size + reserved).dead(reserved)); // Note: First element is reserved EXPECT_TRUE(val0Ref.valid()); EXPECT_TRUE(val1Ref.valid()); EXPECT_NE(val0Ref.ref(), val1Ref.ref()); @@ -415,7 +415,7 @@ TYPED_TEST(TestBase, store_can_be_enumerated) EntryRef val0Ref = this->add(this->values()[0]); EntryRef val1Ref = this->add(this->values()[1]); this->remove(this->add(this->values()[2])); - this->trimHoldLists(); + this->reclaim_memory(); auto enumerator = this->getEnumerator(true); std::vector<uint32_t> refs; @@ -460,7 +460,7 @@ TEST_F(DoubleTest, nan_is_handled) for (auto &value : myvalues) { refs.emplace_back(add(value)); } - trimHoldLists(); + reclaim_memory(); EXPECT_TRUE(std::isnan(store.get(refs[1]))); EXPECT_TRUE(std::signbit(store.get(refs[1]))); EXPECT_TRUE(std::isinf(store.get(refs[2]))); diff --git a/vespalib/src/tests/datastore/unique_store_dictionary/unique_store_dictionary_test.cpp b/vespalib/src/tests/datastore/unique_store_dictionary/unique_store_dictionary_test.cpp index d0fede5c550..496bc814d0d 100644 --- a/vespalib/src/tests/datastore/unique_store_dictionary/unique_store_dictionary_test.cpp +++ b/vespalib/src/tests/datastore/unique_store_dictionary/unique_store_dictionary_test.cpp @@ -62,9 +62,9 @@ struct UniqueStoreDictionaryTest : public ::testing::Test { } void inc_generation() { dict.freeze(); - dict.transfer_hold_lists(gen_handler.getCurrentGeneration()); + dict.assign_generation(gen_handler.getCurrentGeneration()); gen_handler.incGeneration(); - dict.trim_hold_lists(gen_handler.getFirstUsedGeneration()); + dict.reclaim_memory(gen_handler.get_oldest_used_generation()); } void take_snapshot() { dict.freeze(); diff --git a/vespalib/src/tests/datastore/unique_store_string_allocator/unique_store_string_allocator_test.cpp b/vespalib/src/tests/datastore/unique_store_string_allocator/unique_store_string_allocator_test.cpp index 777da0c2b16..e865239787b 100644 --- a/vespalib/src/tests/datastore/unique_store_string_allocator/unique_store_string_allocator_test.cpp +++ b/vespalib/src/tests/datastore/unique_store_string_allocator/unique_store_string_allocator_test.cpp @@ -11,7 +11,7 @@ using namespace vespalib::datastore; using vespalib::MemoryUsage; using generation_t = vespalib::GenerationHandler::generation_t; -using BufferStats = vespalib::datastore::test::BufferStats; +using TestBufferStats = vespalib::datastore::test::BufferStats; using vespalib::alloc::MemoryAllocator; using vespalib::alloc::test::MemoryAllocatorObserver; using AllocStats = MemoryAllocatorObserver::Stats; @@ -51,8 +51,8 @@ struct TestBase : public ::testing::Test { void remove(EntryRef ref) { allocator.hold(ref); } - EntryRef move(EntryRef ref) { - return allocator.move(ref); + EntryRef move_on_compact(EntryRef ref) { + return allocator.move_on_compact(ref); } uint32_t get_buffer_id(EntryRef ref) const { return EntryRefType(ref).bufferId(); @@ -60,16 +60,16 @@ struct TestBase : public ::testing::Test { const BufferState &buffer_state(EntryRef ref) const { return allocator.get_data_store().getBufferState(get_buffer_id(ref)); } - void assert_buffer_state(EntryRef ref, const BufferStats expStats) const { + void assert_buffer_state(EntryRef ref, const TestBufferStats expStats) const { EXPECT_EQ(expStats._used, buffer_state(ref).size()); - EXPECT_EQ(expStats._hold, buffer_state(ref).getHoldElems()); - EXPECT_EQ(expStats._dead, buffer_state(ref).getDeadElems()); - EXPECT_EQ(expStats._extra_used, buffer_state(ref).getExtraUsedBytes()); - EXPECT_EQ(expStats._extra_hold, buffer_state(ref).getExtraHoldBytes()); + EXPECT_EQ(expStats._hold, buffer_state(ref).stats().hold_elems()); + EXPECT_EQ(expStats._dead, buffer_state(ref).stats().dead_elems()); + EXPECT_EQ(expStats._extra_used, buffer_state(ref).stats().extra_used_bytes()); + EXPECT_EQ(expStats._extra_hold, buffer_state(ref).stats().extra_hold_bytes()); } - void trim_hold_lists() { - allocator.get_data_store().transferHoldLists(generation++); - allocator.get_data_store().trimHoldLists(generation); + void reclaim_memory() { + allocator.get_data_store().assign_generation(generation++); + allocator.get_data_store().reclaim_memory(generation); } }; @@ -86,32 +86,32 @@ TEST_F(StringTest, can_add_and_get_values) TEST_F(StringTest, elements_are_put_on_hold_when_value_is_removed) { EntryRef ref = add(small.c_str()); - assert_buffer_state(ref, BufferStats().used(16).hold(0).dead(0)); + assert_buffer_state(ref, TestBufferStats().used(16).hold(0).dead(0)); remove(ref); - assert_buffer_state(ref, BufferStats().used(16).hold(16).dead(0)); - trim_hold_lists(); - assert_buffer_state(ref, BufferStats().used(16).hold(0).dead(16)); + assert_buffer_state(ref, TestBufferStats().used(16).hold(16).dead(0)); + reclaim_memory(); + assert_buffer_state(ref, TestBufferStats().used(16).hold(0).dead(16)); } TEST_F(StringTest, extra_bytes_used_is_tracked) { EntryRef ref = add(spaces1000.c_str()); // Note: The first buffer have the first element reserved -> we expect 2 elements used here. - assert_buffer_state(ref, BufferStats().used(2).hold(0).dead(1).extra_used(1001)); + assert_buffer_state(ref, TestBufferStats().used(2).hold(0).dead(1).extra_used(1001)); remove(ref); - assert_buffer_state(ref, BufferStats().used(2).hold(1).dead(1).extra_used(1001).extra_hold(1001)); - trim_hold_lists(); - assert_buffer_state(ref, BufferStats().used(2).hold(0).dead(2)); + assert_buffer_state(ref, TestBufferStats().used(2).hold(1).dead(1).extra_used(1001).extra_hold(1001)); + reclaim_memory(); + assert_buffer_state(ref, TestBufferStats().used(2).hold(0).dead(2)); ref = add(spaces1000.c_str()); - assert_buffer_state(ref, BufferStats().used(2).hold(0).dead(1).extra_used(1001)); - EntryRef ref2 = move(ref); + assert_buffer_state(ref, TestBufferStats().used(2).hold(0).dead(1).extra_used(1001)); + EntryRef ref2 = move_on_compact(ref); assert_get(ref2, spaces1000.c_str()); - assert_buffer_state(ref, BufferStats().used(3).hold(0).dead(1).extra_used(2002)); + assert_buffer_state(ref, TestBufferStats().used(3).hold(0).dead(1).extra_used(2002)); remove(ref); remove(ref2); - assert_buffer_state(ref, BufferStats().used(3).hold(2).dead(1).extra_used(2002).extra_hold(2002)); - trim_hold_lists(); - assert_buffer_state(ref, BufferStats().used(3).hold(0).dead(3)); + assert_buffer_state(ref, TestBufferStats().used(3).hold(2).dead(1).extra_used(2002).extra_hold(2002)); + reclaim_memory(); + assert_buffer_state(ref, TestBufferStats().used(3).hold(0).dead(3)); } TEST_F(StringTest, string_length_determines_buffer) @@ -134,13 +134,13 @@ TEST_F(StringTest, free_list_is_used_when_enabled) EntryRef ref2 = add(spaces1000.c_str()); remove(ref1); remove(ref2); - trim_hold_lists(); + reclaim_memory(); EntryRef ref3 = add(small.c_str()); EntryRef ref4 = add(spaces1000.c_str()); EXPECT_EQ(ref1, ref3); EXPECT_EQ(ref2, ref4); - assert_buffer_state(ref1, BufferStats().used(16).hold(0).dead(0)); - assert_buffer_state(ref2, BufferStats().used(2).hold(0).dead(1).extra_used(1001)); + assert_buffer_state(ref1, TestBufferStats().used(16).hold(0).dead(0)); + assert_buffer_state(ref2, TestBufferStats().used(2).hold(0).dead(1).extra_used(1001)); } TEST_F(StringTest, free_list_is_not_used_when_disabled) @@ -150,16 +150,16 @@ TEST_F(StringTest, free_list_is_not_used_when_disabled) EntryRef ref2 = add(spaces1000.c_str()); remove(ref1); remove(ref2); - trim_hold_lists(); + reclaim_memory(); EntryRef ref3 = add(small.c_str()); EntryRef ref4 = add(spaces1000.c_str()); EXPECT_NE(ref1, ref3); EXPECT_NE(ref2, ref4); - assert_buffer_state(ref1, BufferStats().used(32).hold(0).dead(16)); - assert_buffer_state(ref2, BufferStats().used(3).hold(0).dead(2).extra_used(1001)); + assert_buffer_state(ref1, TestBufferStats().used(32).hold(0).dead(16)); + assert_buffer_state(ref2, TestBufferStats().used(3).hold(0).dead(2).extra_used(1001)); } -TEST_F(StringTest, free_list_is_never_used_for_move) +TEST_F(StringTest, free_list_is_never_used_for_move_on_compact) { // Free lists are default enabled for UniqueStoreStringAllocator EntryRef ref1 = add(small.c_str()); @@ -168,13 +168,13 @@ TEST_F(StringTest, free_list_is_never_used_for_move) EntryRef ref4 = add(spaces1000.c_str()); remove(ref3); remove(ref4); - trim_hold_lists(); - EntryRef ref5 = move(ref1); - EntryRef ref6 = move(ref2); + reclaim_memory(); + EntryRef ref5 = move_on_compact(ref1); + EntryRef ref6 = move_on_compact(ref2); EXPECT_NE(ref5, ref3); EXPECT_NE(ref6, ref4); - assert_buffer_state(ref1, BufferStats().used(48).hold(0).dead(16)); - assert_buffer_state(ref2, BufferStats().used(4).hold(0).dead(2).extra_used(2002)); + assert_buffer_state(ref1, TestBufferStats().used(48).hold(0).dead(16)); + assert_buffer_state(ref2, TestBufferStats().used(4).hold(0).dead(2).extra_used(2002)); } TEST_F(StringTest, provided_memory_allocator_is_used) diff --git a/vespalib/src/tests/util/generation_hold_list/CMakeLists.txt b/vespalib/src/tests/util/generation_hold_list/CMakeLists.txt new file mode 100644 index 00000000000..c85b2537745 --- /dev/null +++ b/vespalib/src/tests/util/generation_hold_list/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(vespalib_generation_hold_list_test_app TEST + SOURCES + generation_hold_list_test.cpp + DEPENDS + vespalib + GTest::GTest +) +vespa_add_test(NAME vespalib_generation_hold_list_test_app COMMAND vespalib_generation_hold_list_test_app) diff --git a/vespalib/src/tests/util/generation_hold_list/generation_hold_list_test.cpp b/vespalib/src/tests/util/generation_hold_list/generation_hold_list_test.cpp new file mode 100644 index 00000000000..7e56e17990c --- /dev/null +++ b/vespalib/src/tests/util/generation_hold_list/generation_hold_list_test.cpp @@ -0,0 +1,95 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/vespalib/gtest/gtest.h> +#include <vespa/vespalib/util/generation_hold_list.hpp> +#include <vespa/vespalib/util/generationholder.h> +#include <cstdint> + +using namespace vespalib; + +using MyElem = GenerationHeldBase; +using generation_t = GenerationHandler::generation_t; + +TEST(GenerationHolderTest, holding_of_unique_ptr_elements_with_tracking_of_held_bytes) +{ + GenerationHolder h; + h.insert(std::make_unique<MyElem>(3)); + h.assign_generation(0); + h.insert(std::make_unique<MyElem>(5)); + h.assign_generation(1); + h.insert(std::make_unique<MyElem>(7)); + h.assign_generation(2); + h.insert(std::make_unique<MyElem>(11)); + h.assign_generation(4); + EXPECT_EQ(3 + 5 + 7 + 11, h.get_held_bytes()); + + h.reclaim(0); + EXPECT_EQ(3 + 5 + 7 + 11, h.get_held_bytes()); + h.reclaim(1); + EXPECT_EQ(5 + 7 + 11, h.get_held_bytes()); + h.reclaim(2); + EXPECT_EQ(7 + 11, h.get_held_bytes()); + + h.insert(std::make_unique<MyElem>(13)); + h.assign_generation(6); + EXPECT_EQ(7 + 11 + 13, h.get_held_bytes()); + + h.reclaim(6); + EXPECT_EQ(13, h.get_held_bytes()); + h.reclaim(7); + EXPECT_EQ(0, h.get_held_bytes()); + h.reclaim(7); + EXPECT_EQ(0, h.get_held_bytes()); +} + +TEST(GenerationHolderTest, reclaim_all_clears_everything) +{ + GenerationHolder h; + h.insert(std::make_unique<MyElem>(3)); + h.insert(std::make_unique<MyElem>(5)); + h.assign_generation(1); + h.reclaim_all(); + EXPECT_EQ(0, h.get_held_bytes()); +} + +using IntVector = std::vector<int32_t>; +using IntHoldList = GenerationHoldList<int32_t, false, true>; + +struct IntHoldListTest : public testing::Test { + IntHoldList h; + IntHoldListTest() : h() {} + void assert_reclaim(const IntVector& exp, generation_t oldest_used_gen) { + IntVector act; + h.reclaim(oldest_used_gen, [&](int elem){ act.push_back(elem); }); + EXPECT_EQ(exp, act); + } + void assert_reclaim_all(const IntVector& exp) { + IntVector act; + h.reclaim_all([&](int elem){ act.push_back(elem); }); + EXPECT_EQ(exp, act); + } +}; + +TEST_F(IntHoldListTest, reclaim_calls_callback_for_reclaimed_elements) +{ + h.insert(3); + h.assign_generation(1); + h.insert(5); + h.insert(7); + h.assign_generation(2); + + assert_reclaim({}, 1); + assert_reclaim({3}, 2); + assert_reclaim({5, 7}, 3); +} + +TEST_F(IntHoldListTest, reclaim_all_calls_callback_for_all_elements) +{ + h.insert(3); + h.insert(5); + h.assign_generation(2); + assert_reclaim_all({3, 5}); + assert_reclaim_all({}); +} + +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/vespalib/src/tests/util/generation_holder/CMakeLists.txt b/vespalib/src/tests/util/generation_holder/CMakeLists.txt deleted file mode 100644 index 8acf9fadaff..00000000000 --- a/vespalib/src/tests/util/generation_holder/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -vespa_add_executable(vespalib_generation_holder_test_app TEST - SOURCES - generation_holder_test.cpp - DEPENDS - vespalib - GTest::GTest -) -vespa_add_test(NAME vespalib_generation_holder_test_app COMMAND vespalib_generation_holder_test_app) diff --git a/vespalib/src/tests/util/generation_holder/generation_holder_test.cpp b/vespalib/src/tests/util/generation_holder/generation_holder_test.cpp deleted file mode 100644 index 97c3330ac9e..00000000000 --- a/vespalib/src/tests/util/generation_holder/generation_holder_test.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -#include <vespa/vespalib/gtest/gtest.h> -#include <vespa/vespalib/util/generationholder.h> - -using vespalib::GenerationHolder; -using MyHeld = vespalib::GenerationHeldBase; - -TEST(GenerationHolderTest, basic_tracking) -{ - GenerationHolder gh; - gh.hold(std::make_unique<MyHeld>(sizeof(int32_t))); - gh.transferHoldLists(0); - gh.hold(std::make_unique<MyHeld>(sizeof(int32_t))); - gh.transferHoldLists(1); - gh.hold(std::make_unique<MyHeld>(sizeof(int32_t))); - gh.transferHoldLists(2); - gh.hold(std::make_unique<MyHeld>(sizeof(int32_t))); - gh.transferHoldLists(4); - EXPECT_EQ(4u * sizeof(int32_t), gh.getHeldBytes()); - gh.trimHoldLists(0); - EXPECT_EQ(4u * sizeof(int32_t), gh.getHeldBytes()); - gh.trimHoldLists(1); - EXPECT_EQ(3u * sizeof(int32_t), gh.getHeldBytes()); - gh.trimHoldLists(2); - EXPECT_EQ(2u * sizeof(int32_t), gh.getHeldBytes()); - gh.hold(std::make_unique<MyHeld>(sizeof(int32_t))); - gh.transferHoldLists(6); - EXPECT_EQ(3u * sizeof(int32_t), gh.getHeldBytes()); - gh.trimHoldLists(6); - EXPECT_EQ(1u * sizeof(int32_t), gh.getHeldBytes()); - gh.trimHoldLists(7); - EXPECT_EQ(0u * sizeof(int32_t), gh.getHeldBytes()); - gh.trimHoldLists(7); - EXPECT_EQ(0u * sizeof(int32_t), gh.getHeldBytes()); -} - -GTEST_MAIN_RUN_ALL_TESTS() diff --git a/vespalib/src/tests/util/generationhandler/generationhandler_test.cpp b/vespalib/src/tests/util/generationhandler/generationhandler_test.cpp index 00da752a749..0bc72f93a9d 100644 --- a/vespalib/src/tests/util/generationhandler/generationhandler_test.cpp +++ b/vespalib/src/tests/util/generationhandler/generationhandler_test.cpp @@ -26,10 +26,10 @@ GenerationHandlerTest::~GenerationHandlerTest() = default; TEST_F(GenerationHandlerTest, require_that_generation_can_be_increased) { EXPECT_EQ(0u, gh.getCurrentGeneration()); - EXPECT_EQ(0u, gh.getFirstUsedGeneration()); + EXPECT_EQ(0u, gh.get_oldest_used_generation()); gh.incGeneration(); EXPECT_EQ(1u, gh.getCurrentGeneration()); - EXPECT_EQ(1u, gh.getFirstUsedGeneration()); + EXPECT_EQ(1u, gh.get_oldest_used_generation()); } TEST_F(GenerationHandlerTest, require_that_readers_can_take_guards) @@ -87,34 +87,34 @@ TEST_F(GenerationHandlerTest, require_that_guards_can_be_copied) TEST_F(GenerationHandlerTest, require_that_the_first_used_generation_is_correct) { - EXPECT_EQ(0u, gh.getFirstUsedGeneration()); + EXPECT_EQ(0u, gh.get_oldest_used_generation()); gh.incGeneration(); - EXPECT_EQ(1u, gh.getFirstUsedGeneration()); + EXPECT_EQ(1u, gh.get_oldest_used_generation()); { GenGuard g1 = gh.takeGuard(); gh.incGeneration(); EXPECT_EQ(1u, gh.getGenerationRefCount()); - EXPECT_EQ(1u, gh.getFirstUsedGeneration()); + EXPECT_EQ(1u, gh.get_oldest_used_generation()); } - EXPECT_EQ(1u, gh.getFirstUsedGeneration()); - gh.updateFirstUsedGeneration(); // Only writer should call this + EXPECT_EQ(1u, gh.get_oldest_used_generation()); + gh.update_oldest_used_generation(); // Only writer should call this EXPECT_EQ(0u, gh.getGenerationRefCount()); - EXPECT_EQ(2u, gh.getFirstUsedGeneration()); + EXPECT_EQ(2u, gh.get_oldest_used_generation()); { GenGuard g1 = gh.takeGuard(); gh.incGeneration(); gh.incGeneration(); EXPECT_EQ(1u, gh.getGenerationRefCount()); - EXPECT_EQ(2u, gh.getFirstUsedGeneration()); + EXPECT_EQ(2u, gh.get_oldest_used_generation()); { GenGuard g2 = gh.takeGuard(); - EXPECT_EQ(2u, gh.getFirstUsedGeneration()); + EXPECT_EQ(2u, gh.get_oldest_used_generation()); } } - EXPECT_EQ(2u, gh.getFirstUsedGeneration()); - gh.updateFirstUsedGeneration(); // Only writer should call this + EXPECT_EQ(2u, gh.get_oldest_used_generation()); + gh.update_oldest_used_generation(); // Only writer should call this EXPECT_EQ(0u, gh.getGenerationRefCount()); - EXPECT_EQ(4u, gh.getFirstUsedGeneration()); + EXPECT_EQ(4u, gh.get_oldest_used_generation()); } TEST_F(GenerationHandlerTest, require_that_generation_can_grow_large) @@ -124,7 +124,7 @@ TEST_F(GenerationHandlerTest, require_that_generation_can_grow_large) EXPECT_EQ(i, gh.getCurrentGeneration()); guards.push_back(gh.takeGuard()); // take guard on current generation if (i >= 128) { - EXPECT_EQ(i - 128, gh.getFirstUsedGeneration()); + EXPECT_EQ(i - 128, gh.get_oldest_used_generation()); guards.pop_front(); EXPECT_EQ(128u, gh.getGenerationRefCount()); } diff --git a/vespalib/src/tests/util/generationhandler_stress/generation_handler_stress_test.cpp b/vespalib/src/tests/util/generationhandler_stress/generation_handler_stress_test.cpp index 74af25b54a8..fd2769fd8b1 100644 --- a/vespalib/src/tests/util/generationhandler_stress/generation_handler_stress_test.cpp +++ b/vespalib/src/tests/util/generationhandler_stress/generation_handler_stress_test.cpp @@ -238,7 +238,7 @@ Fixture::write_indirect_work(uint64_t cnt, IndirectContext& context) ReadStopper read_stopper(_stopRead); uint32_t sleep_cnt = 0; ASSERT_EQ(0, _generationHandler.getCurrentGeneration()); - auto oldest_gen = _generationHandler.getFirstUsedGeneration(); + auto oldest_gen = _generationHandler.get_oldest_used_generation(); for (uint64_t i = 0; i < cnt; ++i) { auto gen = _generationHandler.getCurrentGeneration(); // Hold data for gen, write new data for next_gen @@ -248,7 +248,7 @@ Fixture::write_indirect_work(uint64_t cnt, IndirectContext& context) *v_ptr = next_gen; context._value_ptr.store(v_ptr, std::memory_order_release); _generationHandler.incGeneration(); - auto first_used_gen = _generationHandler.getFirstUsedGeneration(); + auto first_used_gen = _generationHandler.get_oldest_used_generation(); while (oldest_gen < first_used_gen) { // Clear data that readers should no longer have access to. *context.calc_value_ptr(oldest_gen) = 0; @@ -258,8 +258,8 @@ Fixture::write_indirect_work(uint64_t cnt, IndirectContext& context) // Sleep if writer gets too much ahead of readers. std::this_thread::sleep_for(1ms); ++sleep_cnt; - _generationHandler.updateFirstUsedGeneration(); - first_used_gen = _generationHandler.getFirstUsedGeneration(); + _generationHandler.update_oldest_used_generation(); + first_used_gen = _generationHandler.get_oldest_used_generation(); } } _doneWriteWork += cnt; diff --git a/vespalib/src/tests/util/rcuvector/rcuvector_test.cpp b/vespalib/src/tests/util/rcuvector/rcuvector_test.cpp index eb2b00f9e20..5d6ec3050da 100644 --- a/vespalib/src/tests/util/rcuvector/rcuvector_test.cpp +++ b/vespalib/src/tests/util/rcuvector/rcuvector_test.cpp @@ -102,15 +102,15 @@ TEST(RcuVectorTest, resize) RcuVectorBase<int8_t> v(growStrategy(16, 1.0, 0), g); v.push_back(1); v.push_back(2); - g.transferHoldLists(0); - g.trimHoldLists(1); + g.assign_generation(0); + g.reclaim(1); const int8_t *old = &v[0]; EXPECT_EQ(16u, v.capacity()); EXPECT_EQ(2u, v.size()); v.ensure_size(32, 3); v[0] = 3; v[1] = 3; - g.transferHoldLists(1); + g.assign_generation(1); EXPECT_EQ(1, old[0]); EXPECT_EQ(2, old[1]); EXPECT_EQ(3, v[0]); @@ -119,7 +119,7 @@ TEST(RcuVectorTest, resize) EXPECT_EQ(3, v[31]); EXPECT_EQ(64u, v.capacity()); EXPECT_EQ(32u, v.size()); - g.trimHoldLists(2); + g.reclaim(2); } } @@ -140,7 +140,7 @@ TEST(RcuVectorTest, generation_handling) v.setGeneration(2); v.push_back(50); - v.removeOldGenerations(3); + v.reclaim_memory(3); EXPECT_EQ(0u, v.getMemoryUsage().allocatedBytesOnHold()); v.push_back(60); // new array EXPECT_EQ(24u, v.getMemoryUsage().allocatedBytesOnHold()); @@ -184,7 +184,7 @@ TEST(RcuVectorTest, memory_usage) EXPECT_TRUE(assertUsage(MemoryUsage(6,6,0,2), v.getMemoryUsage())); v.push_back(4); EXPECT_TRUE(assertUsage(MemoryUsage(12,11,0,6), v.getMemoryUsage())); - v.removeOldGenerations(1); + v.reclaim_memory(1); EXPECT_TRUE(assertUsage(MemoryUsage(6,5,0,0), v.getMemoryUsage())); } @@ -197,11 +197,11 @@ void verify_shrink_with_buffer_copying(size_t initial_size, size_t absolute_mini v.push_back(2); v.push_back(3); v.push_back(4); - g.transferHoldLists(0); - g.trimHoldLists(1); + g.assign_generation(0); + g.reclaim(1); MemoryUsage mu; mu = v.getMemoryUsage(); - mu.incAllocatedBytesOnHold(g.getHeldBytes()); + mu.incAllocatedBytesOnHold(g.get_held_bytes()); EXPECT_TRUE(assertUsage(MemoryUsage(initial_capacity, 4, 0, 0), mu)); EXPECT_EQ(4u, v.size()); EXPECT_EQ(initial_capacity, v.capacity()); @@ -211,18 +211,18 @@ void verify_shrink_with_buffer_copying(size_t initial_size, size_t absolute_mini EXPECT_EQ(4, v[3]); const int8_t *old = &v[0]; v.shrink(2); - g.transferHoldLists(1); + g.assign_generation(1); EXPECT_EQ(2u, v.size()); EXPECT_EQ(minimal_capacity, v.capacity()); EXPECT_EQ(1, v[0]); EXPECT_EQ(2, v[1]); EXPECT_EQ(1, old[0]); EXPECT_EQ(2, old[1]); - g.trimHoldLists(2); + g.reclaim(2); EXPECT_EQ(1, v[0]); EXPECT_EQ(2, v[1]); mu = v.getMemoryUsage(); - mu.incAllocatedBytesOnHold(g.getHeldBytes()); + mu.incAllocatedBytesOnHold(g.get_held_bytes()); EXPECT_TRUE(assertUsage(MemoryUsage(minimal_capacity, 2, 0, 0), mu)); } @@ -256,7 +256,7 @@ struct ShrinkFixture { EXPECT_EQ(oldPtr, &vec[0]); } void assertEmptyHoldList() { - EXPECT_EQ(0u, g.getHeldBytes()); + EXPECT_EQ(0u, g.get_held_bytes()); } static size_t page_ints() { return round_up_to_page_size(1) / sizeof(int); } }; @@ -294,8 +294,8 @@ TEST(RcuVectorTest, small_expand) v.push_back(2); EXPECT_EQ(2u, v.capacity()); EXPECT_EQ(2u, v.size()); - g.transferHoldLists(1); - g.trimHoldLists(2); + g.assign_generation(1); + g.reclaim(2); } struct FixtureBase { @@ -325,10 +325,10 @@ struct Fixture : public FixtureBase { Fixture(); ~Fixture(); - void transfer_and_trim(generation_t transfer_gen, generation_t trim_gen) + void assign_and_reclaim(generation_t assign_gen, generation_t reclaim_gen) { - g.transferHoldLists(transfer_gen); - g.trimHoldLists(trim_gen); + g.assign_generation(assign_gen); + g.reclaim(reclaim_gen); } }; @@ -345,7 +345,7 @@ TEST(RcuVectorTest, memory_allocator_can_be_set) { Fixture f; EXPECT_EQ(AllocStats(2, 0), f.stats); - f.transfer_and_trim(1, 2); + f.assign_and_reclaim(1, 2); EXPECT_EQ(AllocStats(2, 1), f.stats); } @@ -355,7 +355,7 @@ TEST(RcuVectorTest, memory_allocator_is_preserved_across_reset) f.arr.reset(); f.arr.reserve(100); EXPECT_EQ(AllocStats(4, 1), f.stats); - f.transfer_and_trim(1, 2); + f.assign_and_reclaim(1, 2); EXPECT_EQ(AllocStats(4, 3), f.stats); } @@ -366,7 +366,7 @@ TEST(RcuVectorTest, created_replacement_vector_uses_same_memory_allocator) EXPECT_EQ(AllocStats(2, 0), f.stats); arr2.reserve(100); EXPECT_EQ(AllocStats(3, 0), f.stats); - f.transfer_and_trim(1, 2); + f.assign_and_reclaim(1, 2); EXPECT_EQ(AllocStats(3, 1), f.stats); } @@ -377,7 +377,7 @@ TEST(RcuVectorTest, ensure_size_and_shrink_use_same_memory_allocator) EXPECT_EQ(AllocStats(3, 0), f.stats); f.arr.shrink(1000); EXPECT_EQ(AllocStats(4, 0), f.stats); - f.transfer_and_trim(1, 2); + f.assign_and_reclaim(1, 2); EXPECT_EQ(AllocStats(4, 3), f.stats); } @@ -432,10 +432,10 @@ void StressFixture::commit() { auto current_gen = generation_handler.getCurrentGeneration(); - g.transferHoldLists(current_gen); + g.assign_generation(current_gen); generation_handler.incGeneration(); - auto first_used_gen = generation_handler.getFirstUsedGeneration(); - g.trimHoldLists(first_used_gen); + auto first_used_gen = generation_handler.get_oldest_used_generation(); + g.reclaim(first_used_gen); } void diff --git a/vespalib/src/vespa/vespalib/btree/btree.hpp b/vespalib/src/vespa/vespalib/btree/btree.hpp index c6d8886254d..81687b6e62d 100644 --- a/vespalib/src/vespa/vespalib/btree/btree.hpp +++ b/vespalib/src/vespa/vespalib/btree/btree.hpp @@ -20,7 +20,7 @@ BTree<KeyT, DataT, AggrT, CompareT, TraitsT, AggrCalcT>::~BTree() { clear(); _alloc.freeze(); - _alloc.clearHoldLists(); + _alloc.reclaim_all_memory(); } template <typename KeyT, typename DataT, typename AggrT, typename CompareT, diff --git a/vespalib/src/vespa/vespalib/btree/btreenodeallocator.h b/vespalib/src/vespa/vespalib/btree/btreenodeallocator.h index 86c9621f869..77900edf848 100644 --- a/vespalib/src/vespa/vespalib/btree/btreenodeallocator.h +++ b/vespalib/src/vespa/vespalib/btree/btreenodeallocator.h @@ -101,7 +101,7 @@ public: /** * Try to free held nodes if nobody can be referencing them. */ - void trimHoldLists(generation_t usedGen); + void reclaim_memory(generation_t oldest_used_gen); /** * Transfer nodes from hold1 lists to hold2 lists, they are no @@ -109,9 +109,9 @@ public: * older versions of the frozen structure must leave before elements * can be unheld. */ - void transferHoldLists(generation_t generation); + void assign_generation(generation_t current_gen); - void clearHoldLists(); + void reclaim_all_memory(); static bool isValidRef(BTreeNode::Ref ref) { return NodeStore::isValidRef(ref); } @@ -164,14 +164,9 @@ public: vespalib::string toString(const BTreeNode * node) const; bool getCompacting(EntryRef ref) const { return _nodeStore.getCompacting(ref); } - std::vector<uint32_t> startCompact() { return _nodeStore.startCompact(); } std::unique_ptr<vespalib::datastore::CompactingBuffers> start_compact_worst(const CompactionStrategy& compaction_strategy) { return _nodeStore.start_compact_worst(compaction_strategy); } - void finishCompact(const std::vector<uint32_t> &toHold) { - return _nodeStore.finishCompact(toHold); - } - template <typename FunctionType> void foreach_key(EntryRef ref, FunctionType func) const { _nodeStore.foreach_key(ref, func); diff --git a/vespalib/src/vespa/vespalib/btree/btreenodeallocator.hpp b/vespalib/src/vespa/vespalib/btree/btreenodeallocator.hpp index 81262f560c7..a38b68afe73 100644 --- a/vespalib/src/vespa/vespalib/btree/btreenodeallocator.hpp +++ b/vespalib/src/vespa/vespalib/btree/btreenodeallocator.hpp @@ -34,7 +34,7 @@ BTreeNodeAllocator<KeyT, DataT, AggrT, INTERNAL_SLOTS, LEAF_SLOTS>:: assert(_treeToFreeze.empty()); assert(_internalHoldUntilFreeze.empty()); assert(_leafHoldUntilFreeze.empty()); - DataStoreBase::MemStats stats = _nodeStore.getMemStats(); + auto stats = _nodeStore.getMemStats(); assert(stats._usedBytes == stats._deadBytes); assert(stats._holdBytes == 0); (void) stats; @@ -235,7 +235,7 @@ freeze() InternalNodeType *inode = mapInternalRef(i); (void) inode; assert(inode->getFrozen()); - _nodeStore.freeElem(i); + _nodeStore.holdElem(i); } _internalHoldUntilFreeze.clear(); } @@ -245,7 +245,7 @@ freeze() LeafNodeType *lnode = mapLeafRef(i); (void) lnode; assert(lnode->getFrozen()); - _nodeStore.freeElem(i); + _nodeStore.holdElem(i); } _leafHoldUntilFreeze.clear(); } @@ -266,18 +266,18 @@ template <typename KeyT, typename DataT, typename AggrT, size_t INTERNAL_SLOTS, size_t LEAF_SLOTS> void BTreeNodeAllocator<KeyT, DataT, AggrT, INTERNAL_SLOTS, LEAF_SLOTS>:: -trimHoldLists(generation_t usedGen) +reclaim_memory(generation_t oldest_used_gen) { - _nodeStore.trimHoldLists(usedGen); + _nodeStore.reclaim_memory(oldest_used_gen); } template <typename KeyT, typename DataT, typename AggrT, size_t INTERNAL_SLOTS, size_t LEAF_SLOTS> void BTreeNodeAllocator<KeyT, DataT, AggrT, INTERNAL_SLOTS, LEAF_SLOTS>:: -transferHoldLists(generation_t generation) +assign_generation(generation_t current_gen) { - _nodeStore.transferHoldLists(generation); + _nodeStore.assign_generation(current_gen); } @@ -285,9 +285,9 @@ template <typename KeyT, typename DataT, typename AggrT, size_t INTERNAL_SLOTS, size_t LEAF_SLOTS> void BTreeNodeAllocator<KeyT, DataT, AggrT, INTERNAL_SLOTS, LEAF_SLOTS>:: -clearHoldLists() +reclaim_all_memory() { - _nodeStore.clearHoldLists(); + _nodeStore.reclaim_all_memory(); } diff --git a/vespalib/src/vespa/vespalib/btree/btreenodestore.h b/vespalib/src/vespa/vespalib/btree/btreenodestore.h index d05ec840f83..a63a4d20170 100644 --- a/vespalib/src/vespa/vespalib/btree/btreenodestore.h +++ b/vespalib/src/vespa/vespalib/btree/btreenodestore.h @@ -156,32 +156,24 @@ public: _store.holdElem(ref, 1); } - void freeElem(EntryRef ref) { - _store.freeElem(ref, 1); - } - - std::vector<uint32_t> startCompact(); - std::unique_ptr<vespalib::datastore::CompactingBuffers> start_compact_worst(const CompactionStrategy& compaction_strategy); - void finishCompact(const std::vector<uint32_t> &toHold); - - void transferHoldLists(generation_t generation) { - _store.transferHoldLists(generation); + void assign_generation(generation_t current_gen) { + _store.assign_generation(current_gen); } // Inherit doc from DataStoreBase - datastore::DataStoreBase::MemStats getMemStats() const { + datastore::MemoryStats getMemStats() const { return _store.getMemStats(); } // Inherit doc from DataStoreBase - void trimHoldLists(generation_t usedGen) { - _store.trimHoldLists(usedGen); + void reclaim_memory(generation_t oldest_used_gen) { + _store.reclaim_memory(oldest_used_gen); } - void clearHoldLists() { - _store.clearHoldLists(); + void reclaim_all_memory() { + _store.reclaim_all_memory(); } // Inherit doc from DataStoreBase diff --git a/vespalib/src/vespa/vespalib/btree/btreenodestore.hpp b/vespalib/src/vespa/vespalib/btree/btreenodestore.hpp index 0f9eeb9daec..a1ffb4d445d 100644 --- a/vespalib/src/vespa/vespalib/btree/btreenodestore.hpp +++ b/vespalib/src/vespa/vespalib/btree/btreenodestore.hpp @@ -55,20 +55,6 @@ BTreeNodeStore<KeyT, DataT, AggrT, INTERNAL_SLOTS, LEAF_SLOTS>:: _store.dropBuffers(); // Drop buffers before type handlers are dropped } - -template <typename KeyT, typename DataT, typename AggrT, - size_t INTERNAL_SLOTS, size_t LEAF_SLOTS> -std::vector<uint32_t> -BTreeNodeStore<KeyT, DataT, AggrT, INTERNAL_SLOTS, LEAF_SLOTS>:: -startCompact() -{ - std::vector<uint32_t> iToHold = _store.startCompact(NODETYPE_INTERNAL); - std::vector<uint32_t> lToHold = _store.startCompact(NODETYPE_LEAF); - std::vector<uint32_t> ret = iToHold; - ret.insert(ret.end(), lToHold.begin(), lToHold.end()); - return ret; -} - template <typename KeyT, typename DataT, typename AggrT, size_t INTERNAL_SLOTS, size_t LEAF_SLOTS> std::unique_ptr<vespalib::datastore::CompactingBuffers> @@ -78,15 +64,6 @@ start_compact_worst(const CompactionStrategy &compaction_strategy) return _store.start_compact_worst_buffers(datastore::CompactionSpec(true, false), compaction_strategy); } -template <typename KeyT, typename DataT, typename AggrT, - size_t INTERNAL_SLOTS, size_t LEAF_SLOTS> -void -BTreeNodeStore<KeyT, DataT, AggrT, INTERNAL_SLOTS, LEAF_SLOTS>:: -finishCompact(const std::vector<uint32_t> &toHold) -{ - _store.finishCompact(toHold); -} - } #define VESPALIB_DATASTORE_INSTANTIATE_BUFFERTYPE_INTERNALNODE(K, A, S) \ diff --git a/vespalib/src/vespa/vespalib/btree/btreestore.h b/vespalib/src/vespa/vespalib/btree/btreestore.h index 54bc397175d..e5c55d5775d 100644 --- a/vespalib/src/vespa/vespalib/btree/btreestore.h +++ b/vespalib/src/vespa/vespalib/btree/btreestore.h @@ -332,25 +332,25 @@ public: // Inherit doc from DataStoreBase void - trimHoldLists(generation_t usedGen) + reclaim_memory(generation_t oldest_used_gen) { - _allocator.trimHoldLists(usedGen); - _store.trimHoldLists(usedGen); + _allocator.reclaim_memory(oldest_used_gen); + _store.reclaim_memory(oldest_used_gen); } // Inherit doc from DataStoreBase void - transferHoldLists(generation_t generation) + assign_generation(generation_t current_gen) { - _allocator.transferHoldLists(generation); - _store.transferHoldLists(generation); + _allocator.assign_generation(current_gen); + _store.assign_generation(current_gen); } void - clearHoldLists() + reclaim_all_memory() { - _allocator.clearHoldLists(); - _store.clearHoldLists(); + _allocator.reclaim_all_memory(); + _store.reclaim_all_memory(); } diff --git a/vespalib/src/vespa/vespalib/coro/CMakeLists.txt b/vespalib/src/vespa/vespalib/coro/CMakeLists.txt new file mode 100644 index 00000000000..d190c2e8ddc --- /dev/null +++ b/vespalib/src/vespa/vespalib/coro/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_library(vespalib_vespalib_coro OBJECT + SOURCES + DEPENDS +) diff --git a/vespalib/src/vespa/vespalib/coro/detached.h b/vespalib/src/vespa/vespalib/coro/detached.h new file mode 100644 index 00000000000..5e3fa1452fa --- /dev/null +++ b/vespalib/src/vespa/vespalib/coro/detached.h @@ -0,0 +1,32 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <coroutine> +#include <exception> + +namespace vespalib::coro { + +/** + * coroutine return type + * + * The coroutine is eager (will not suspend in initial_suspend) and + * self destroying (will not suspend in final_suspend). The return + * value gives no way of interacting with the coroutine. Without any + * co_await operations this acts similar to a normal subroutine. Note + * that letting a detached coroutine wait for a Lazy<T> will + * essentially attach it to the Lazy<T> as a continuation and resume + * it, but will require the Lazy<T> not to be deleted mid flight + * (started but not completed). + **/ +struct Detached { + struct promise_type { + Detached get_return_object() { return {}; } + static std::suspend_never initial_suspend() noexcept { return {}; } + static std::suspend_never final_suspend() noexcept { return {}; } + static void unhandled_exception() { std::terminate(); } + void return_void() noexcept {}; + }; +}; + +} diff --git a/vespalib/src/vespa/vespalib/coro/lazy.h b/vespalib/src/vespa/vespalib/coro/lazy.h new file mode 100644 index 00000000000..5a10c05bc24 --- /dev/null +++ b/vespalib/src/vespa/vespalib/coro/lazy.h @@ -0,0 +1,111 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <concepts> +#include <coroutine> +#include <optional> +#include <exception> +#include <utility> + +namespace vespalib::coro { + +/** + * coroutine return type + * + * The coroutine is lazy (will suspend in initial_suspend) and + * destroyed from the outside (will suspend in final_suspend). Waiting + * for a Lazy<T> using co_await will use symmetric transfer to suspend + * the waiting coroutine and resume this one. The waiting coroutine + * is registered as a continuation and will be resumed again once the + * result is available (also using symmetric transfer). The result is + * assumed to be produced asynchronously. If you need to access it + * from the outside (in that specific thread); use sync_wait. + **/ +template <std::movable T> +class [[nodiscard]] Lazy { +public: + struct promise_type { + Lazy<T> get_return_object() { return Lazy(Handle::from_promise(*this)); } + static std::suspend_always initial_suspend() noexcept { return {}; } + static auto final_suspend() noexcept { + struct awaiter { + bool await_ready() const noexcept { return false; } + std::coroutine_handle<> await_suspend(Handle handle) const noexcept { + return handle.promise().waiter; + } + void await_resume() const noexcept {} + }; + return awaiter(); + } + template <typename RET> + requires std::is_convertible_v<RET&&,T> + void return_value(RET &&ret_value) noexcept(std::is_nothrow_constructible_v<T,RET&&>) { + value = std::forward<RET>(ret_value); + } + void unhandled_exception() noexcept { + exception = std::current_exception(); + } + std::optional<T> value; + std::exception_ptr exception; + std::coroutine_handle<> waiter; + promise_type(promise_type &&) = delete; + promise_type(const promise_type &) = delete; + promise_type() noexcept : value(std::nullopt), exception(), waiter(std::noop_coroutine()) {} + T &result() & { + if (exception) { + std::rethrow_exception(exception); + } + return *value; + } + T &&result() && { + if (exception) { + std::rethrow_exception(exception); + } + return std::move(*value); + } + }; + using Handle = std::coroutine_handle<promise_type>; + +private: + Handle _handle; + + struct awaiter_base { + Handle handle; + awaiter_base(Handle handle_in) noexcept : handle(handle_in) {} + bool await_ready() const noexcept { return handle.done(); } + Handle await_suspend(std::coroutine_handle<> waiter) const noexcept { + handle.promise().waiter = waiter; + return handle; + } + }; + +public: + Lazy(const Lazy &) = delete; + Lazy &operator=(const Lazy &) = delete; + explicit Lazy(Handle handle_in) noexcept : _handle(handle_in) {} + Lazy(Lazy &&rhs) noexcept : _handle(std::exchange(rhs._handle, nullptr)) {} + auto operator co_await() & noexcept { + struct awaiter : awaiter_base { + using awaiter_base::handle; + awaiter(Handle handle_in) noexcept : awaiter_base(handle_in) {} + decltype(auto) await_resume() const { return handle.promise().result(); } + }; + return awaiter(_handle); + } + auto operator co_await() && noexcept { + struct awaiter : awaiter_base { + using awaiter_base::handle; + awaiter(Handle handle_in) noexcept : awaiter_base(handle_in) {} + decltype(auto) await_resume() const { return std::move(handle.promise()).result(); } + }; + return awaiter(_handle); + } + ~Lazy() { + if (_handle) { + _handle.destroy(); + } + } +}; + +} diff --git a/vespalib/src/vespa/vespalib/coro/sync_wait.h b/vespalib/src/vespa/vespalib/coro/sync_wait.h new file mode 100644 index 00000000000..bdea2dfc7f0 --- /dev/null +++ b/vespalib/src/vespa/vespalib/coro/sync_wait.h @@ -0,0 +1,59 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "detached.h" +#include "lazy.h" +#include <vespa/vespalib/util/gate.h> + +#include <coroutine> +#include <exception> + +namespace vespalib::coro { + +template <typename T, typename S> +Detached signal_when_done(Lazy<T> &value, S &sink) { + try { + sink(co_await value); + } catch (...) { + sink(std::current_exception()); + } +} + +/** + * Wait for a lazy value to be calculated (note that waiting for a + * value will also start calculating it). Make sure the thread waiting + * is not needed in the calculation of the value, or you will end up + * with a deadlock. + **/ +template <typename T> +T &sync_wait(Lazy<T> &value) { + struct MySink { + Gate gate; + T *result; + std::exception_ptr exception; + void operator()(T &result_in) { + result = &result_in; + gate.countDown(); + } + void operator()(std::exception_ptr exception_in) { + exception = exception_in; + gate.countDown(); + } + MySink() : gate(), result(nullptr), exception() {} + }; + MySink sink; + signal_when_done(value, sink); + sink.gate.await(); + if (sink.exception) { + std::rethrow_exception(sink.exception); + } + return *sink.result; +} + +template <typename T> +T &&sync_wait(Lazy<T> &&value) { + return std::move(sync_wait(value)); +} + +} diff --git a/vespalib/src/vespa/vespalib/datastore/CMakeLists.txt b/vespalib/src/vespa/vespalib/datastore/CMakeLists.txt index 9990e3f5764..f11004363f8 100644 --- a/vespalib/src/vespa/vespalib/datastore/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/datastore/CMakeLists.txt @@ -5,6 +5,7 @@ vespa_add_library(vespalib_vespalib_datastore OBJECT array_store_config.cpp atomic_entry_ref.cpp buffer_free_list.cpp + buffer_stats.cpp buffer_type.cpp bufferstate.cpp compact_buffer_candidates.cpp @@ -19,6 +20,7 @@ vespa_add_library(vespalib_vespalib_datastore OBJECT fixed_size_hash_map.cpp free_list.cpp large_array_buffer_type.cpp + memory_stats.cpp sharded_hash_map.cpp small_array_buffer_type.cpp unique_store.cpp diff --git a/vespalib/src/vespa/vespalib/datastore/allocator.hpp b/vespalib/src/vespa/vespalib/datastore/allocator.hpp index 9b69be49b8e..a65fd8a2352 100644 --- a/vespalib/src/vespa/vespalib/datastore/allocator.hpp +++ b/vespalib/src/vespa/vespalib/datastore/allocator.hpp @@ -28,7 +28,7 @@ Allocator<EntryT, RefT>::alloc(Args && ... args) RefT ref(oldBufferSize, buffer_id); EntryT *entry = _store.getEntry<EntryT>(ref); new (static_cast<void *>(entry)) EntryT(std::forward<Args>(args)...); - state.pushed_back(1); + state.stats().pushed_back(1); return HandleType(ref, entry); } @@ -48,7 +48,7 @@ Allocator<EntryT, RefT>::allocArray(ConstArrayRef array) for (size_t i = 0; i < array.size(); ++i) { new (static_cast<void *>(buf + i)) EntryT(array[i]); } - state.pushed_back(array.size()); + state.stats().pushed_back(array.size()); return HandleType(ref, buf); } @@ -68,7 +68,7 @@ Allocator<EntryT, RefT>::allocArray(size_t size) for (size_t i = 0; i < size; ++i) { new (static_cast<void *>(buf + i)) EntryT(); } - state.pushed_back(size); + state.stats().pushed_back(size); return HandleType(ref, buf); } diff --git a/vespalib/src/vespa/vespalib/datastore/array_store.h b/vespalib/src/vespa/vespalib/datastore/array_store.h index db037ee12fb..95d632d9603 100644 --- a/vespalib/src/vespa/vespalib/datastore/array_store.h +++ b/vespalib/src/vespa/vespalib/datastore/array_store.h @@ -10,6 +10,7 @@ #include "entryref.h" #include "atomic_entry_ref.h" #include "i_compaction_context.h" +#include "i_compactable.h" #include "large_array_buffer_type.h" #include "small_array_buffer_type.h" #include <vespa/vespalib/util/array.h> @@ -28,7 +29,7 @@ namespace vespalib::datastore { * The max value of maxSmallArrayTypeId is (2^bufferBits - 1). */ template <typename EntryT, typename RefT = EntryRefT<19>, typename TypeMapperT = ArrayStoreTypeMapper<EntryT> > -class ArrayStore +class ArrayStore : public ICompactable { public: using AllocSpec = ArrayStoreConfig::AllocSpec; @@ -66,7 +67,7 @@ private: public: ArrayStore(const ArrayStoreConfig &cfg, std::shared_ptr<alloc::MemoryAllocator> memory_allocator); ArrayStore(const ArrayStoreConfig &cfg, std::shared_ptr<alloc::MemoryAllocator> memory_allocator, TypeMapper&& mapper); - ~ArrayStore(); + ~ArrayStore() override; EntryRef add(const ConstArrayRef &array); ConstArrayRef get(EntryRef ref) const { if (!ref.valid()) { @@ -104,6 +105,7 @@ public: } void remove(EntryRef ref); + EntryRef move_on_compact(EntryRef ref) override; ICompactionContext::UP compactWorst(CompactionSpec compaction_spec, const CompactionStrategy& compaction_strategy); vespalib::MemoryUsage getMemoryUsage() const { return _store.getMemoryUsage(); } @@ -114,8 +116,8 @@ public: vespalib::AddressSpace addressSpaceUsage() const; // Pass on hold list management to underlying store - void transferHoldLists(generation_t generation) { _store.transferHoldLists(generation); } - void trimHoldLists(generation_t firstUsed) { _store.trimHoldLists(firstUsed); } + void assign_generation(generation_t current_gen) { _store.assign_generation(current_gen); } + void reclaim_memory(generation_t oldest_used_gen) { _store.reclaim_memory(oldest_used_gen); } vespalib::GenerationHolder &getGenerationHolder() { return _store.getGenerationHolder(); } void setInitializing(bool initializing) { _store.setInitializing(initializing); } diff --git a/vespalib/src/vespa/vespalib/datastore/array_store.hpp b/vespalib/src/vespa/vespalib/datastore/array_store.hpp index e79398271fb..95f4a3c4155 100644 --- a/vespalib/src/vespa/vespalib/datastore/array_store.hpp +++ b/vespalib/src/vespa/vespalib/datastore/array_store.hpp @@ -4,13 +4,14 @@ #include "array_store.h" #include "compacting_buffers.h" +#include "compaction_context.h" #include "compaction_spec.h" -#include "entry_ref_filter.h" #include "datastore.hpp" +#include "entry_ref_filter.h" #include "large_array_buffer_type.hpp" #include "small_array_buffer_type.hpp" -#include <atomic> #include <algorithm> +#include <atomic> namespace vespalib::datastore { @@ -57,7 +58,7 @@ ArrayStore<EntryT, RefT, TypeMapperT>::ArrayStore(const ArrayStoreConfig &cfg, s template <typename EntryT, typename RefT, typename TypeMapperT> ArrayStore<EntryT, RefT, TypeMapperT>::~ArrayStore() { - _store.clearHoldLists(); + _store.reclaim_all_memory(); _store.dropBuffers(); } @@ -114,7 +115,7 @@ ArrayStore<EntryT, RefT, TypeMapperT>::addLargeArray(const ConstArrayRef &array) auto handle = _store.template freeListAllocator<LargeArray, NoOpReclaimer>(_largeArrayTypeId) .alloc(array.cbegin(), array.cend()); auto& state = _store.getBufferState(RefT(handle.ref).bufferId()); - state.incExtraUsedBytes(sizeof(EntryT) * array.size()); + state.stats().inc_extra_used_bytes(sizeof(EntryT) * array.size()); return handle.ref; } @@ -125,7 +126,7 @@ ArrayStore<EntryT, RefT, TypeMapperT>::allocate_large_array(size_t array_size) using NoOpReclaimer = DefaultReclaimer<LargeArray>; auto handle = _store.template freeListAllocator<LargeArray, NoOpReclaimer>(_largeArrayTypeId).alloc(array_size); auto& state = _store.getBufferState(RefT(handle.ref).bufferId()); - state.incExtraUsedBytes(sizeof(EntryT) * array_size); + state.stats().inc_extra_used_bytes(sizeof(EntryT) * array_size); return handle.ref; } @@ -145,38 +146,11 @@ ArrayStore<EntryT, RefT, TypeMapperT>::remove(EntryRef ref) } } -namespace arraystore { - template <typename EntryT, typename RefT, typename TypeMapperT> -class CompactionContext : public ICompactionContext { -private: - using ArrayStoreType = ArrayStore<EntryT, RefT, TypeMapperT>; - ArrayStoreType &_store; - std::unique_ptr<vespalib::datastore::CompactingBuffers> _compacting_buffers; - EntryRefFilter _filter; - -public: - CompactionContext(ArrayStoreType &store, - std::unique_ptr<vespalib::datastore::CompactingBuffers> compacting_buffers) - : _store(store), - _compacting_buffers(std::move(compacting_buffers)), - _filter(_compacting_buffers->make_entry_ref_filter()) - { - } - ~CompactionContext() override { - _compacting_buffers->finish(); - } - void compact(vespalib::ArrayRef<AtomicEntryRef> refs) override { - for (auto &atomic_entry_ref : refs) { - auto ref = atomic_entry_ref.load_relaxed(); - if (ref.valid() && _filter.has(ref)) { - EntryRef newRef = _store.add(_store.get(ref)); - atomic_entry_ref.store_release(newRef); - } - } - } -}; - +EntryRef +ArrayStore<EntryT, RefT, TypeMapperT>::move_on_compact(EntryRef ref) +{ + return add(get(ref)); } template <typename EntryT, typename RefT, typename TypeMapperT> @@ -184,8 +158,7 @@ ICompactionContext::UP ArrayStore<EntryT, RefT, TypeMapperT>::compactWorst(CompactionSpec compaction_spec, const CompactionStrategy &compaction_strategy) { auto compacting_buffers = _store.start_compact_worst_buffers(compaction_spec, compaction_strategy); - return std::make_unique<arraystore::CompactionContext<EntryT, RefT, TypeMapperT>> - (*this, std::move(compacting_buffers)); + return std::make_unique<CompactionContext>(*this, std::move(compacting_buffers)); } template <typename EntryT, typename RefT, typename TypeMapperT> diff --git a/vespalib/src/vespa/vespalib/datastore/buffer_stats.cpp b/vespalib/src/vespa/vespalib/datastore/buffer_stats.cpp new file mode 100644 index 00000000000..8d97414626e --- /dev/null +++ b/vespalib/src/vespa/vespalib/datastore/buffer_stats.cpp @@ -0,0 +1,57 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "buffer_stats.h" +#include <cassert> + +namespace vespalib::datastore { + +BufferStats::BufferStats() + : _alloc_elems(0), + _used_elems(0), + _hold_elems(0), + _dead_elems(0), + _extra_used_bytes(0), + _extra_hold_bytes(0) +{ +} + +void +BufferStats::add_to_mem_stats(size_t element_size, MemoryStats& stats) const +{ + size_t extra_used = extra_used_bytes(); + stats._allocElems += capacity(); + stats._usedElems += size(); + stats._deadElems += dead_elems(); + stats._holdElems += hold_elems(); + stats._allocBytes += (capacity() * element_size) + extra_used; + stats._usedBytes += (size() * element_size) + extra_used; + stats._deadBytes += dead_elems() * element_size; + stats._holdBytes += (hold_elems() * element_size) + extra_hold_bytes(); +} + +InternalBufferStats::InternalBufferStats() + : BufferStats() +{ +} + +void +InternalBufferStats::clear() +{ + _alloc_elems.store(0, std::memory_order_relaxed); + _used_elems.store(0, std::memory_order_relaxed); + _hold_elems.store(0, std::memory_order_relaxed); + _dead_elems.store(0, std::memory_order_relaxed); + _extra_used_bytes.store(0, std::memory_order_relaxed); + _extra_hold_bytes.store(0, std::memory_order_relaxed); +} + +void +InternalBufferStats::dec_hold_elems(size_t value) +{ + ElemCount elems = hold_elems(); + assert(elems >= value); + _hold_elems.store(elems - value, std::memory_order_relaxed); +} + +} + diff --git a/vespalib/src/vespa/vespalib/datastore/buffer_stats.h b/vespalib/src/vespa/vespalib/datastore/buffer_stats.h new file mode 100644 index 00000000000..66f8b532c41 --- /dev/null +++ b/vespalib/src/vespa/vespalib/datastore/buffer_stats.h @@ -0,0 +1,76 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "buffer_type.h" +#include "memory_stats.h" +#include <atomic> + +namespace vespalib::datastore { + +/** + * Represents statistics for a given buffer in a data store. + */ +class BufferStats { +protected: + // The number of elements that are allocated in the buffer. + std::atomic<ElemCount> _alloc_elems; + // The number of elements (of the allocated) that are used: _used_elems <= _alloc_elems. + std::atomic<ElemCount> _used_elems; + // The number of elements (of the used) that are on hold: _hold_elems <= _used_elems. + // "On hold" is a transitionary state used when removing elements. + std::atomic<ElemCount> _hold_elems; + // The number of elements (of the used) that are dead: _dead_elems <= _used_elems. + // A dead element was first on hold, and is now available for reuse in the free list (if enabled). + std::atomic<ElemCount> _dead_elems; + + // Number of bytes that are heap allocated (and used) by elements that are stored in this buffer. + // For simple types this is always 0. + std::atomic<size_t> _extra_used_bytes; + // Number of bytes that are heap allocated (and used) by elements that are stored in this buffer and is now on hold. + // For simple types this is always 0. + std::atomic<size_t> _extra_hold_bytes; + +public: + BufferStats(); + + size_t size() const { return _used_elems.load(std::memory_order_relaxed); } + size_t capacity() const { return _alloc_elems.load(std::memory_order_relaxed); } + size_t remaining() const { return capacity() - size(); } + + void pushed_back(size_t num_elems) { + _used_elems.store(size() + num_elems, std::memory_order_relaxed); + } + + size_t dead_elems() const { return _dead_elems.load(std::memory_order_relaxed); } + size_t hold_elems() const { return _hold_elems.load(std::memory_order_relaxed); } + size_t extra_used_bytes() const { return _extra_used_bytes.load(std::memory_order_relaxed); } + size_t extra_hold_bytes() const { return _extra_hold_bytes.load(std::memory_order_relaxed); } + + void inc_extra_used_bytes(size_t value) { _extra_used_bytes.store(extra_used_bytes() + value, std::memory_order_relaxed); } + + void add_to_mem_stats(size_t element_size, MemoryStats& stats) const; +}; + +/** + * Provides low-level access to buffer stats for integration in BufferState. + */ +class InternalBufferStats : public BufferStats { +public: + InternalBufferStats(); + void clear(); + void set_alloc_elems(size_t value) { _alloc_elems.store(value, std::memory_order_relaxed); } + void set_dead_elems(size_t value) { _dead_elems.store(value, std::memory_order_relaxed); } + void set_hold_elems(size_t value) { _hold_elems.store(value, std::memory_order_relaxed); } + void inc_dead_elems(size_t value) { _dead_elems.store(dead_elems() + value, std::memory_order_relaxed); } + void inc_hold_elems(size_t value) { _hold_elems.store(hold_elems() + value, std::memory_order_relaxed); } + void dec_hold_elems(size_t value); + void inc_extra_hold_bytes(size_t value) { _extra_hold_bytes.store(extra_hold_bytes() + value, std::memory_order_relaxed); } + std::atomic<ElemCount>& used_elems_ref() { return _used_elems; } + std::atomic<ElemCount>& dead_elems_ref() { return _dead_elems; } + std::atomic<size_t>& extra_used_bytes_ref() { return _extra_used_bytes; } + std::atomic<size_t>& extra_hold_bytes_ref() { return _extra_hold_bytes; } +}; + +} + diff --git a/vespalib/src/vespa/vespalib/datastore/bufferstate.cpp b/vespalib/src/vespa/vespalib/datastore/bufferstate.cpp index d24d8336131..45a94693eeb 100644 --- a/vespalib/src/vespa/vespalib/datastore/bufferstate.cpp +++ b/vespalib/src/vespa/vespalib/datastore/bufferstate.cpp @@ -11,13 +11,8 @@ using vespalib::alloc::MemoryAllocator; namespace vespalib::datastore { BufferState::BufferState() - : _usedElems(0), - _allocElems(0), - _deadElems(0u), - _holdElems(0u), - _extraUsedBytes(0), - _extraHoldBytes(0), - _free_list(_deadElems), + : _stats(), + _free_list(_stats.dead_elems_ref()), _typeHandler(nullptr), _buffer(Alloc::alloc(0, MemoryAllocator::HUGEPAGE_SIZE)), _arraySize(0), @@ -33,14 +28,7 @@ BufferState::~BufferState() assert(getState() == State::FREE); assert(!_free_list.enabled()); assert(_free_list.empty()); - assert(_holdElems == 0); -} - -void -BufferState::decHoldElems(size_t value) { - ElemCount hold_elems = getHoldElems(); - assert(hold_elems >= value); - _holdElems.store(hold_elems - value, std::memory_order_relaxed); + assert(_stats.hold_elems() == 0); } namespace { @@ -100,10 +88,10 @@ BufferState::onActive(uint32_t bufferId, uint32_t typeId, assert(_typeHandler == nullptr); assert(capacity() == 0); assert(size() == 0); - assert(getDeadElems() == 0u); - assert(getHoldElems() == 0); - assert(getExtraUsedBytes() == 0); - assert(getExtraHoldBytes() == 0); + assert(_stats.dead_elems() == 0u); + assert(_stats.hold_elems() == 0); + assert(_stats.extra_used_bytes() == 0); + assert(_stats.extra_hold_bytes() == 0); assert(_free_list.empty()); size_t reservedElements = typeHandler->getReservedElements(bufferId); @@ -115,14 +103,15 @@ BufferState::onActive(uint32_t bufferId, uint32_t typeId, _buffer.create(alloc.bytes).swap(_buffer); assert(_buffer.get() != nullptr || alloc.elements == 0u); buffer.store(_buffer.get(), std::memory_order_release); - _allocElems.store(alloc.elements, std::memory_order_relaxed); + _stats.set_alloc_elems(alloc.elements); _typeHandler.store(typeHandler, std::memory_order_release); assert(typeId <= std::numeric_limits<uint16_t>::max()); _typeId = typeId; _arraySize = typeHandler->getArraySize(); _free_list.set_array_size(_arraySize); _state.store(State::ACTIVE, std::memory_order_release); - typeHandler->onActive(bufferId, &_usedElems, &_deadElems, buffer.load(std::memory_order::relaxed)); + typeHandler->onActive(bufferId, &_stats.used_elems_ref(), &_stats.dead_elems_ref(), + buffer.load(std::memory_order::relaxed)); } void @@ -132,11 +121,11 @@ BufferState::onHold(uint32_t buffer_id) assert(getTypeHandler() != nullptr); _state.store(State::HOLD, std::memory_order_release); _compacting = false; - assert(getDeadElems() <= size()); - assert(getHoldElems() <= (size() - getDeadElems())); - _deadElems.store(0, std::memory_order_relaxed); - _holdElems.store(size(), std::memory_order_relaxed); // Put everyting on hold - getTypeHandler()->onHold(buffer_id, &_usedElems, &_deadElems); + assert(_stats.dead_elems() <= size()); + assert(_stats.hold_elems() <= (size() - _stats.dead_elems())); + _stats.set_dead_elems(0); + _stats.set_hold_elems(size()); + getTypeHandler()->onHold(buffer_id, &_stats.used_elems_ref(), &_stats.dead_elems_ref()); _free_list.disable(); } @@ -146,18 +135,13 @@ BufferState::onFree(std::atomic<void*>& buffer) assert(buffer.load(std::memory_order_relaxed) == _buffer.get()); assert(getState() == State::HOLD); assert(_typeHandler != nullptr); - assert(getDeadElems() <= size()); - assert(getHoldElems() == size() - getDeadElems()); + assert(_stats.dead_elems() <= size()); + assert(_stats.hold_elems() == (size() - _stats.dead_elems())); getTypeHandler()->destroyElements(buffer, size()); Alloc::alloc().swap(_buffer); getTypeHandler()->onFree(size()); buffer.store(nullptr, std::memory_order_release); - _usedElems.store(0, std::memory_order_relaxed); - _allocElems.store(0, std::memory_order_relaxed); - _deadElems.store(0, std::memory_order_relaxed); - _holdElems.store(0, std::memory_order_relaxed); - _extraUsedBytes.store(0, std::memory_order_relaxed); - _extraHoldBytes.store(0, std::memory_order_relaxed); + _stats.clear(); _state.store(State::FREE, std::memory_order_release); _typeHandler = nullptr; _arraySize = 0; @@ -192,6 +176,36 @@ BufferState::disableElemHoldList() _disableElemHoldList = true; } +bool +BufferState::hold_elems(size_t num_elems, size_t extra_bytes) +{ + assert(isActive()); + if (_disableElemHoldList) { + // The elements are directly marked as dead as they are not put on hold. + _stats.inc_dead_elems(num_elems); + return true; + } + _stats.inc_hold_elems(num_elems); + _stats.inc_extra_hold_bytes(extra_bytes); + return false; +} + +void +BufferState::free_elems(EntryRef ref, size_t num_elems, size_t ref_offset) +{ + if (isActive()) { + if (_free_list.enabled() && (num_elems == getArraySize())) { + _free_list.push_entry(ref); + } + } else { + assert(isOnHold()); + } + _stats.inc_dead_elems(num_elems); + _stats.dec_hold_elems(num_elems); + getTypeHandler()->cleanHold(_buffer.get(), (ref_offset * _arraySize), num_elems, + BufferTypeBase::CleanContext(_stats.extra_used_bytes_ref(), + _stats.extra_hold_bytes_ref())); +} void BufferState::fallbackResize(uint32_t bufferId, @@ -211,13 +225,13 @@ BufferState::fallbackResize(uint32_t bufferId, std::atomic_thread_fence(std::memory_order_release); _buffer = std::move(newBuffer); buffer.store(_buffer.get(), std::memory_order_release); - _allocElems.store(alloc.elements, std::memory_order_relaxed); + _stats.set_alloc_elems(alloc.elements); } void BufferState::resume_primary_buffer(uint32_t buffer_id) { - getTypeHandler()->resume_primary_buffer(buffer_id, &_usedElems, &_deadElems); + getTypeHandler()->resume_primary_buffer(buffer_id, &_stats.used_elems_ref(), &_stats.dead_elems_ref()); } } diff --git a/vespalib/src/vespa/vespalib/datastore/bufferstate.h b/vespalib/src/vespa/vespalib/datastore/bufferstate.h index 8f32a93b487..3f023b41c51 100644 --- a/vespalib/src/vespa/vespalib/datastore/bufferstate.h +++ b/vespalib/src/vespa/vespalib/datastore/bufferstate.h @@ -3,6 +3,7 @@ #pragma once #include "buffer_free_list.h" +#include "buffer_stats.h" #include "buffer_type.h" #include "entryref.h" #include <vespa/vespalib/util/generationhandler.h> @@ -38,17 +39,7 @@ public: }; private: - std::atomic<ElemCount> _usedElems; - std::atomic<ElemCount> _allocElems; - std::atomic<ElemCount> _deadElems; - std::atomic<ElemCount> _holdElems; - // Number of bytes that are heap allocated by elements that are stored in this buffer. - // For simple types this is 0. - std::atomic<size_t> _extraUsedBytes; - // Number of bytes that are heap allocated by elements that are stored in this buffer and is now on hold. - // For simple types this is 0. - std::atomic<size_t> _extraHoldBytes; - + InternalBufferStats _stats; BufferFreeList _free_list; std::atomic<BufferTypeBase*> _typeHandler; Alloc _buffer; @@ -91,36 +82,38 @@ public: */ void onFree(std::atomic<void*>& buffer); - /** - * Disable hold of elements, just mark then as dead without cleanup. + * Disable hold of elements, just mark elements as dead without cleanup. * Typically used when tearing down data structure in a controlled manner. */ void disableElemHoldList(); - BufferFreeList& free_list() { return _free_list; } - const BufferFreeList& free_list() const { return _free_list; } + /** + * Update stats to reflect that the given elements are put on hold. + * Returns true if element hold list is disabled for this buffer. + */ + bool hold_elems(size_t num_elems, size_t extra_bytes); - size_t size() const { return _usedElems.load(std::memory_order_relaxed); } - size_t capacity() const { return _allocElems.load(std::memory_order_relaxed); } - size_t remaining() const { return capacity() - size(); } - void pushed_back(size_t numElems) { - pushed_back(numElems, 0); - } - void pushed_back(size_t numElems, size_t extraBytes) { - _usedElems.store(size() + numElems, std::memory_order_relaxed); - _extraUsedBytes.store(getExtraUsedBytes() + extraBytes, std::memory_order_relaxed); - } - void cleanHold(void *buffer, size_t offset, ElemCount numElems) { - getTypeHandler()->cleanHold(buffer, offset, numElems, BufferTypeBase::CleanContext(_extraUsedBytes, _extraHoldBytes)); - } + /** + * Free the given elements and update stats accordingly. + * + * The given entry ref is put on the free list (if enabled). + * Hold cleaning of elements is executed on the buffer type. + */ + void free_elems(EntryRef ref, size_t num_elems, size_t ref_offset); + + BufferStats& stats() { return _stats; } + const BufferStats& stats() const { return _stats; } + + void enable_free_list(FreeList& type_free_list) { _free_list.enable(type_free_list); } + void disable_free_list() { _free_list.disable(); } + + size_t size() const { return _stats.size(); } + size_t capacity() const { return _stats.capacity(); } + size_t remaining() const { return _stats.remaining(); } void dropBuffer(uint32_t buffer_id, std::atomic<void*>& buffer); uint32_t getTypeId() const { return _typeId; } uint32_t getArraySize() const { return _arraySize; } - size_t getDeadElems() const { return _deadElems.load(std::memory_order_relaxed); } - size_t getHoldElems() const { return _holdElems.load(std::memory_order_relaxed); } - size_t getExtraUsedBytes() const { return _extraUsedBytes.load(std::memory_order_relaxed); } - size_t getExtraHoldBytes() const { return _extraHoldBytes.load(std::memory_order_relaxed); } bool getCompacting() const { return _compacting; } void setCompacting() { _compacting = true; } uint32_t get_used_arrays() const noexcept { return size() / _arraySize; } @@ -136,15 +129,6 @@ public: const BufferTypeBase *getTypeHandler() const { return _typeHandler.load(std::memory_order_relaxed); } BufferTypeBase *getTypeHandler() { return _typeHandler.load(std::memory_order_relaxed); } - void incDeadElems(size_t value) { _deadElems.store(getDeadElems() + value, std::memory_order_relaxed); } - void incHoldElems(size_t value) { _holdElems.store(getHoldElems() + value, std::memory_order_relaxed); } - void decHoldElems(size_t value); - void incExtraUsedBytes(size_t value) { _extraUsedBytes.store(getExtraUsedBytes() + value, std::memory_order_relaxed); } - void incExtraHoldBytes(size_t value) { - _extraHoldBytes.store(getExtraHoldBytes() + value, std::memory_order_relaxed); - } - - bool hasDisabledElemHoldList() const { return _disableElemHoldList; } void resume_primary_buffer(uint32_t buffer_id); }; diff --git a/vespalib/src/vespa/vespalib/datastore/compact_buffer_candidates.cpp b/vespalib/src/vespa/vespalib/datastore/compact_buffer_candidates.cpp index 41c216a9684..dd47a159e9b 100644 --- a/vespalib/src/vespa/vespalib/datastore/compact_buffer_candidates.cpp +++ b/vespalib/src/vespa/vespalib/datastore/compact_buffer_candidates.cpp @@ -13,7 +13,7 @@ CompactBufferCandidates::CompactBufferCandidates(uint32_t num_buffers, uint32_t _max_buffers(std::max(max_buffers, 1u)), _active_buffers_ratio(std::min(1.0, std::max(0.0001, active_buffers_ratio))), _ratio(ratio), - _slack(slack), + _slack(_ratio == 0.0 ? 0u : slack), _free_buffers(0) { _candidates.reserve(num_buffers); diff --git a/vespalib/src/vespa/vespalib/datastore/compaction_context.cpp b/vespalib/src/vespa/vespalib/datastore/compaction_context.cpp index 65e028119a2..1ce6401605e 100644 --- a/vespalib/src/vespa/vespalib/datastore/compaction_context.cpp +++ b/vespalib/src/vespa/vespalib/datastore/compaction_context.cpp @@ -25,7 +25,7 @@ CompactionContext::compact(vespalib::ArrayRef<AtomicEntryRef> refs) for (auto &atomic_entry_ref : refs) { auto ref = atomic_entry_ref.load_relaxed(); if (ref.valid() && _filter.has(ref)) { - EntryRef newRef = _store.move(ref); + EntryRef newRef = _store.move_on_compact(ref); atomic_entry_ref.store_release(newRef); } } diff --git a/vespalib/src/vespa/vespalib/datastore/compaction_strategy.cpp b/vespalib/src/vespa/vespalib/datastore/compaction_strategy.cpp index 2dbd501f78e..4eb4ff16864 100644 --- a/vespalib/src/vespa/vespalib/datastore/compaction_strategy.cpp +++ b/vespalib/src/vespa/vespalib/datastore/compaction_strategy.cpp @@ -5,6 +5,7 @@ #include <vespa/vespalib/util/memoryusage.h> #include <vespa/vespalib/util/address_space.h> #include <iostream> +#include <limits> namespace vespalib::datastore { @@ -34,4 +35,10 @@ std::ostream& operator<<(std::ostream& os, const CompactionStrategy& compaction_ return os; } +CompactionStrategy +CompactionStrategy::make_compact_all_active_buffers_strategy() +{ + return CompactionStrategy(0.0, 0.0, std::numeric_limits<uint32_t>::max(), 1.0); +} + } diff --git a/vespalib/src/vespa/vespalib/datastore/compaction_strategy.h b/vespalib/src/vespa/vespalib/datastore/compaction_strategy.h index 2bcf30fc6fc..f78e123e5de 100644 --- a/vespalib/src/vespa/vespalib/datastore/compaction_strategy.h +++ b/vespalib/src/vespa/vespalib/datastore/compaction_strategy.h @@ -74,6 +74,7 @@ public: bool should_compact_memory(const MemoryUsage& memory_usage) const; bool should_compact_address_space(const AddressSpace& address_space) const; CompactionSpec should_compact(const MemoryUsage& memory_usage, const AddressSpace& address_space) const; + static CompactionStrategy make_compact_all_active_buffers_strategy(); }; std::ostream& operator<<(std::ostream& os, const CompactionStrategy& compaction_strategy); diff --git a/vespalib/src/vespa/vespalib/datastore/datastore.cpp b/vespalib/src/vespa/vespalib/datastore/datastore.cpp index 15686a9f79d..76d622f3704 100644 --- a/vespalib/src/vespa/vespalib/datastore/datastore.cpp +++ b/vespalib/src/vespa/vespalib/datastore/datastore.cpp @@ -1,7 +1,6 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "datastore.hpp" -#include <vespa/vespalib/util/array.hpp> #include <vespa/vespalib/util/rcuvector.hpp> namespace vespalib::datastore { @@ -10,7 +9,6 @@ template class DataStoreT<EntryRefT<22> >; } -template void vespalib::Array<vespalib::datastore::DataStoreBase::ElemHold1ListElem>::increase(size_t); template class vespalib::RcuVector<vespalib::datastore::EntryRef>; template class vespalib::RcuVectorBase<vespalib::datastore::EntryRef>; template class vespalib::RcuVector<vespalib::datastore::AtomicEntryRef>; diff --git a/vespalib/src/vespa/vespalib/datastore/datastore.h b/vespalib/src/vespa/vespalib/datastore/datastore.h index 86c39b547d3..95f47e98ef5 100644 --- a/vespalib/src/vespa/vespalib/datastore/datastore.h +++ b/vespalib/src/vespa/vespalib/datastore/datastore.h @@ -27,7 +27,7 @@ template <typename RefT = EntryRefT<22> > class DataStoreT : public DataStoreBase { private: - void free_elem_internal(EntryRef ref, size_t numElems, bool was_held); + void free_elem_internal(EntryRef ref, size_t numElems); public: typedef RefT RefType; @@ -38,22 +38,6 @@ public: ~DataStoreT() override; /** - * Increase number of dead elements in buffer. - * - * @param ref Reference to dead stored features - * @param dead Number of newly dead elements - */ - void incDead(EntryRef ref, size_t deadElems) { - RefType intRef(ref); - DataStoreBase::incDead(intRef.bufferId(), deadElems); - } - - /** - * Free element(s). - */ - void freeElem(EntryRef ref, size_t numElems) { free_elem_internal(ref, numElems, false); } - - /** * Hold element(s). */ void holdElem(EntryRef ref, size_t numElems) { @@ -61,14 +45,9 @@ public: } void holdElem(EntryRef ref, size_t numElems, size_t extraBytes); - /** - * Trim elem hold list, freeing elements that no longer needs to be held. - * - * @param usedGen lowest generation that is still used. - */ - void trimElemHoldList(generation_t usedGen) override; + void reclaim_entry_refs(generation_t oldest_used_gen) override; - void clearElemHoldList() override; + void reclaim_all_entry_refs() override; bool getCompacting(EntryRef ref) const { return getBufferState(RefType(ref).bufferId()).getCompacting(); @@ -97,7 +76,6 @@ class DataStore : public DataStoreT<RefT> protected: typedef DataStoreT<RefT> ParentType; using ParentType::ensureBufferCapacity; - using ParentType::_primary_buffer_ids; using ParentType::getEntry; using ParentType::dropBuffers; using ParentType::init_primary_buffers; diff --git a/vespalib/src/vespa/vespalib/datastore/datastore.hpp b/vespalib/src/vespa/vespalib/datastore/datastore.hpp index 5b8df719915..bfb63954875 100644 --- a/vespalib/src/vespa/vespalib/datastore/datastore.hpp +++ b/vespalib/src/vespa/vespalib/datastore/datastore.hpp @@ -7,7 +7,7 @@ #include "free_list_allocator.hpp" #include "free_list_raw_allocator.hpp" #include "raw_allocator.hpp" -#include <vespa/vespalib/util/array.hpp> +#include <vespa/vespalib/util/generation_hold_list.hpp> namespace vespalib::datastore { @@ -22,23 +22,11 @@ DataStoreT<RefT>::~DataStoreT() = default; template <typename RefT> void -DataStoreT<RefT>::free_elem_internal(EntryRef ref, size_t numElems, bool was_held) +DataStoreT<RefT>::free_elem_internal(EntryRef ref, size_t numElems) { RefType intRef(ref); BufferState &state = getBufferState(intRef.bufferId()); - if (state.isActive()) { - if (state.free_list().enabled() && (numElems == state.getArraySize())) { - state.free_list().push_entry(ref); - } - } else { - assert(state.isOnHold() && was_held); - } - state.incDeadElems(numElems); - if (was_held) { - state.decHoldElems(numElems); - } - state.cleanHold(getBuffer(intRef.bufferId()), - intRef.offset() * state.getArraySize(), numElems); + state.free_elems(ref, numElems, intRef.offset()); } template <typename RefT> @@ -47,48 +35,27 @@ DataStoreT<RefT>::holdElem(EntryRef ref, size_t numElems, size_t extraBytes) { RefType intRef(ref); BufferState &state = getBufferState(intRef.bufferId()); - assert(state.isActive()); - if (state.hasDisabledElemHoldList()) { - state.incDeadElems(numElems); - return; + if (!state.hold_elems(numElems, extraBytes)) { + _entry_ref_hold_list.insert({ref, numElems}); } - _elemHold1List.push_back(ElemHold1ListElem(ref, numElems)); - state.incHoldElems(numElems); - state.incExtraHoldBytes(extraBytes); } template <typename RefT> void -DataStoreT<RefT>::trimElemHoldList(generation_t usedGen) +DataStoreT<RefT>::reclaim_entry_refs(generation_t oldest_used_gen) { - ElemHold2List &elemHold2List = _elemHold2List; - - ElemHold2List::iterator it(elemHold2List.begin()); - ElemHold2List::iterator ite(elemHold2List.end()); - uint32_t freed = 0; - for (; it != ite; ++it) { - if (static_cast<sgeneration_t>(it->_generation - usedGen) >= 0) - break; - free_elem_internal(it->_ref, it->_len, true); - ++freed; - } - if (freed != 0) { - elemHold2List.erase(elemHold2List.begin(), it); - } + _entry_ref_hold_list.reclaim(oldest_used_gen, [this](const auto& elem) { + free_elem_internal(elem.ref, elem.num_elems); + }); } template <typename RefT> void -DataStoreT<RefT>::clearElemHoldList() +DataStoreT<RefT>::reclaim_all_entry_refs() { - ElemHold2List &elemHold2List = _elemHold2List; - - ElemHold2List::iterator it(elemHold2List.begin()); - ElemHold2List::iterator ite(elemHold2List.end()); - for (; it != ite; ++it) { - free_elem_internal(it->_ref, it->_len, true); - } - elemHold2List.clear(); + _entry_ref_hold_list.reclaim_all([this](const auto& elem) { + free_elem_internal(elem.ref, elem.num_elems); + }); } template <typename RefT> diff --git a/vespalib/src/vespa/vespalib/datastore/datastorebase.cpp b/vespalib/src/vespa/vespalib/datastore/datastorebase.cpp index 67749ee913d..a14082e2d5c 100644 --- a/vespalib/src/vespa/vespalib/datastore/datastorebase.cpp +++ b/vespalib/src/vespa/vespalib/datastore/datastorebase.cpp @@ -5,7 +5,7 @@ #include "compacting_buffers.h" #include "compaction_spec.h" #include "compaction_strategy.h" -#include <vespa/vespalib/util/array.hpp> +#include <vespa/vespalib/util/generation_hold_list.hpp> #include <vespa/vespalib/util/stringfmt.h> #include <algorithm> #include <limits> @@ -16,6 +16,10 @@ LOG_SETUP(".vespalib.datastore.datastorebase"); using vespalib::GenerationHeldBase; +namespace vespalib { +template class GenerationHoldList<datastore::DataStoreBase::EntryRefHoldElem, false, true>; +} + namespace vespalib::datastore { namespace { @@ -36,7 +40,7 @@ constexpr size_t TOO_DEAD_SLACK = 0x4000u; bool primary_buffer_too_dead(const BufferState &state) { - size_t deadElems = state.getDeadElems(); + size_t deadElems = state.stats().dead_elems(); size_t deadBytes = deadElems * state.getArraySize(); return ((deadBytes >= TOO_DEAD_SLACK) && (deadElems * 2 >= state.size())); } @@ -88,8 +92,7 @@ DataStoreBase::DataStoreBase(uint32_t numBuffers, uint32_t offset_bits, size_t m _free_lists(), _freeListsEnabled(false), _initializing(false), - _elemHold1List(), - _elemHold2List(), + _entry_ref_hold_list(), _numBuffers(numBuffers), _offset_bits(offset_bits), _hold_buffer_count(0u), @@ -102,9 +105,6 @@ DataStoreBase::DataStoreBase(uint32_t numBuffers, uint32_t offset_bits, size_t m DataStoreBase::~DataStoreBase() { disableFreeLists(); - - assert(_elemHold1List.empty()); - assert(_elemHold2List.empty()); } void @@ -221,22 +221,10 @@ DataStoreBase::addType(BufferTypeBase *typeHandler) } void -DataStoreBase::transferElemHoldList(generation_t generation) -{ - ElemHold2List &elemHold2List = _elemHold2List; - for (const ElemHold1ListElem & elemHold1 : _elemHold1List) { - elemHold2List.push_back(ElemHold2ListElem(elemHold1, generation)); - } - _elemHold1List.clear(); -} - -void -DataStoreBase::transferHoldLists(generation_t generation) +DataStoreBase::assign_generation(generation_t current_gen) { - _genHolder.transferHoldLists(generation); - if (hasElemHold1()) { - transferElemHoldList(generation); - } + _genHolder.assign_generation(current_gen); + _entry_ref_hold_list.assign_generation(current_gen); } void @@ -248,18 +236,18 @@ DataStoreBase::doneHoldBuffer(uint32_t bufferId) } void -DataStoreBase::trimHoldLists(generation_t usedGen) +DataStoreBase::reclaim_memory(generation_t oldest_used_gen) { - trimElemHoldList(usedGen); // Trim entries before trimming buffers - _genHolder.trimHoldLists(usedGen); + reclaim_entry_refs(oldest_used_gen); // Trim entries before trimming buffers + _genHolder.reclaim(oldest_used_gen); } void -DataStoreBase::clearHoldLists() +DataStoreBase::reclaim_all_memory() { - transferElemHoldList(0); - clearElemHoldList(); - _genHolder.clearHoldLists(); + _entry_ref_hold_list.assign_generation(0); + reclaim_all_entry_refs(); + _genHolder.reclaim_all(); } void @@ -269,13 +257,13 @@ DataStoreBase::dropBuffers() for (uint32_t bufferId = 0; bufferId < numBuffers; ++bufferId) { _states[bufferId].dropBuffer(bufferId, _buffers[bufferId].get_atomic_buffer()); } - _genHolder.clearHoldLists(); + _genHolder.reclaim_all(); } vespalib::MemoryUsage DataStoreBase::getMemoryUsage() const { - MemStats stats = getMemStats(); + auto stats = getMemStats(); vespalib::MemoryUsage usage; usage.setAllocatedBytes(stats._allocBytes); usage.setUsedBytes(stats._usedBytes); @@ -289,18 +277,18 @@ DataStoreBase::holdBuffer(uint32_t bufferId) { _states[bufferId].onHold(bufferId); size_t holdBytes = 0u; // getMemStats() still accounts held buffers - GenerationHeldBase::UP hold(new BufferHold(holdBytes, *this, bufferId)); - _genHolder.hold(std::move(hold)); + auto hold = std::make_unique<BufferHold>(holdBytes, *this, bufferId); + _genHolder.insert(std::move(hold)); } void DataStoreBase::enableFreeLists() { - for (BufferState & bState : _states) { + for (auto& bState : _states) { if (!bState.isActive() || bState.getCompacting()) { continue; } - bState.free_list().enable(_free_lists[bState.getTypeId()]); + bState.enable_free_list(_free_lists[bState.getTypeId()]); } _freeListsEnabled = true; } @@ -308,8 +296,8 @@ DataStoreBase::enableFreeLists() void DataStoreBase::disableFreeLists() { - for (BufferState & bState : _states) { - bState.free_list().disable(); + for (auto& bState : _states) { + bState.disable_free_list(); } _freeListsEnabled = false; } @@ -321,17 +309,11 @@ DataStoreBase::enableFreeList(uint32_t bufferId) if (_freeListsEnabled && state.isActive() && !state.getCompacting()) { - state.free_list().enable(_free_lists[state.getTypeId()]); + state.enable_free_list(_free_lists[state.getTypeId()]); } } void -DataStoreBase::disableFreeList(uint32_t bufferId) -{ - _states[bufferId].free_list().disable(); -} - -void DataStoreBase::disableElemHoldList() { for (auto &state : _states) { @@ -341,47 +323,29 @@ DataStoreBase::disableElemHoldList() } } -namespace { - -void -add_buffer_state_to_mem_stats(const BufferState& state, size_t elementSize, DataStoreBase::MemStats& stats) -{ - size_t extra_used_bytes = state.getExtraUsedBytes(); - stats._allocElems += state.capacity(); - stats._usedElems += state.size(); - stats._deadElems += state.getDeadElems(); - stats._holdElems += state.getHoldElems(); - stats._allocBytes += (state.capacity() * elementSize) + extra_used_bytes; - stats._usedBytes += (state.size() * elementSize) + extra_used_bytes; - stats._deadBytes += state.getDeadElems() * elementSize; - stats._holdBytes += (state.getHoldElems() * elementSize) + state.getExtraHoldBytes(); -} - -} - -DataStoreBase::MemStats +MemoryStats DataStoreBase::getMemStats() const { - MemStats stats; + MemoryStats stats; - for (const BufferState & bState: _states) { + for (const auto& bState: _states) { auto typeHandler = bState.getTypeHandler(); - BufferState::State state = bState.getState(); + auto state = bState.getState(); if ((state == BufferState::State::FREE) || (typeHandler == nullptr)) { ++stats._freeBuffers; } else if (state == BufferState::State::ACTIVE) { size_t elementSize = typeHandler->elementSize(); ++stats._activeBuffers; - add_buffer_state_to_mem_stats(bState, elementSize, stats); + bState.stats().add_to_mem_stats(elementSize, stats); } else if (state == BufferState::State::HOLD) { size_t elementSize = typeHandler->elementSize(); ++stats._holdBuffers; - add_buffer_state_to_mem_stats(bState, elementSize, stats); + bState.stats().add_to_mem_stats(elementSize, stats); } else { LOG_ABORT("should not be reached"); } } - size_t genHolderHeldBytes = _genHolder.getHeldBytes(); + size_t genHolderHeldBytes = _genHolder.get_held_bytes(); stats._holdBytes += genHolderHeldBytes; stats._allocBytes += genHolderHeldBytes; stats._usedBytes += genHolderHeldBytes; @@ -394,11 +358,11 @@ DataStoreBase::getAddressSpaceUsage() const size_t usedArrays = 0; size_t deadArrays = 0; size_t limitArrays = 0; - for (const BufferState & bState: _states) { + for (const auto& bState: _states) { if (bState.isActive()) { uint32_t arraySize = bState.getArraySize(); usedArrays += bState.size() / arraySize; - deadArrays += bState.getDeadElems() / arraySize; + deadArrays += bState.stats().dead_elems() / arraySize; limitArrays += bState.capacity() / arraySize; } else if (bState.isOnHold()) { uint32_t arraySize = bState.getArraySize(); @@ -410,7 +374,7 @@ DataStoreBase::getAddressSpaceUsage() const LOG_ABORT("should not be reached"); } } - return vespalib::AddressSpace(usedArrays, deadArrays, limitArrays); + return {usedArrays, deadArrays, limitArrays}; } void @@ -427,26 +391,6 @@ DataStoreBase::onActive(uint32_t bufferId, uint32_t typeId, size_t elemsNeeded) enableFreeList(bufferId); } -std::vector<uint32_t> -DataStoreBase::startCompact(uint32_t typeId) -{ - std::vector<uint32_t> toHold; - - for (uint32_t bufferId = 0; bufferId < _numBuffers; ++bufferId) { - BufferState &state = getBufferState(bufferId); - if (state.isActive() && - state.getTypeId() == typeId && - !state.getCompacting()) { - state.setCompacting(); - toHold.push_back(bufferId); - disableFreeList(bufferId); - } - } - switch_primary_buffer(typeId, 0u); - inc_compaction_count(); - return toHold; -} - void DataStoreBase::finishCompact(const std::vector<uint32_t> &toHold) { @@ -467,52 +411,14 @@ DataStoreBase::fallbackResize(uint32_t bufferId, size_t elemsNeeded) state.fallbackResize(bufferId, elemsNeeded, _buffers[bufferId].get_atomic_buffer(), toHoldBuffer); - GenerationHeldBase::UP - hold(new FallbackHold(oldAllocElems * elementSize, - std::move(toHoldBuffer), - oldUsedElems, - state.getTypeHandler(), - state.getTypeId())); + auto hold = std::make_unique<FallbackHold>(oldAllocElems * elementSize, + std::move(toHoldBuffer), + oldUsedElems, + state.getTypeHandler(), + state.getTypeId()); if (!_initializing) { - _genHolder.hold(std::move(hold)); - } -} - -uint32_t -DataStoreBase::startCompactWorstBuffer(uint32_t typeId) -{ - uint32_t buffer_id = get_primary_buffer_id(typeId); - const BufferTypeBase *typeHandler = _typeHandlers[typeId]; - assert(typeHandler->get_active_buffers_count() >= 1u); - if (typeHandler->get_active_buffers_count() == 1u) { - // Single active buffer for type, no need for scan - markCompacting(buffer_id); - return buffer_id; - } - // Multiple active buffers for type, must perform full scan - return startCompactWorstBuffer(buffer_id, - [=](const BufferState &state) { return state.isActive(typeId); }); -} - -template <typename BufferStateActiveFilter> -uint32_t -DataStoreBase::startCompactWorstBuffer(uint32_t initWorstBufferId, BufferStateActiveFilter &&filterFunc) -{ - uint32_t worstBufferId = initWorstBufferId; - size_t worstDeadElems = 0; - for (uint32_t bufferId = 0; bufferId < _numBuffers; ++bufferId) { - const auto &state = getBufferState(bufferId); - if (filterFunc(state)) { - assert(!state.getCompacting()); - size_t deadElems = state.getDeadElems() - state.getTypeHandler()->getReservedElements(bufferId); - if (deadElems > worstDeadElems) { - worstBufferId = bufferId; - worstDeadElems = deadElems; - } - } + _genHolder.insert(std::move(hold)); } - markCompacting(worstBufferId); - return worstBufferId; } void @@ -527,7 +433,7 @@ DataStoreBase::markCompacting(uint32_t bufferId) assert(!state.getCompacting()); state.setCompacting(); state.disableElemHoldList(); - state.free_list().disable(); + state.disable_free_list(); inc_compaction_count(); } @@ -535,9 +441,15 @@ std::unique_ptr<CompactingBuffers> DataStoreBase::start_compact_worst_buffers(CompactionSpec compaction_spec, const CompactionStrategy& compaction_strategy) { // compact memory usage - CompactBufferCandidates elem_buffers(_numBuffers, compaction_strategy.get_max_buffers(), compaction_strategy.get_active_buffers_ratio(), compaction_strategy.getMaxDeadBytesRatio() / 2, CompactionStrategy::DEAD_BYTES_SLACK); + CompactBufferCandidates elem_buffers(_numBuffers, compaction_strategy.get_max_buffers(), + compaction_strategy.get_active_buffers_ratio(), + compaction_strategy.getMaxDeadBytesRatio() / 2, + CompactionStrategy::DEAD_BYTES_SLACK); // compact address space - CompactBufferCandidates array_buffers(_numBuffers, compaction_strategy.get_max_buffers(), compaction_strategy.get_active_buffers_ratio(), compaction_strategy.getMaxDeadAddressSpaceRatio() / 2, CompactionStrategy::DEAD_ADDRESS_SPACE_SLACK); + CompactBufferCandidates array_buffers(_numBuffers, compaction_strategy.get_max_buffers(), + compaction_strategy.get_active_buffers_ratio(), + compaction_strategy.getMaxDeadAddressSpaceRatio() / 2, + CompactionStrategy::DEAD_ADDRESS_SPACE_SLACK); uint32_t free_buffers = 0; for (uint32_t bufferId = 0; bufferId < _numBuffers; ++bufferId) { const auto &state = getBufferState(bufferId); @@ -546,7 +458,7 @@ DataStoreBase::start_compact_worst_buffers(CompactionSpec compaction_spec, const uint32_t arraySize = typeHandler->getArraySize(); uint32_t reservedElements = typeHandler->getReservedElements(bufferId); size_t used_elems = state.size(); - size_t deadElems = state.getDeadElems() - reservedElements; + size_t deadElems = state.stats().dead_elems() - reservedElements; if (compaction_spec.compact_memory()) { elem_buffers.add(bufferId, used_elems, deadElems); } diff --git a/vespalib/src/vespa/vespalib/datastore/datastorebase.h b/vespalib/src/vespa/vespalib/datastore/datastorebase.h index d83b3a84847..598f0872253 100644 --- a/vespalib/src/vespa/vespalib/datastore/datastorebase.h +++ b/vespalib/src/vespa/vespalib/datastore/datastorebase.h @@ -4,12 +4,14 @@ #include "bufferstate.h" #include "free_list.h" +#include "memory_stats.h" #include <vespa/vespalib/util/address_space.h> #include <vespa/vespalib/util/generationholder.h> +#include <vespa/vespalib/util/generation_hold_list.h> #include <vespa/vespalib/util/memoryusage.h> -#include <vector> -#include <deque> #include <atomic> +#include <deque> +#include <vector> namespace vespalib::datastore { @@ -24,25 +26,19 @@ class CompactionStrategy; */ class DataStoreBase { -public: - /** - * Hold list before freeze, before knowing how long elements must be held. - */ - class ElemHold1ListElem - { - public: - EntryRef _ref; - size_t _len; // Aligned length - - ElemHold1ListElem(EntryRef ref, size_t len) - : _ref(ref), - _len(len) - { } +protected: + struct EntryRefHoldElem { + EntryRef ref; + size_t num_elems; + + EntryRefHoldElem(EntryRef ref_in, size_t num_elems_in) + : ref(ref_in), + num_elems(num_elems_in) + {} }; -protected: + using EntryRefHoldList = GenerationHoldList<EntryRefHoldElem, false, true>; using generation_t = vespalib::GenerationHandler::generation_t; - using sgeneration_t = vespalib::GenerationHandler::sgeneration_t; private: class BufferAndTypeId { @@ -59,32 +55,16 @@ private: uint32_t _typeId; }; std::vector<BufferAndTypeId> _buffers; // For fast mapping with known types -protected: + // Provides a mapping from typeId -> primary buffer for that type. // The primary buffer is used for allocations of new element(s) if no available slots are found in free lists. std::vector<uint32_t> _primary_buffer_ids; +protected: void* getBuffer(uint32_t bufferId) { return _buffers[bufferId].get_buffer_relaxed(); } /** - * Hold list at freeze, when knowing how long elements must be held - */ - class ElemHold2ListElem : public ElemHold1ListElem - { - public: - generation_t _generation; - - ElemHold2ListElem(const ElemHold1ListElem &hold1, generation_t generation) - : ElemHold1ListElem(hold1), - _generation(generation) - { } - }; - - using ElemHold1List = vespalib::Array<ElemHold1ListElem>; - using ElemHold2List = std::deque<ElemHold2ListElem>; - - /** - * Class used to hold the old buffer as part of fallbackResize(). + * Class used to hold the entire old buffer as part of fallbackResize(). */ class FallbackHold : public vespalib::GenerationHeldBase { @@ -102,52 +82,6 @@ protected: class BufferHold; -public: - class MemStats - { - public: - size_t _allocElems; - size_t _usedElems; - size_t _deadElems; - size_t _holdElems; - size_t _allocBytes; - size_t _usedBytes; - size_t _deadBytes; - size_t _holdBytes; - uint32_t _freeBuffers; - uint32_t _activeBuffers; - uint32_t _holdBuffers; - - MemStats() - : _allocElems(0), - _usedElems(0), - _deadElems(0), - _holdElems(0), - _allocBytes(0), - _usedBytes(0), - _deadBytes(0), - _holdBytes(0), - _freeBuffers(0), - _activeBuffers(0), - _holdBuffers(0) - { } - - MemStats& operator+=(const MemStats &rhs) { - _allocElems += rhs._allocElems; - _usedElems += rhs._usedElems; - _deadElems += rhs._deadElems; - _holdElems += rhs._holdElems; - _allocBytes += rhs._allocBytes; - _usedBytes += rhs._usedBytes; - _deadBytes += rhs._deadBytes; - _holdBytes += rhs._holdBytes; - _freeBuffers += rhs._freeBuffers; - _activeBuffers += rhs._activeBuffers; - _holdBuffers += rhs._holdBuffers; - return *this; - } - }; - private: std::vector<BufferState> _states; protected: @@ -156,10 +90,7 @@ protected: std::vector<FreeList> _free_lists; bool _freeListsEnabled; bool _initializing; - - ElemHold1List _elemHold1List; - ElemHold2List _elemHold2List; - + EntryRefHoldList _entry_ref_hold_list; const uint32_t _numBuffers; const uint32_t _offset_bits; uint32_t _hold_buffer_count; @@ -174,6 +105,7 @@ protected: virtual ~DataStoreBase(); +private: /** * Get the next buffer id after the given buffer id. */ @@ -183,6 +115,7 @@ protected: ret = 0; return ret; } +protected: /** * Get the primary buffer for the given type id. @@ -194,15 +127,14 @@ protected: /** * Trim elem hold list, freeing elements that no longer needs to be held. * - * @param usedGen lowest generation that is still used. + * @param oldest_used_gen the oldest generation that is still used. */ - virtual void trimElemHoldList(generation_t usedGen) = 0; + virtual void reclaim_entry_refs(generation_t oldest_used_gen) = 0; - virtual void clearElemHoldList() = 0; + virtual void reclaim_all_entry_refs() = 0; - template <typename BufferStateActiveFilter> - uint32_t startCompactWorstBuffer(uint32_t initWorstBufferId, BufferStateActiveFilter &&filterFunc); void markCompacting(uint32_t bufferId); + public: uint32_t addType(BufferTypeBase *typeHandler); void init_primary_buffers(); @@ -238,9 +170,11 @@ public: */ void switch_primary_buffer(uint32_t typeId, size_t elemsNeeded); +private: bool consider_grow_active_buffer(uint32_t type_id, size_t elems_needed); void switch_or_grow_primary_buffer(uint32_t typeId, size_t elemsNeeded); +public: vespalib::MemoryUsage getMemoryUsage() const; vespalib::AddressSpace getAddressSpaceUsage() const; @@ -252,31 +186,28 @@ public: const BufferState &getBufferState(uint32_t bufferId) const { return _states[bufferId]; } BufferState &getBufferState(uint32_t bufferId) { return _states[bufferId]; } uint32_t getNumBuffers() const { return _numBuffers; } - bool hasElemHold1() const { return !_elemHold1List.empty(); } - - /** - * Transfer element holds from hold1 list to hold2 list. - */ - void transferElemHoldList(generation_t generation); +public: /** - * Transfer holds from hold1 to hold2 lists, assigning generation. + * Assign generation on data elements on hold lists added since the last time this function was called. */ - void transferHoldLists(generation_t generation); + void assign_generation(generation_t current_gen); +private: /** * Hold of buffer has ended. */ void doneHoldBuffer(uint32_t bufferId); +public: /** - * Trim hold lists, freeing buffers that no longer needs to be held. + * Reclaim memory from hold lists, freeing buffers and entry refs that no longer needs to be held. * - * @param usedGen lowest generation that is still used. + * @param oldest_used_gen oldest generation that is still used. */ - void trimHoldLists(generation_t usedGen); + void reclaim_memory(generation_t oldest_used_gen); - void clearHoldLists(); + void reclaim_all_memory(); template <typename EntryType, typename RefType> EntryType *getEntry(RefType ref) { @@ -300,12 +231,6 @@ public: void dropBuffers(); - - void incDead(uint32_t bufferId, size_t deadElems) { - BufferState &state = _states[bufferId]; - state.incDeadElems(deadElems); - } - /** * Enable free list management. * This only works for fixed size elements. @@ -317,16 +242,14 @@ public: */ void disableFreeLists(); +private: /** * Enable free list management. * This only works for fixed size elements. */ void enableFreeList(uint32_t bufferId); - /** - * Disable free list management. - */ - void disableFreeList(uint32_t bufferId); +public: void disableElemHoldList(); bool has_free_lists_enabled() const { return _freeListsEnabled; } @@ -341,7 +264,7 @@ public: /** * Returns aggregated memory statistics for all buffers in this data store. */ - MemStats getMemStats() const; + MemoryStats getMemStats() const; /** * Assume that no readers are present while data structure is being initialized. @@ -359,16 +282,18 @@ private: void onActive(uint32_t bufferId, uint32_t typeId, size_t elemsNeeded); void inc_hold_buffer_count(); + public: uint32_t getTypeId(uint32_t bufferId) const { return _buffers[bufferId].getTypeId(); } - std::vector<uint32_t> startCompact(uint32_t typeId); - void finishCompact(const std::vector<uint32_t> &toHold); + +private: void fallbackResize(uint32_t bufferId, size_t elementsNeeded); +public: vespalib::GenerationHolder &getGenerationHolder() { return _genHolder; } @@ -378,7 +303,6 @@ public: return self._genHolder; } - uint32_t startCompactWorstBuffer(uint32_t typeId); std::unique_ptr<CompactingBuffers> start_compact_worst_buffers(CompactionSpec compaction_spec, const CompactionStrategy &compaction_strategy); uint64_t get_compaction_count() const { return _compaction_count.load(std::memory_order_relaxed); } void inc_compaction_count() const { ++_compaction_count; } @@ -386,3 +310,7 @@ public: }; } + +namespace vespalib { +extern template class GenerationHoldList<datastore::DataStoreBase::EntryRefHoldElem, false, true>; +} diff --git a/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.cpp b/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.cpp index 6f001ce3c94..47c64722785 100644 --- a/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.cpp +++ b/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.cpp @@ -5,10 +5,15 @@ #include "entry_ref_filter.h" #include "i_compactable.h" #include <vespa/vespalib/util/array.hpp> +#include <vespa/vespalib/util/generation_hold_list.hpp> #include <vespa/vespalib/util/memoryusage.h> #include <cassert> #include <stdexcept> +namespace vespalib { +template class GenerationHoldList<uint32_t, false, true>; +} + namespace vespalib::datastore { FixedSizeHashMap::Node::Node(Node&&) @@ -30,8 +35,7 @@ FixedSizeHashMap::FixedSizeHashMap(uint32_t modulo, uint32_t capacity, uint32_t _free_head(no_node_idx), _free_count(0u), _hold_count(0u), - _hold_1_list(), - _hold_2_list(), + _hold_list(), _num_shards(num_shards) { _nodes.reserve(capacity); @@ -49,7 +53,10 @@ FixedSizeHashMap::FixedSizeHashMap(uint32_t modulo, uint32_t capacity, uint32_t } } -FixedSizeHashMap::~FixedSizeHashMap() = default; +FixedSizeHashMap::~FixedSizeHashMap() +{ + _hold_list.reclaim_all(); +} void FixedSizeHashMap::force_add(const EntryComparator& comp, const KvType& kv) @@ -96,37 +103,6 @@ FixedSizeHashMap::add(const ShardedHashComparator & comp, std::function<EntryRef return _nodes[node_idx].get_kv(); } -void -FixedSizeHashMap::transfer_hold_lists_slow(generation_t generation) -{ - auto &hold_2_list = _hold_2_list; - for (uint32_t node_idx : _hold_1_list) { - hold_2_list.push_back(std::make_pair(generation, node_idx)); - } - _hold_1_list.clear(); - -} - - -void -FixedSizeHashMap::trim_hold_lists_slow(generation_t first_used) -{ - while (!_hold_2_list.empty()) { - auto& first = _hold_2_list.front(); - if (static_cast<sgeneration_t>(first.first - first_used) >= 0) { - break; - } - uint32_t node_idx = first.second; - auto& node = _nodes[node_idx]; - node.get_next_node_idx().store(_free_head, std::memory_order_relaxed); - _free_head = node_idx; - ++_free_count; - --_hold_count; - node.on_free(); - _hold_2_list.erase(_hold_2_list.begin()); - } -} - FixedSizeHashMap::KvType* FixedSizeHashMap::remove(const ShardedHashComparator & comp) { @@ -145,7 +121,7 @@ FixedSizeHashMap::remove(const ShardedHashComparator & comp) } --_count; ++_hold_count; - _hold_1_list.push_back(node_idx); + _hold_list.insert(node_idx); return &_nodes[node_idx].get_kv(); } prev_node_idx = node_idx; @@ -154,6 +130,19 @@ FixedSizeHashMap::remove(const ShardedHashComparator & comp) return nullptr; } +void +FixedSizeHashMap::reclaim_memory(generation_t oldest_used_gen) +{ + _hold_list.reclaim(oldest_used_gen, [this](uint32_t node_idx) { + auto& node = _nodes[node_idx]; + node.get_next_node_idx().store(_free_head, std::memory_order_relaxed); + _free_head = node_idx; + ++_free_count; + --_hold_count; + node.on_free(); + }); +} + MemoryUsage FixedSizeHashMap::get_memory_usage() const { @@ -183,7 +172,7 @@ FixedSizeHashMap::foreach_key(const std::function<void(EntryRef)>& callback) con } void -FixedSizeHashMap::move_keys(ICompactable& compactable, const EntryRefFilter &compacting_buffers) +FixedSizeHashMap::move_keys_on_compact(ICompactable& compactable, const EntryRefFilter &compacting_buffers) { for (auto& chain_head : _chain_heads) { uint32_t node_idx = chain_head.load_relaxed(); @@ -192,7 +181,7 @@ FixedSizeHashMap::move_keys(ICompactable& compactable, const EntryRefFilter &com EntryRef old_ref = node.get_kv().first.load_relaxed(); assert(old_ref.valid()); if (compacting_buffers.has(old_ref)) { - EntryRef new_ref = compactable.move(old_ref); + EntryRef new_ref = compactable.move_on_compact(old_ref); node.get_kv().first.store_release(new_ref); } node_idx = node.get_next_node_idx().load(std::memory_order_relaxed); diff --git a/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.h b/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.h index c522bcc3c33..1e13f206adb 100644 --- a/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.h +++ b/vespalib/src/vespa/vespalib/datastore/fixed_size_hash_map.h @@ -6,11 +6,12 @@ #include "entry_comparator.h" #include <vespa/vespalib/util/array.h> #include <vespa/vespalib/util/arrayref.h> +#include <vespa/vespalib/util/generation_hold_list.h> #include <vespa/vespalib/util/generationhandler.h> -#include <limits> #include <atomic> #include <deque> #include <functional> +#include <limits> namespace vespalib { class GenerationHolder; @@ -56,8 +57,8 @@ private: * A reader must own an appropriate GenerationHandler::Guard to ensure * that memory is held while it can be accessed by reader. * - * The writer must update generation and call transfer_hold_lists and - * trim_hold_lists as needed to free up memory no longer needed by any + * The writer must update generation and call assign_generation and + * reclaim_memory as needed to free up memory no longer needed by any * readers. */ class FixedSizeHashMap { @@ -65,7 +66,6 @@ public: static constexpr uint32_t no_node_idx = std::numeric_limits<uint32_t>::max(); using KvType = std::pair<AtomicEntryRef, AtomicEntryRef>; using generation_t = GenerationHandler::generation_t; - using sgeneration_t = GenerationHandler::sgeneration_t; private: class ChainHead { std::atomic<uint32_t> _node_idx; @@ -103,6 +103,8 @@ private: const KvType& get_kv() const noexcept { return _kv; } }; + using NodeIdxHoldList = GenerationHoldList<uint32_t, false, true>; + Array<ChainHead> _chain_heads; Array<Node> _nodes; uint32_t _modulo; @@ -110,12 +112,9 @@ private: uint32_t _free_head; uint32_t _free_count; uint32_t _hold_count; - Array<uint32_t> _hold_1_list; - std::deque<std::pair<generation_t, uint32_t>> _hold_2_list; + NodeIdxHoldList _hold_list; uint32_t _num_shards; - void transfer_hold_lists_slow(generation_t generation); - void trim_hold_lists_slow(generation_t first_used); void force_add(const EntryComparator& comp, const KvType& kv); public: FixedSizeHashMap(uint32_t module, uint32_t capacity, uint32_t num_shards); @@ -143,23 +142,17 @@ public: return nullptr; } - void transfer_hold_lists(generation_t generation) { - if (!_hold_1_list.empty()) { - transfer_hold_lists_slow(generation); - } + void assign_generation(generation_t current_gen) { + _hold_list.assign_generation(current_gen); } - void trim_hold_lists(generation_t first_used) { - if (!_hold_2_list.empty() && static_cast<sgeneration_t>(_hold_2_list.front().first - first_used) < 0) { - trim_hold_lists_slow(first_used); - } - } + void reclaim_memory(generation_t oldest_used_gen); bool full() const noexcept { return _nodes.size() == _nodes.capacity() && _free_count == 0u; } size_t size() const noexcept { return _count; } MemoryUsage get_memory_usage() const; void foreach_key(const std::function<void(EntryRef)>& callback) const; - void move_keys(ICompactable& compactable, const EntryRefFilter &compacting_buffers); + void move_keys_on_compact(ICompactable& compactable, const EntryRefFilter &compacting_buffers); /* * Scan dictionary and call normalize function for each value. If * returned value is different then write back the modified value to @@ -182,3 +175,7 @@ public: }; } + +namespace vespalib { +extern template class GenerationHoldList<uint32_t, false, true>; +} diff --git a/vespalib/src/vespa/vespalib/datastore/free_list.cpp b/vespalib/src/vespa/vespalib/datastore/free_list.cpp index 6c96e51241c..44e49b68df9 100644 --- a/vespalib/src/vespa/vespalib/datastore/free_list.cpp +++ b/vespalib/src/vespa/vespalib/datastore/free_list.cpp @@ -1,6 +1,7 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "free_list.h" +#include <algorithm> #include <cassert> namespace vespalib::datastore { diff --git a/vespalib/src/vespa/vespalib/datastore/i_compactable.h b/vespalib/src/vespa/vespalib/datastore/i_compactable.h index 069d32bb481..31c082e4371 100644 --- a/vespalib/src/vespa/vespalib/datastore/i_compactable.h +++ b/vespalib/src/vespa/vespalib/datastore/i_compactable.h @@ -8,12 +8,13 @@ namespace vespalib::datastore { * Interface for moving an entry as part of compaction of data in old * buffers into new buffers. * - * Old entry is unchanged and not placed on any hold lists since we - * expect the old buffers to be freed soon anyway. + * A copy of the old entry is created and a reference to the new copy is + * returned. The old entry is unchanged and not placed on any hold + * lists since we expect the old buffers to be freed soon anyway. */ struct ICompactable { virtual ~ICompactable() = default; - virtual EntryRef move(EntryRef ref) = 0; + virtual EntryRef move_on_compact(EntryRef ref) = 0; }; } diff --git a/vespalib/src/vespa/vespalib/datastore/i_unique_store_dictionary.h b/vespalib/src/vespa/vespalib/datastore/i_unique_store_dictionary.h index bb105d41519..5a75a30d182 100644 --- a/vespalib/src/vespa/vespalib/datastore/i_unique_store_dictionary.h +++ b/vespalib/src/vespa/vespalib/datastore/i_unique_store_dictionary.h @@ -25,12 +25,12 @@ public: using generation_t = vespalib::GenerationHandler::generation_t; virtual ~IUniqueStoreDictionary() = default; virtual void freeze() = 0; - virtual void transfer_hold_lists(generation_t generation) = 0; - virtual void trim_hold_lists(generation_t firstUsed) = 0; + virtual void assign_generation(generation_t current_gen) = 0; + virtual void reclaim_memory(generation_t oldest_used_gen) = 0; virtual UniqueStoreAddResult add(const EntryComparator& comp, std::function<EntryRef(void)> insertEntry) = 0; virtual EntryRef find(const EntryComparator& comp) = 0; virtual void remove(const EntryComparator& comp, EntryRef ref) = 0; - virtual void move_keys(ICompactable& compactable, const EntryRefFilter& compacting_buffers) = 0; + virtual void move_keys_on_compact(ICompactable& compactable, const EntryRefFilter& compacting_buffers) = 0; virtual uint32_t get_num_uniques() const = 0; virtual vespalib::MemoryUsage get_memory_usage() const = 0; virtual void build(vespalib::ConstArrayRef<EntryRef>, vespalib::ConstArrayRef<uint32_t> ref_counts, std::function<void(EntryRef)> hold) = 0; diff --git a/vespalib/src/vespa/vespalib/datastore/memory_stats.cpp b/vespalib/src/vespa/vespalib/datastore/memory_stats.cpp new file mode 100644 index 00000000000..8e060b4cfb4 --- /dev/null +++ b/vespalib/src/vespa/vespalib/datastore/memory_stats.cpp @@ -0,0 +1,40 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "memory_stats.h" + +namespace vespalib::datastore { + +MemoryStats::MemoryStats() + : _allocElems(0), + _usedElems(0), + _deadElems(0), + _holdElems(0), + _allocBytes(0), + _usedBytes(0), + _deadBytes(0), + _holdBytes(0), + _freeBuffers(0), + _activeBuffers(0), + _holdBuffers(0) +{ +} + +MemoryStats& +MemoryStats::operator+=(const MemoryStats& rhs) +{ + _allocElems += rhs._allocElems; + _usedElems += rhs._usedElems; + _deadElems += rhs._deadElems; + _holdElems += rhs._holdElems; + _allocBytes += rhs._allocBytes; + _usedBytes += rhs._usedBytes; + _deadBytes += rhs._deadBytes; + _holdBytes += rhs._holdBytes; + _freeBuffers += rhs._freeBuffers; + _activeBuffers += rhs._activeBuffers; + _holdBuffers += rhs._holdBuffers; + return *this; +} + +} + diff --git a/vespalib/src/vespa/vespalib/datastore/memory_stats.h b/vespalib/src/vespa/vespalib/datastore/memory_stats.h new file mode 100644 index 00000000000..18d7dd77559 --- /dev/null +++ b/vespalib/src/vespa/vespalib/datastore/memory_stats.h @@ -0,0 +1,32 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <cstddef> +#include <cstdint> + +namespace vespalib::datastore { + +/** + * Represents aggregated memory statistics for all buffers in a data store. + */ +class MemoryStats +{ +public: + size_t _allocElems; + size_t _usedElems; + size_t _deadElems; + size_t _holdElems; + size_t _allocBytes; + size_t _usedBytes; + size_t _deadBytes; + size_t _holdBytes; + uint32_t _freeBuffers; + uint32_t _activeBuffers; + uint32_t _holdBuffers; + + MemoryStats(); + MemoryStats& operator+=(const MemoryStats& rhs); +}; + +} diff --git a/vespalib/src/vespa/vespalib/datastore/raw_allocator.hpp b/vespalib/src/vespa/vespalib/datastore/raw_allocator.hpp index 0d67bf71c20..7395ef68a73 100644 --- a/vespalib/src/vespa/vespalib/datastore/raw_allocator.hpp +++ b/vespalib/src/vespa/vespalib/datastore/raw_allocator.hpp @@ -28,7 +28,7 @@ RawAllocator<EntryT, RefT>::alloc(size_t numElems, size_t extraElems) assert((numElems % arraySize) == 0u); RefT ref((oldBufferSize / arraySize), buffer_id); EntryT *buffer = _store.getEntryArray<EntryT>(ref, arraySize); - state.pushed_back(numElems); + state.stats().pushed_back(numElems); return HandleType(ref, buffer); } diff --git a/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.cpp b/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.cpp index 2ae22084472..a28c3071646 100644 --- a/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.cpp +++ b/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.cpp @@ -32,7 +32,7 @@ ShardedHashMap::ShardedHashMap(std::unique_ptr<const EntryComparator> comp) ShardedHashMap::~ShardedHashMap() { - _gen_holder.clearHoldLists(); + _gen_holder.reclaim_all(); for (size_t i = 0; i < num_shards; ++i) { auto map = _maps[i].load(std::memory_order_relaxed); delete map; @@ -58,7 +58,7 @@ ShardedHashMap::hold_shard(std::unique_ptr<const FixedSizeHashMap> map) { auto usage = map->get_memory_usage(); auto hold = std::make_unique<ShardedHashMapShardHeld>(usage.allocatedBytes(), std::move(map)); - _gen_holder.hold(std::move(hold)); + _gen_holder.insert(std::move(hold)); } ShardedHashMap::KvType& @@ -107,27 +107,27 @@ ShardedHashMap::find(const EntryComparator& comp, EntryRef key_ref) const } void -ShardedHashMap::transfer_hold_lists(generation_t generation) +ShardedHashMap::assign_generation(generation_t current_gen) { for (size_t i = 0; i < num_shards; ++i) { auto map = _maps[i].load(std::memory_order_relaxed); if (map != nullptr) { - map->transfer_hold_lists(generation); + map->assign_generation(current_gen); } } - _gen_holder.transferHoldLists(generation); + _gen_holder.assign_generation(current_gen); } void -ShardedHashMap::trim_hold_lists(generation_t first_used) +ShardedHashMap::reclaim_memory(generation_t oldest_used_gen) { for (size_t i = 0; i < num_shards; ++i) { auto map = _maps[i].load(std::memory_order_relaxed); if (map != nullptr) { - map->trim_hold_lists(first_used); + map->reclaim_memory(oldest_used_gen); } } - _gen_holder.trimHoldLists(first_used); + _gen_holder.reclaim(oldest_used_gen); } size_t @@ -153,7 +153,7 @@ ShardedHashMap::get_memory_usage() const memory_usage.merge(map->get_memory_usage()); } } - size_t gen_holder_held_bytes = _gen_holder.getHeldBytes(); + size_t gen_holder_held_bytes = _gen_holder.get_held_bytes(); memory_usage.incAllocatedBytes(gen_holder_held_bytes); memory_usage.incAllocatedBytesOnHold(gen_holder_held_bytes); return memory_usage; @@ -171,12 +171,12 @@ ShardedHashMap::foreach_key(std::function<void(EntryRef)> callback) const } void -ShardedHashMap::move_keys(ICompactable& compactable, const EntryRefFilter& compacting_buffers) +ShardedHashMap::move_keys_on_compact(ICompactable& compactable, const EntryRefFilter& compacting_buffers) { for (size_t i = 0; i < num_shards; ++i) { auto map = _maps[i].load(std::memory_order_relaxed); if (map != nullptr) { - map->move_keys(compactable, compacting_buffers); + map->move_keys_on_compact(compactable, compacting_buffers); } } } @@ -222,7 +222,7 @@ ShardedHashMap::foreach_value(std::function<void(const std::vector<EntryRef>&)> bool ShardedHashMap::has_held_buffers() const { - return _gen_holder.getHeldBytes() != 0; + return _gen_holder.get_held_bytes() != 0; } void diff --git a/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.h b/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.h index e0ba9488351..572a8790828 100644 --- a/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.h +++ b/vespalib/src/vespa/vespalib/datastore/sharded_hash_map.h @@ -28,8 +28,8 @@ struct ICompactable; * A reader must own an appropriate GenerationHandler::Guard to ensure * that memory is held while it can be accessed by reader. * - * The writer must update generation and call transfer_hold_lists and - * trim_hold_lists as needed to free up memory no longer needed by any + * The writer must update generation and call assign_generation and + * reclaim_memory as needed to free up memory no longer needed by any * readers. */ class ShardedHashMap { @@ -52,13 +52,13 @@ public: KvType* remove(const EntryComparator& comp, EntryRef key_ref); KvType* find(const EntryComparator& comp, EntryRef key_ref); const KvType* find(const EntryComparator& comp, EntryRef key_ref) const; - void transfer_hold_lists(generation_t generation); - void trim_hold_lists(generation_t first_used); + void assign_generation(generation_t current_gen); + void reclaim_memory(generation_t oldest_used_gen); size_t size() const noexcept; const EntryComparator &get_default_comparator() const noexcept { return *_comp; } MemoryUsage get_memory_usage() const; void foreach_key(std::function<void(EntryRef)> callback) const; - void move_keys(ICompactable& compactable, const EntryRefFilter& compacting_buffers); + void move_keys_on_compact(ICompactable& compactable, const EntryRefFilter& compacting_buffers); bool normalize_values(std::function<EntryRef(EntryRef)> normalize); bool normalize_values(std::function<void(std::vector<EntryRef>&)> normalize, const EntryRefFilter& filter); void foreach_value(std::function<void(const std::vector<EntryRef>&)> callback, const EntryRefFilter& filter); diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store.h b/vespalib/src/vespa/vespalib/datastore/unique_store.h index e7c374985a7..1313d57fbab 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store.h +++ b/vespalib/src/vespa/vespalib/datastore/unique_store.h @@ -70,8 +70,8 @@ public: inline const DataStoreType& get_data_store() const noexcept { return _allocator.get_data_store(); } // Pass on hold list management to underlying store - void transferHoldLists(generation_t generation); - void trimHoldLists(generation_t firstUsed); + void assign_generation(generation_t current_gen); + void reclaim_memory(generation_t oldest_used_gen); vespalib::GenerationHolder &getGenerationHolder() { return _store.getGenerationHolder(); } void setInitializing(bool initializing) { _store.setInitializing(initializing); } void freeze(); diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store.hpp b/vespalib/src/vespa/vespalib/datastore/unique_store.hpp index 37a56bf2561..b8493017020 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store.hpp +++ b/vespalib/src/vespa/vespalib/datastore/unique_store.hpp @@ -109,20 +109,20 @@ private: } } - EntryRef move(EntryRef oldRef) override { + EntryRef move_on_compact(EntryRef oldRef) override { RefT iRef(oldRef); uint32_t buffer_id = iRef.bufferId(); auto &inner_mapping = _mapping[buffer_id]; assert(iRef.offset() < inner_mapping.size()); EntryRef &mappedRef = inner_mapping[iRef.offset()]; assert(!mappedRef.valid()); - EntryRef newRef = _store.move(oldRef); + EntryRef newRef = _store.move_on_compact(oldRef); mappedRef = newRef; return newRef; } void fillMapping() { - _dict.move_keys(*this, _filter); + _dict.move_keys_on_compact(*this, _filter); } public: @@ -190,18 +190,18 @@ UniqueStore<EntryT, RefT, Compare, Allocator>::bufferState(EntryRef ref) const template <typename EntryT, typename RefT, typename Compare, typename Allocator> void -UniqueStore<EntryT, RefT, Compare, Allocator>::transferHoldLists(generation_t generation) +UniqueStore<EntryT, RefT, Compare, Allocator>::assign_generation(generation_t current_gen) { - _dict->transfer_hold_lists(generation); - _store.transferHoldLists(generation); + _dict->assign_generation(current_gen); + _store.assign_generation(current_gen); } template <typename EntryT, typename RefT, typename Compare, typename Allocator> void -UniqueStore<EntryT, RefT, Compare, Allocator>::trimHoldLists(generation_t firstUsed) +UniqueStore<EntryT, RefT, Compare, Allocator>::reclaim_memory(generation_t oldest_used_gen) { - _dict->trim_hold_lists(firstUsed); - _store.trimHoldLists(firstUsed); + _dict->reclaim_memory(oldest_used_gen); + _store.reclaim_memory(oldest_used_gen); } template <typename EntryT, typename RefT, typename Compare, typename Allocator> diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.h b/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.h index 04df88ab4b9..0f6d9ddfc9b 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.h +++ b/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.h @@ -35,7 +35,7 @@ public: ~UniqueStoreAllocator() override; EntryRef allocate(const EntryType& value); void hold(EntryRef ref); - EntryRef move(EntryRef ref) override; + EntryRef move_on_compact(EntryRef ref) override; const WrappedEntryType& get_wrapped(EntryRef ref) const { RefType iRef(ref); return *_store.template getEntry<WrappedEntryType>(iRef); diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.hpp b/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.hpp index 04a229d4ffa..8ad11b18218 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.hpp +++ b/vespalib/src/vespa/vespalib/datastore/unique_store_allocator.hpp @@ -28,7 +28,7 @@ UniqueStoreAllocator<EntryT, RefT>::UniqueStoreAllocator(std::shared_ptr<alloc:: template <typename EntryT, typename RefT> UniqueStoreAllocator<EntryT, RefT>::~UniqueStoreAllocator() { - _store.clearHoldLists(); + _store.reclaim_all_memory(); _store.dropBuffers(); } @@ -48,7 +48,7 @@ UniqueStoreAllocator<EntryT, RefT>::hold(EntryRef ref) template <typename EntryT, typename RefT> EntryRef -UniqueStoreAllocator<EntryT, RefT>::move(EntryRef ref) +UniqueStoreAllocator<EntryT, RefT>::move_on_compact(EntryRef ref) { return _store.template allocator<WrappedEntryType>(0).alloc(get_wrapped(ref)).ref; } diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.h b/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.h index 702bae38e7c..8c5f284bb14 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.h +++ b/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.h @@ -74,12 +74,12 @@ public: UniqueStoreDictionary(std::unique_ptr<EntryComparator> compare); ~UniqueStoreDictionary() override; void freeze() override; - void transfer_hold_lists(generation_t generation) override; - void trim_hold_lists(generation_t firstUsed) override; + void assign_generation(generation_t current_gen) override; + void reclaim_memory(generation_t oldest_used_gen) override; UniqueStoreAddResult add(const EntryComparator& comp, std::function<EntryRef(void)> insertEntry) override; EntryRef find(const EntryComparator& comp) override; void remove(const EntryComparator& comp, EntryRef ref) override; - void move_keys(ICompactable& compactable, const EntryRefFilter& compacting_buffers) override; + void move_keys_on_compact(ICompactable& compactable, const EntryRefFilter& compacting_buffers) override; uint32_t get_num_uniques() const override; vespalib::MemoryUsage get_memory_usage() const override; void build(vespalib::ConstArrayRef<EntryRef>, vespalib::ConstArrayRef<uint32_t> ref_counts, std::function<void(EntryRef)> hold) override; diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.hpp b/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.hpp index 8029b66309d..6708b4c1448 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.hpp +++ b/vespalib/src/vespa/vespalib/datastore/unique_store_dictionary.hpp @@ -41,25 +41,25 @@ UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::freeze() template <typename BTreeDictionaryT, typename ParentT, typename HashDictionaryT> void -UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::transfer_hold_lists(generation_t generation) +UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::assign_generation(generation_t current_gen) { if constexpr (has_btree_dictionary) { - this->_btree_dict.getAllocator().transferHoldLists(generation); + this->_btree_dict.getAllocator().assign_generation(current_gen); } if constexpr (has_hash_dictionary) { - this->_hash_dict.transfer_hold_lists(generation); + this->_hash_dict.assign_generation(current_gen); } } template <typename BTreeDictionaryT, typename ParentT, typename HashDictionaryT> void -UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::trim_hold_lists(generation_t firstUsed) +UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::reclaim_memory(generation_t oldest_used_gen) { if constexpr (has_btree_dictionary) { - this->_btree_dict.getAllocator().trimHoldLists(firstUsed); + this->_btree_dict.getAllocator().reclaim_memory(oldest_used_gen); } if constexpr (has_hash_dictionary) { - this->_hash_dict.trim_hold_lists(firstUsed); + this->_hash_dict.reclaim_memory(oldest_used_gen); } } @@ -140,7 +140,7 @@ UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::remove(const template <typename BTreeDictionaryT, typename ParentT, typename HashDictionaryT> void -UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::move_keys(ICompactable &compactable, const EntryRefFilter& compacting_buffers) +UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::move_keys_on_compact(ICompactable &compactable, const EntryRefFilter& compacting_buffers) { if constexpr (has_btree_dictionary) { auto itr = this->_btree_dict.begin(); @@ -148,7 +148,7 @@ UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::move_keys(ICo EntryRef oldRef(itr.getKey().load_relaxed()); assert(oldRef.valid()); if (compacting_buffers.has(oldRef)) { - EntryRef newRef(compactable.move(oldRef)); + EntryRef newRef(compactable.move_on_compact(oldRef)); this->_btree_dict.thaw(itr); itr.writeKey(AtomicEntryRef(newRef)); if constexpr (has_hash_dictionary) { @@ -160,7 +160,7 @@ UniqueStoreDictionary<BTreeDictionaryT, ParentT, HashDictionaryT>::move_keys(ICo ++itr; } } else { - this->_hash_dict.move_keys(compactable, compacting_buffers); + this->_hash_dict.move_keys_on_compact(compactable, compacting_buffers); } } diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.h b/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.h index be5fa8f6c1e..8977fd1cce8 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.h +++ b/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.h @@ -111,7 +111,7 @@ public: ~UniqueStoreStringAllocator() override; EntryRef allocate(const char *value); void hold(EntryRef ref); - EntryRef move(EntryRef ref) override; + EntryRef move_on_compact(EntryRef ref) override; const UniqueStoreEntryBase& get_wrapped(EntryRef ref) const { RefType iRef(ref); auto &state = _store.getBufferState(iRef.bufferId()); diff --git a/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.hpp b/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.hpp index 71ea16bcde2..65cab4850ba 100644 --- a/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.hpp +++ b/vespalib/src/vespa/vespalib/datastore/unique_store_string_allocator.hpp @@ -30,7 +30,7 @@ UniqueStoreStringAllocator<RefT>::UniqueStoreStringAllocator(std::shared_ptr<all template <typename RefT> UniqueStoreStringAllocator<RefT>::~UniqueStoreStringAllocator() { - _store.clearHoldLists(); + _store.reclaim_all_memory(); _store.dropBuffers(); } @@ -49,7 +49,7 @@ UniqueStoreStringAllocator<RefT>::allocate(const char *value) auto handle = _store.template freeListAllocator<WrappedExternalEntryType, UniqueStoreEntryReclaimer<WrappedExternalEntryType>>(0).alloc(std::string(value)); RefT iRef(handle.ref); auto &state = _store.getBufferState(iRef.bufferId()); - state.incExtraUsedBytes(value_len + 1); + state.stats().inc_extra_used_bytes(value_len + 1); return handle.ref; } } @@ -71,7 +71,7 @@ UniqueStoreStringAllocator<RefT>::hold(EntryRef ref) template <typename RefT> EntryRef -UniqueStoreStringAllocator<RefT>::move(EntryRef ref) +UniqueStoreStringAllocator<RefT>::move_on_compact(EntryRef ref) { RefT iRef(ref); uint32_t type_id = _store.getTypeId(iRef.bufferId()); @@ -87,7 +87,7 @@ UniqueStoreStringAllocator<RefT>::move(EntryRef ref) auto handle = _store.template allocator<WrappedExternalEntryType>(0).alloc(*_store.template getEntry<WrappedExternalEntryType>(iRef)); auto &state = _store.getBufferState(RefT(handle.ref).bufferId()); auto &value = static_cast<const WrappedExternalEntryType *>(handle.data)->value(); - state.incExtraUsedBytes(value.size() + 1); + state.stats().inc_extra_used_bytes(value.size() + 1); return handle.ref; } } diff --git a/vespalib/src/vespa/vespalib/util/generation_hold_list.h b/vespalib/src/vespa/vespalib/util/generation_hold_list.h new file mode 100644 index 00000000000..bdb58afb504 --- /dev/null +++ b/vespalib/src/vespa/vespalib/util/generation_hold_list.h @@ -0,0 +1,106 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "generationhandler.h" +#include <atomic> +#include <deque> +#include <vector> + +namespace vespalib { + +/** + * Class used to hold data elements until they can be safely reclaimed when they are no longer accessed by readers. + * + * This class must be used in accordance with a GenerationHandler. + */ +template <typename T, bool track_bytes_held, bool use_deque> +class GenerationHoldList { +private: + using generation_t = vespalib::GenerationHandler::generation_t; + + struct ElemWithGen { + T elem; + generation_t gen; + ElemWithGen(T elem_in, generation_t gen_in) + : elem(std::move(elem_in)), + gen(gen_in) + {} + size_t byte_size() const { + if constexpr (track_bytes_held) { + return elem->byte_size(); + } + return 0; + } + }; + + struct NoopFunc { void operator()(const T&){} }; + + using ElemList = std::vector<T>; + using ElemWithGenList = std::conditional_t<use_deque, + std::deque<ElemWithGen>, + std::vector<ElemWithGen>>; + + ElemList _phase_1_list; + ElemWithGenList _phase_2_list; + std::atomic<size_t> _held_bytes; + + /** + * Transfer elements from phase 1 to phase 2 list, assigning the current generation. + */ + void assign_generation_internal(generation_t current_gen); + + template<typename Func> + void reclaim_internal(generation_t oldest_used_gen, Func callback); + +public: + GenerationHoldList(); + ~GenerationHoldList(); + + /** + * Insert the given data element on this hold list. + */ + void insert(T data); + + /** + * Assign the current generation to all data elements inserted on the hold list + * since the last time this function was called. + */ + void assign_generation(generation_t current_gen) { + if (!_phase_1_list.empty()) { + assign_generation_internal(current_gen); + } + } + + /** + * Reclaim all data elements where the assigned generation < oldest used generation. + * The callback function is called for each data element reclaimed. + **/ + template<typename Func> + void reclaim(generation_t oldest_used_gen, Func callback) { + if (!_phase_2_list.empty() && (_phase_2_list.front().gen < oldest_used_gen)) { + reclaim_internal(oldest_used_gen, callback); + } + } + + void reclaim(generation_t oldest_used_gen) { + reclaim(oldest_used_gen, NoopFunc()); + } + + /** + * Reclaim all data elements from this hold list. + */ + void reclaim_all(); + + /** + * Reclaim all data elements from this hold list. + * The callback function is called for all data elements reclaimed. + */ + template<typename Func> + void reclaim_all(Func callback); + + size_t get_held_bytes() const { return _held_bytes.load(std::memory_order_relaxed); } + +}; + +} diff --git a/vespalib/src/vespa/vespalib/util/generation_hold_list.hpp b/vespalib/src/vespa/vespalib/util/generation_hold_list.hpp new file mode 100644 index 00000000000..4855d1c651d --- /dev/null +++ b/vespalib/src/vespa/vespalib/util/generation_hold_list.hpp @@ -0,0 +1,88 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "generation_hold_list.h" +#include <cassert> + +namespace vespalib { + +template <typename T, bool track_bytes_held, bool use_deque> +void +GenerationHoldList<T, track_bytes_held, use_deque>::assign_generation_internal(generation_t current_gen) +{ + for (auto& elem : _phase_1_list) { + _phase_2_list.push_back(ElemWithGen(std::move(elem), current_gen)); + } + _phase_1_list.clear(); +} + +template <typename T, bool track_bytes_held, bool use_deque> +template <typename Func> +void +GenerationHoldList<T, track_bytes_held, use_deque>::reclaim_internal(generation_t oldest_used_gen, Func func) +{ + auto itr = _phase_2_list.begin(); + auto ite = _phase_2_list.end(); + for (; itr != ite; ++itr) { + if (itr->gen >= oldest_used_gen) { + break; + } + const auto& elem = itr->elem; + func(elem); + if constexpr (track_bytes_held) { + _held_bytes.store(get_held_bytes() - itr->byte_size(), std::memory_order_relaxed); + } + } + if (itr != _phase_2_list.begin()) { + _phase_2_list.erase(_phase_2_list.begin(), itr); + } +} + +template <typename T, bool track_bytes_held, bool use_deque> +GenerationHoldList<T, track_bytes_held, use_deque>::GenerationHoldList() + : _phase_1_list(), + _phase_2_list(), + _held_bytes() +{ +} + +template <typename T, bool track_bytes_held, bool use_deque> +GenerationHoldList<T, track_bytes_held, use_deque>::~GenerationHoldList() +{ + assert(_phase_1_list.empty()); + assert(_phase_2_list.empty()); + assert(get_held_bytes() == 0); +} + +template <typename T, bool track_bytes_held, bool use_deque> +void +GenerationHoldList<T, track_bytes_held, use_deque>::insert(T data) +{ + _phase_1_list.push_back(std::move(data)); + if constexpr (track_bytes_held) { + _held_bytes.store(get_held_bytes() + _phase_1_list.back()->byte_size(), std::memory_order_relaxed); + } +} + +template <typename T, bool track_bytes_held, bool use_deque> +void +GenerationHoldList<T, track_bytes_held, use_deque>::reclaim_all() +{ + _phase_1_list.clear(); + _phase_2_list.clear(); + _held_bytes = 0; +} + +template <typename T, bool track_bytes_held, bool use_deque> +template <typename Func> +void +GenerationHoldList<T, track_bytes_held, use_deque>::reclaim_all(Func func) +{ + for (const auto& elem_with_gen : _phase_2_list) { + func(elem_with_gen.elem); + } + reclaim_all(); +} + +} diff --git a/vespalib/src/vespa/vespalib/util/generationhandler.cpp b/vespalib/src/vespa/vespalib/util/generationhandler.cpp index d1cc0271068..3562926d88d 100644 --- a/vespalib/src/vespa/vespalib/util/generationhandler.cpp +++ b/vespalib/src/vespa/vespalib/util/generationhandler.cpp @@ -111,7 +111,7 @@ GenerationHandler::Guard::operator=(Guard &&rhs) } void -GenerationHandler::updateFirstUsedGeneration() +GenerationHandler::update_oldest_used_generation() { for (;;) { if (_first == _last.load(std::memory_order_relaxed)) @@ -125,12 +125,12 @@ GenerationHandler::updateFirstUsedGeneration() toFree->_next = _free; _free = toFree; } - _firstUsedGeneration.store(_first->_generation, std::memory_order_relaxed); + _oldest_used_generation.store(_first->_generation, std::memory_order_relaxed); } GenerationHandler::GenerationHandler() : _generation(0), - _firstUsedGeneration(0), + _oldest_used_generation(0), _last(nullptr), _first(nullptr), _free(nullptr), @@ -144,7 +144,7 @@ GenerationHandler::GenerationHandler() GenerationHandler::~GenerationHandler(void) { - updateFirstUsedGeneration(); + update_oldest_used_generation(); assert(_first == _last.load(std::memory_order_relaxed)); while (_free != nullptr) { GenerationHold *toFree = _free; @@ -190,7 +190,7 @@ GenerationHandler::incGeneration() // reader set_generation(ngen); last->_generation.store(ngen, std::memory_order_relaxed); - updateFirstUsedGeneration(); + update_oldest_used_generation(); return; } GenerationHold *nhold = nullptr; @@ -207,7 +207,7 @@ GenerationHandler::incGeneration() last->_next = nhold; set_generation(ngen); _last.store(nhold, std::memory_order_release); - updateFirstUsedGeneration(); + update_oldest_used_generation(); } uint32_t @@ -215,7 +215,7 @@ GenerationHandler::getGenerationRefCount(generation_t gen) const { if (static_cast<sgeneration_t>(gen - getCurrentGeneration()) > 0) return 0u; - if (static_cast<sgeneration_t>(getFirstUsedGeneration() - gen) > 0) + if (static_cast<sgeneration_t>(get_oldest_used_generation() - gen) > 0) return 0u; for (GenerationHold *hold = _first; hold != nullptr; hold = hold->_next) { if (hold->_generation.load(std::memory_order_relaxed) == gen) diff --git a/vespalib/src/vespa/vespalib/util/generationhandler.h b/vespalib/src/vespa/vespalib/util/generationhandler.h index 9637ad0e414..6ba71b7f5fb 100644 --- a/vespalib/src/vespa/vespalib/util/generationhandler.h +++ b/vespalib/src/vespa/vespalib/util/generationhandler.h @@ -73,7 +73,7 @@ public: private: std::atomic<generation_t> _generation; - std::atomic<generation_t> _firstUsedGeneration; + std::atomic<generation_t> _oldest_used_generation; std::atomic<GenerationHold *> _last; // Points to "current generation" entry GenerationHold *_first; // Points to "firstUsedGeneration" entry GenerationHold *_free; // List of free entries @@ -101,17 +101,17 @@ public: void incGeneration(); /** - * Update first used generation. + * Update the oldest used generation. * Should be called by the writer thread. */ - void updateFirstUsedGeneration(); + void update_oldest_used_generation(); /** - * Returns the first generation guarded by a reader. It might be too low - * if writer hasn't updated first used generation after last reader left. + * Returns the oldest generation guarded by a reader. + * It might be too low if writer hasn't updated oldest used generation after last reader left. */ - generation_t getFirstUsedGeneration() const noexcept { - return _firstUsedGeneration.load(std::memory_order_relaxed); + generation_t get_oldest_used_generation() const noexcept { + return _oldest_used_generation.load(std::memory_order_relaxed); } /** diff --git a/vespalib/src/vespa/vespalib/util/generationholder.cpp b/vespalib/src/vespa/vespalib/util/generationholder.cpp index 5bfa7d152ce..07bde82f007 100644 --- a/vespalib/src/vespa/vespalib/util/generationholder.cpp +++ b/vespalib/src/vespa/vespalib/util/generationholder.cpp @@ -1,66 +1,20 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. #include "generationholder.h" -#include <cassert> +#include "generation_hold_list.hpp" namespace vespalib { -GenerationHeldBase::~GenerationHeldBase() = default; - -GenerationHolder::GenerationHolder() - : _hold1List(), - _hold2List(), - _heldBytes(0) -{ } - -GenerationHolder::~GenerationHolder() -{ - assert(_hold1List.empty()); - assert(_hold2List.empty()); - assert(getHeldBytes() == 0); -} +template class GenerationHoldList<GenerationHeldBase::UP, true, false>; -void -GenerationHolder::hold(GenerationHeldBase::UP data) -{ - _hold1List.push_back(GenerationHeldBase::SP(data.release())); - _heldBytes.store(getHeldBytes() + _hold1List.back()->getSize(), std::memory_order_relaxed); -} +template void GenerationHolderParent::reclaim_internal + <GenerationHolderParent::NoopFunc>(generation_t oldest_used_gen, NoopFunc func); -void -GenerationHolder::transferHoldListsSlow(generation_t generation) -{ - HoldList::iterator it(_hold1List.begin()); - HoldList::iterator ite(_hold1List.end()); - HoldList &hold2List = _hold2List; - for (; it != ite; ++it) { - assert((*it)->_generation == 0u); - (*it)->_generation = generation; - hold2List.push_back(*it); - } - _hold1List.clear(); -} - -void -GenerationHolder::trimHoldListsSlow(generation_t usedGen) -{ - for (;;) { - if (_hold2List.empty()) - break; - GenerationHeldBase &first = *_hold2List.front(); - if (static_cast<sgeneration_t>(first._generation - usedGen) >= 0) - break; - _heldBytes.store(getHeldBytes() - first.getSize(), std::memory_order_relaxed); - _hold2List.erase(_hold2List.begin()); - } -} +GenerationHeldBase::~GenerationHeldBase() = default; -void -GenerationHolder::clearHoldLists() +GenerationHolder::GenerationHolder() + : GenerationHolderParent() { - _hold1List.clear(); - _hold2List.clear(); - _heldBytes = 0; } } diff --git a/vespalib/src/vespa/vespalib/util/generationholder.h b/vespalib/src/vespa/vespalib/util/generationholder.h index ed68a80a308..86d402f9b3b 100644 --- a/vespalib/src/vespa/vespalib/util/generationholder.h +++ b/vespalib/src/vespa/vespalib/util/generationholder.h @@ -2,8 +2,8 @@ #pragma once +#include "generation_hold_list.h" #include "generationhandler.h" -#include <vector> #include <memory> namespace vespalib { @@ -11,79 +11,31 @@ namespace vespalib { class GenerationHeldBase { public: - typedef GenerationHandler::generation_t generation_t; - typedef std::unique_ptr<GenerationHeldBase> UP; - typedef std::shared_ptr<GenerationHeldBase> SP; + using generation_t = GenerationHandler::generation_t; + using UP = std::unique_ptr<GenerationHeldBase>; + using SP = std::shared_ptr<GenerationHeldBase>; - generation_t _generation; private: - size_t _size; + size_t _byte_size; public: - GenerationHeldBase(size_t size) - : _generation(0u), - _size(size) + GenerationHeldBase(size_t byte_size_in) + : _byte_size(byte_size_in) { } virtual ~GenerationHeldBase(); - size_t getSize() const { return _size; } + size_t byte_size() const { return _byte_size; } }; +using GenerationHolderParent = GenerationHoldList<GenerationHeldBase::UP, true, false>; + /* * GenerationHolder is meant to hold large elements until readers can * no longer access them. */ -class GenerationHolder -{ -private: - typedef GenerationHandler::generation_t generation_t; - typedef GenerationHandler::sgeneration_t sgeneration_t; - - typedef std::vector<GenerationHeldBase::SP> HoldList; - - HoldList _hold1List; - HoldList _hold2List; - std::atomic<size_t> _heldBytes; - - /** - * Transfer holds from hold1 to hold2 lists, assigning generation. - */ - void transferHoldListsSlow(generation_t generation); - - /** - * Remove all data elements from this holder where generation < usedGen. - **/ - void trimHoldListsSlow(generation_t usedGen); - +class GenerationHolder : public GenerationHolderParent { public: GenerationHolder(); - ~GenerationHolder(); - - /** - * Add the given data pointer to this holder. - **/ - void hold(GenerationHeldBase::UP data); - - /** - * Transfer holds from hold1 to hold2 lists, assigning generation. - */ - void transferHoldLists(generation_t generation) { - if (!_hold1List.empty()) { - transferHoldListsSlow(generation); - } - } - - /** - * Remove all data elements from this holder where generation < usedGen. - **/ - void trimHoldLists(generation_t usedGen) { - if (!_hold2List.empty() && static_cast<sgeneration_t>(_hold2List.front()->_generation - usedGen) < 0) { - trimHoldListsSlow(usedGen); - } - } - - void clearHoldLists(); - size_t getHeldBytes() const { return _heldBytes.load(std::memory_order_relaxed); } }; } diff --git a/vespalib/src/vespa/vespalib/util/rcuvector.h b/vespalib/src/vespa/vespalib/util/rcuvector.h index 5d084fe3815..b0929303692 100644 --- a/vespalib/src/vespa/vespalib/util/rcuvector.h +++ b/vespalib/src/vespa/vespalib/util/rcuvector.h @@ -182,7 +182,7 @@ public: /** * Remove all old data vectors where generation < firstUsed. **/ - void removeOldGenerations(generation_t firstUsed); + void reclaim_memory(generation_t oldest_used_gen); MemoryUsage getMemoryUsage() const override; }; diff --git a/vespalib/src/vespa/vespalib/util/rcuvector.hpp b/vespalib/src/vespa/vespalib/util/rcuvector.hpp index 97a73a73cc9..eadda8ac1e9 100644 --- a/vespalib/src/vespa/vespalib/util/rcuvector.hpp +++ b/vespalib/src/vespa/vespalib/util/rcuvector.hpp @@ -80,7 +80,7 @@ RcuVectorBase<T>::replaceVector(ArrayType replacement) { replacement.swap(_data); // atomic switch of underlying data size_t holdSize = replacement.capacity() * sizeof(T); auto hold = std::make_unique<RcuVectorHeld<ArrayType>>(holdSize, std::move(replacement)); - _genHolder.hold(std::move(hold)); + _genHolder.insert(std::move(hold)); onReallocation(); } @@ -116,7 +116,7 @@ RcuVectorBase<T>::shrink(size_t newSize) tmpData.swap(_data); // atomic switch of underlying data size_t holdSize = tmpData.capacity() * sizeof(T); auto hold = std::make_unique<RcuVectorHeld<ArrayType>>(holdSize, std::move(tmpData)); - _genHolder.hold(std::move(hold)); + _genHolder.insert(std::move(hold)); onReallocation(); } } @@ -162,7 +162,7 @@ template <typename T> void RcuVector<T>::onReallocation() { RcuVectorBase<T>::onReallocation(); - _genHolderStore.transferHoldLists(_generation); + _genHolderStore.assign_generation(_generation); } template <typename T> @@ -182,14 +182,14 @@ RcuVector<T>::RcuVector(GrowStrategy growStrategy) template <typename T> RcuVector<T>::~RcuVector() { - _genHolderStore.clearHoldLists(); + _genHolderStore.reclaim_all(); } template <typename T> void -RcuVector<T>::removeOldGenerations(generation_t firstUsed) +RcuVector<T>::reclaim_memory(generation_t oldest_used_gen) { - _genHolderStore.trimHoldLists(firstUsed); + _genHolderStore.reclaim(oldest_used_gen); } template <typename T> @@ -197,7 +197,7 @@ MemoryUsage RcuVector<T>::getMemoryUsage() const { MemoryUsage retval(RcuVectorBase<T>::getMemoryUsage()); - retval.mergeGenerationHeldBytes(_genHolderStore.getHeldBytes()); + retval.mergeGenerationHeldBytes(_genHolderStore.get_held_bytes()); return retval; } diff --git a/zkfacade/abi-spec.json b/zkfacade/abi-spec.json index d227f8490dc..41a1854c276 100644 --- a/zkfacade/abi-spec.json +++ b/zkfacade/abi-spec.json @@ -1,4 +1,54 @@ { + "com.yahoo.vespa.curator.api.VespaCurator$Data": { + "superClass": "java.lang.Record", + "interfaces": [], + "attributes": [ + "public", + "final", + "record" + ], + "methods": [ + "public void <init>(com.yahoo.vespa.curator.api.VespaCurator$Meta, byte[])", + "public final java.lang.String toString()", + "public final int hashCode()", + "public final boolean equals(java.lang.Object)", + "public com.yahoo.vespa.curator.api.VespaCurator$Meta meta()", + "public byte[] data()" + ], + "fields": [] + }, + "com.yahoo.vespa.curator.api.VespaCurator$Meta": { + "superClass": "java.lang.Record", + "interfaces": [], + "attributes": [ + "public", + "final", + "record" + ], + "methods": [ + "public void <init>(int)", + "public final java.lang.String toString()", + "public final int hashCode()", + "public final boolean equals(java.lang.Object)", + "public int version()" + ], + "fields": [] + }, + "com.yahoo.vespa.curator.api.VespaCurator$SingletonWorker": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void activate()", + "public abstract void deactivate()", + "public java.lang.String id()" + ], + "fields": [] + }, "com.yahoo.vespa.curator.api.VespaCurator": { "superClass": "java.lang.Object", "interfaces": [], @@ -8,7 +58,18 @@ "abstract" ], "methods": [ - "public abstract java.lang.AutoCloseable lock(com.yahoo.path.Path, java.time.Duration)" + "public abstract java.util.Optional stat(com.yahoo.path.Path)", + "public abstract java.util.Optional read(com.yahoo.path.Path)", + "public abstract com.yahoo.vespa.curator.api.VespaCurator$Meta write(com.yahoo.path.Path, byte[])", + "public abstract java.util.Optional write(com.yahoo.path.Path, byte[], int)", + "public abstract void deleteAll(com.yahoo.path.Path)", + "public abstract void delete(com.yahoo.path.Path)", + "public abstract boolean delete(com.yahoo.path.Path, int)", + "public abstract java.util.List list(com.yahoo.path.Path)", + "public abstract java.lang.AutoCloseable lock(com.yahoo.path.Path, java.time.Duration)", + "public abstract void register(com.yahoo.vespa.curator.api.VespaCurator$SingletonWorker, java.time.Duration)", + "public abstract void unregister(com.yahoo.vespa.curator.api.VespaCurator$SingletonWorker, java.time.Duration)", + "public abstract boolean isActive(java.lang.String)" ], "fields": [] } diff --git a/zkfacade/pom.xml b/zkfacade/pom.xml index 17be17d8302..41053585185 100644 --- a/zkfacade/pom.xml +++ b/zkfacade/pom.xml @@ -13,6 +13,12 @@ <version>8-SNAPSHOT</version> <dependencies> <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>testutil</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java index f159d471f55..ab0c77746fc 100644 --- a/zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java +++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/Curator.java @@ -6,7 +6,6 @@ import com.yahoo.component.AbstractComponent; import com.yahoo.component.annotation.Inject; import com.yahoo.concurrent.DaemonThreadFactory; import com.yahoo.path.Path; -import com.yahoo.vespa.curator.api.VespaCurator; import com.yahoo.vespa.curator.recipes.CuratorCounter; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.zookeeper.VespaZooKeeperServer; @@ -59,13 +58,13 @@ import java.util.logging.Logger; * @author vegardh * @author bratseth */ -public class Curator extends AbstractComponent implements VespaCurator, AutoCloseable { +public class Curator extends AbstractComponent implements AutoCloseable { private static final Logger LOG = Logger.getLogger(Curator.class.getName()); private static final File ZK_CLIENT_CONFIG_FILE = new File(Defaults.getDefaults().underVespaHome("conf/zookeeper/zookeeper-client.cfg")); - // Note that session timeout has min and max values are related to tickTime defined by server, see configserver.def - private static final Duration ZK_SESSION_TIMEOUT = Duration.ofSeconds(120); + // Note that session timeout has min and max values are related to tickTime defined by server, see zookeeper-server.def + static final Duration DEFAULT_ZK_SESSION_TIMEOUT = Duration.ofSeconds(120); private static final Duration ZK_CONNECTION_TIMEOUT = Duration.ofSeconds(30); private static final Duration BASE_SLEEP_TIME = Duration.ofSeconds(1); @@ -77,18 +76,21 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos private final CuratorFramework curatorFramework; private final ConnectionSpec connectionSpec; private final long juteMaxBuffer; + private final Duration sessionTimeout; // All lock keys, to allow re-entrancy. This will grow forever, but this should be too slow to be a problem private final ConcurrentHashMap<Path, Lock> locks = new ConcurrentHashMap<>(); /** Creates a curator instance from a comma-separated string of ZooKeeper host:port strings */ public static Curator create(String connectionSpec) { - return new Curator(ConnectionSpec.create(connectionSpec), Optional.of(ZK_CLIENT_CONFIG_FILE), defaultJuteMaxBuffer); + return new Curator(ConnectionSpec.create(connectionSpec), Optional.of(ZK_CLIENT_CONFIG_FILE), + defaultJuteMaxBuffer, DEFAULT_ZK_SESSION_TIMEOUT); } // For testing only, use Optional.empty for clientConfigFile parameter to create default zookeeper client config public static Curator create(String connectionSpec, Optional<File> clientConfigFile) { - return new Curator(ConnectionSpec.create(connectionSpec), clientConfigFile, defaultJuteMaxBuffer); + return new Curator(ConnectionSpec.create(connectionSpec), clientConfigFile, + defaultJuteMaxBuffer, DEFAULT_ZK_SESSION_TIMEOUT); } @Inject @@ -99,31 +101,35 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos CuratorConfig.Server::port, curatorConfig.zookeeperLocalhostAffinity()), Optional.of(ZK_CLIENT_CONFIG_FILE), - defaultJuteMaxBuffer); + defaultJuteMaxBuffer, + Duration.ofSeconds(curatorConfig.zookeeperSessionTimeoutSeconds())); } protected Curator(String connectionSpec, String zooKeeperEnsembleConnectionSpec, Function<RetryPolicy, CuratorFramework> curatorFactory) { - this(ConnectionSpec.create(connectionSpec, zooKeeperEnsembleConnectionSpec), curatorFactory.apply(DEFAULT_RETRY_POLICY), defaultJuteMaxBuffer); + this(ConnectionSpec.create(connectionSpec, zooKeeperEnsembleConnectionSpec), curatorFactory.apply(DEFAULT_RETRY_POLICY), + defaultJuteMaxBuffer, DEFAULT_ZK_SESSION_TIMEOUT); } - Curator(ConnectionSpec connectionSpec, Optional<File> clientConfigFile, long juteMaxBuffer) { + Curator(ConnectionSpec connectionSpec, Optional<File> clientConfigFile, long juteMaxBuffer, Duration sessionTimeout) { this(connectionSpec, CuratorFrameworkFactory .builder() .retryPolicy(DEFAULT_RETRY_POLICY) - .sessionTimeoutMs((int) ZK_SESSION_TIMEOUT.toMillis()) + .sessionTimeoutMs((int) sessionTimeout.toMillis()) .connectionTimeoutMs((int) ZK_CONNECTION_TIMEOUT.toMillis()) .connectString(connectionSpec.local()) .zookeeperFactory(new VespaZooKeeperFactory(createClientConfig(clientConfigFile))) .dontUseContainerParents() // TODO: Consider changing this in Vespa 9 .build(), - juteMaxBuffer); + juteMaxBuffer, + sessionTimeout); } - private Curator(ConnectionSpec connectionSpec, CuratorFramework curatorFramework, long juteMaxBuffer) { + private Curator(ConnectionSpec connectionSpec, CuratorFramework curatorFramework, long juteMaxBuffer, Duration sessionTimeout) { this.connectionSpec = Objects.requireNonNull(connectionSpec); this.curatorFramework = Objects.requireNonNull(curatorFramework); this.juteMaxBuffer = juteMaxBuffer; + this.sessionTimeout = sessionTimeout; addLoggingListener(); curatorFramework.start(); } @@ -142,6 +148,10 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos } } + public Duration sessionTimeout() { + return sessionTimeout; + } + /** For internal use; prefer creating a {@link CuratorCounter} */ public DistributedAtomicLong createAtomicCounter(String path) { return new DistributedAtomicLong(curatorFramework, path, new ExponentialBackoffRetry((int) BASE_SLEEP_TIME.toMillis(), MAX_RETRIES)); @@ -195,7 +205,11 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos * If the path and any of its parents does not exists they are created. */ // TODO: Use create().orSetData() in Curator 4 and later - public void set(Path path, byte[] data) { + public Stat set(Path path, byte[] data) { + return set(path, data, -1); + } + + public Stat set(Path path, byte[] data, int expectedVersion) { if (data.length > juteMaxBuffer) throw new IllegalArgumentException("Cannot not set data at " + path.getAbsolute() + ", " + data.length + " bytes is too much, max number of bytes allowed per node is " + juteMaxBuffer); @@ -205,12 +219,13 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos String absolutePath = path.getAbsolute(); try { - framework().setData().forPath(absolutePath, data); + return framework().setData().withVersion(expectedVersion).forPath(absolutePath, data); } catch (Exception e) { throw new RuntimeException("Could not set data at " + absolutePath, e); } } + /** @see #create(Path, Duration) */ public boolean create(Path path) { return create(path, null); } @@ -220,6 +235,9 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos * Returns whether a change was attempted. */ public boolean create(Path path, Duration ttl) { + return create(path, ttl, null); + } + private boolean create(Path path, Duration ttl, Stat stat) { if (exists(path)) return false; String absolutePath = path.getAbsolute(); @@ -231,7 +249,8 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos throw new IllegalArgumentException(ttl.toString()); b.withTtl(millis).withMode(CreateMode.PERSISTENT_WITH_TTL); } - b.creatingParentsIfNeeded().forPath(absolutePath, new byte[0]); + if (stat == null) b.creatingParentsIfNeeded() .forPath(absolutePath, new byte[0]); + else b.creatingParentsIfNeeded().storingStatIn(stat).forPath(absolutePath, new byte[0]); } catch (org.apache.zookeeper.KeeperException.NodeExistsException e) { // Path created between exists() and create() call, do nothing } catch (Exception e) { @@ -258,12 +277,25 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos } /** - * Deletes the given path and any children it may have. - * If the path does not exists nothing is done. + * Deletes the path and any children it may have. + * If the path does not exist, nothing is done. */ public void delete(Path path) { + delete(path, true); + } + + /** + * Deletes the path and any children it may have. + * If the path does not exist, nothing is done. + */ + public void delete(Path path, boolean recursive) { + delete(path, -1, recursive); + } + + public void delete(Path path, int expectedVersion, boolean recursive) { try { - framework().delete().guaranteed().deletingChildrenIfNeeded().forPath(path.getAbsolute()); + if (recursive) framework().delete().guaranteed().deletingChildrenIfNeeded().withVersion(expectedVersion).forPath(path.getAbsolute()); + else framework().delete().guaranteed() .withVersion(expectedVersion).forPath(path.getAbsolute()); } catch (KeeperException.NoNodeException e) { // Do nothing } catch (Exception e) { @@ -290,8 +322,13 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos * Empty is returned if the path does not exist. */ public Optional<byte[]> getData(Path path) { + return getData(path, null); + } + + Optional<byte[]> getData(Path path, Stat stat) { try { - return Optional.of(framework().getData().forPath(path.getAbsolute())); + return stat == null ? Optional.of(framework().getData() .forPath(path.getAbsolute())) + : Optional.of(framework().getData().storingStatIn(stat).forPath(path.getAbsolute())); } catch (KeeperException.NoNodeException e) { return Optional.empty(); @@ -317,14 +354,17 @@ public class Curator extends AbstractComponent implements VespaCurator, AutoClos } } - /** Create and acquire a re-entrant lock in given path */ - public Lock lock(Path path, Duration timeout) { - create(path); + /** Create and acquire a re-entrant lock in given path with a TTL */ + public Lock lock(Path path, Duration timeout, Duration ttl) { + create(path, ttl); Lock lock = locks.computeIfAbsent(path, (pathArg) -> new Lock(pathArg.getAbsolute(), this)); lock.acquire(timeout); return lock; } + /** Create and acquire a re-entrant lock in given path */ + public Lock lock(Path path, Duration timeout) { return lock(path, timeout, null); } + /** Returns the curator framework API */ public CuratorFramework framework() { return curatorFramework; diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/CuratorWrapper.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/CuratorWrapper.java new file mode 100644 index 00000000000..27d969c0c09 --- /dev/null +++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/CuratorWrapper.java @@ -0,0 +1,166 @@ +package com.yahoo.vespa.curator; + +import com.yahoo.component.AbstractComponent; +import com.yahoo.component.annotation.Inject; +import com.yahoo.concurrent.UncheckedTimeoutException; +import com.yahoo.jdisc.Metric; +import com.yahoo.path.Path; +import com.yahoo.vespa.curator.api.VespaCurator; +import com.yahoo.yolean.UncheckedInterruptedException; +import org.apache.curator.framework.state.ConnectionState; +import org.apache.zookeeper.KeeperException.BadVersionException; +import org.apache.zookeeper.data.Stat; + +import java.time.Clock; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Implementation of {@link VespaCurator} which delegates to a {@link Curator}. + * This prefixes all paths with {@code "/user"}, to ensure separation with system ZK usage. + * + * @author jonmv + */ +public class CuratorWrapper extends AbstractComponent implements VespaCurator { + + static final Path userRoot = Path.fromString("user"); + + private final Curator curator; + private final SingletonManager singletons; + + @Inject + public CuratorWrapper(Curator curator, Metric metric) { + this(curator, Clock.systemUTC(), Duration.ofSeconds(1), metric); + } + + CuratorWrapper(Curator curator, Clock clock, Duration tickTimeout, Metric metric) { + this.curator = curator; + this.singletons = new SingletonManager(curator, clock, tickTimeout, metric); + + curator.framework().getConnectionStateListenable().addListener((curatorFramework, connectionState) -> { + if (connectionState == ConnectionState.LOST) singletons.invalidate(); + }); + } + + @Override + public Optional<Meta> stat(Path path) { + return curator.getStat(userRoot.append(path)).map(stat -> new Meta(stat.getVersion())); + } + + @Override + public Optional<Data> read(Path path) { + Stat stat = new Stat(); + return curator.getData(userRoot.append(path), stat).map(data -> new Data(new Meta(stat.getVersion()), data)); + } + + @Override + public Meta write(Path path, byte[] data) { + return new Meta(curator.set(userRoot.append(path), data).getVersion()); + } + + @Override + public Optional<Meta> write(Path path, byte[] data, int expectedVersion) { + try { + return Optional.of(new Meta(curator.set(userRoot.append(path), data, expectedVersion).getVersion())); + } + catch (RuntimeException e) { + if (e.getCause() instanceof BadVersionException) return Optional.empty(); + throw e; + } + } + + @Override + public void deleteAll(Path path) { + curator.delete(userRoot.append(path)); + } + + @Override + public void delete(Path path) { + curator.delete(userRoot.append(path), false); + } + + @Override + public boolean delete(Path path, int expectedVersion) { + try { + curator.delete(userRoot.append(path), expectedVersion, false); + return true; + } + catch (RuntimeException e) { + if (e.getCause() instanceof BadVersionException) return false; + throw e; + } + } + + @Override + public List<String> list(Path path) { + return curator.getChildren(userRoot.append(path)); + } + + @Override + public AutoCloseable lock(Path path, Duration timeout) { + return curator.lock(userRoot.append(path), timeout); + } + + @Override + public void register(SingletonWorker singleton, Duration timeout) { + try { + await(singletons.register(singleton.id(), singleton), timeout, "register " + singleton); + } + catch (RuntimeException e) { + try { + unregister(singleton, timeout); + } + catch (Exception f) { + e.addSuppressed(f); + } + throw e; + } + } + + @Override + public void unregister(SingletonWorker singleton, Duration timeout) { + await(singletons.unregister(singleton), timeout, "unregister " + singleton); + } + + private void await(Future<?> future, Duration timeout, String action) { + try { + future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } + catch (InterruptedException e) { + future.cancel(true); + throw new UncheckedInterruptedException("interrupted while " + action, e, true); + } + catch (TimeoutException e) { + future.cancel(true); + throw new UncheckedTimeoutException("timed out while " + action, e); + } + catch (ExecutionException e) { + throw new RuntimeException("failed to " + action, e.getCause()); + } + } + + @Override + public boolean isActive(String singletonId) { + return singletons.isActive(singletonId); + } + + @Override + public void deconstruct() { + try { + singletons.shutdown().get(); + } + catch (InterruptedException e) { + throw new UncheckedInterruptedException(e, true); + } + catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + } + +} diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/SingletonManager.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/SingletonManager.java new file mode 100644 index 00000000000..04f1f6bbc09 --- /dev/null +++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/SingletonManager.java @@ -0,0 +1,427 @@ +package com.yahoo.vespa.curator; + +import com.yahoo.jdisc.Metric; +import com.yahoo.path.Path; +import com.yahoo.protect.Process; +import com.yahoo.vespa.curator.api.VespaCurator.SingletonWorker; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; + +/** + * Manages {@link com.yahoo.vespa.curator.api.VespaCurator.SingletonWorker}. + * + * @author jonmv + */ +class SingletonManager { + + private static final Logger logger = Logger.getLogger(SingletonManager.class.getName()); + + private final Curator curator; + private final Clock clock; + private final Duration tickTimeout; + private final Map<String, Janitor> janitors = new HashMap<>(); + private final Map<String, Integer> count = new HashMap<>(); + private final Map<SingletonWorker, String> registrations = new IdentityHashMap<>(); + private final Metric metric; + + SingletonManager(Curator curator, Clock clock, Duration tickTimeout, Metric metric) { + this.curator = curator; + this.clock = clock; + this.tickTimeout = tickTimeout; + this.metric = metric; + } + + synchronized CompletableFuture<?> register(String singletonId, SingletonWorker singleton) { + if (singletonId.isEmpty() || singletonId.contains("/") || singletonId.contains("..")) { + throw new IllegalArgumentException("singleton ID must be non-empty, and may not contain '/' or '..', but got " + singletonId); + } + String old = registrations.putIfAbsent(singleton, singletonId); + if (old != null) throw new IllegalArgumentException(singleton + " already registered with ID " + old); + count.merge(singletonId, 1, Integer::sum); + return janitors.computeIfAbsent(singletonId, Janitor::new).register(singleton); + } + + synchronized CompletableFuture<?> unregister(SingletonWorker singleton) { + String id = registrations.remove(singleton); + if (id == null) throw new IllegalArgumentException(singleton + " is not registered"); + return janitors.get(id).unregister(singleton) + .whenComplete((__, ___) -> unregistered(id, singleton)); + } + + synchronized void unregistered(String singletonId, SingletonWorker singleton) { + registrations.remove(singleton); + if (count.merge(singletonId, -1, Integer::sum) > 0) return; + count.remove(singletonId); + janitors.remove(singletonId).shutdown(); + } + + /** + * The instant until which this container holds the exclusive lease for activation of singletons with this ID. + * The container may abandon the lease early, if deactivation is triggered and completes before the deadline. + * Unless connection to the underlying ZK cluster is lost, the returned value will regularly move forwards in time. + */ + synchronized Optional<Instant> activeUntil(String singletonId) { + return Optional.ofNullable(janitors.get(singletonId)).map(janitor -> janitor.doom.get()); + } + + /** Whether this container currently holds the activation lease for the given singleton ID. */ + boolean isActive(String singletonId) { + return activeUntil(singletonId).map(clock.instant()::isBefore).orElse(false); + } + + /** Invalidate all leases, due to connection loss. */ + synchronized void invalidate() { + for (Janitor janitor : janitors.values()) janitor.invalidate(); + } + + public synchronized CompletableFuture<?> shutdown() { + CompletableFuture<?>[] futures = new CompletableFuture[registrations.size()]; + int i = 0; + for (SingletonWorker singleton : List.copyOf(registrations.keySet())) { + String id = registrations.get(singleton); + logger.log(WARNING, singleton + " still registered with id '" + id + "' at shutdown"); + futures[i++] = unregister(singleton); + } + return CompletableFuture.allOf(futures) + .orTimeout(10, TimeUnit.SECONDS); + } + + + private class Janitor { + + static class Task { + + enum Type { register, unregister } + + final Type type; + final SingletonWorker singleton; + final CompletableFuture<?> future = new CompletableFuture<>(); + + private Task(Type type, SingletonWorker singleton) { + this.type = type; + this.singleton = singleton; + } + + static Task register(SingletonWorker singleton) { return new Task(Type.register, singleton); } + static Task unregister(SingletonWorker singleton) { return new Task(Type.unregister, singleton); } + + } + + private static final Instant INVALID = Instant.ofEpochMilli(-1); + + final BlockingDeque<Task> tasks = new LinkedBlockingDeque<>(); + final Deque<SingletonWorker> singletons = new ArrayDeque<>(2); + final AtomicReference<Instant> doom = new AtomicReference<>(); + final AtomicBoolean shutdown = new AtomicBoolean(); + final Thread worker; + final String id; + final Path path; + final MetricHelper metrics; + Lock lock = null; + boolean active; + + Janitor(String id) { + this.id = id; + this.path = Path.fromString("/vespa/singleton/v1/" + id); + this.worker = new Thread(this::run, "singleton-janitor-" + id); + this.metrics = new MetricHelper(); + + worker.setDaemon(true); + worker.start(); + } + + public void unlock() { + doom.set(null); + if (lock != null) try { + logger.log(INFO, "Relinquishing lease for " + id); + lock.close(); + lock = null; + } + catch (Exception e) { + logger.log(WARNING, "Failed closing " + lock, e); + } + } + + private void run() { + try { + while ( ! shutdown.get()) { + try { + // Attempt to acquire the lock, and extend our doom. + renewLease(); + // Ensure activation status is set accordingly to doom, or clear state if this fails. + updateStatus(); + // Process the next pending, externally triggered task, if any. + doTask(); + } + catch (InterruptedException e) { + if ( ! shutdown.get()) { + logger.log(WARNING, worker + " interrupted, restarting event loop"); + } + } + } + unlock(); + } + catch (Throwable t) { + Process.logAndDie(worker + " can't continue, shutting down", t); + } + } + + protected void doTask() throws InterruptedException { + Task task = tasks.poll(tickTimeout.toMillis(), TimeUnit.MILLISECONDS); + try { + if (task != null) switch (task.type) { + case register -> { + doRegister(task.singleton); + task.future.complete(null); + } + case unregister -> { + doUnregister(task.singleton); + task.future.complete(null); + } + } + } + catch (RuntimeException e) { + logger.log(WARNING, "Exception attempting to " + task.type + " " + task.singleton + " in " + worker, e); + task.future.completeExceptionally(e); + } + } + + private void doRegister(SingletonWorker singleton) { + logger.log(INFO, "Registering " + singleton + " with ID: " + id); + SingletonWorker current = singletons.peek(); + singletons.push(singleton); + if (active) { + RuntimeException e = null; + if (current != null) try { + logger.log(INFO, "Deactivating " + current); + metrics.deactivation(current::deactivate); + } + catch (RuntimeException f) { + e = f; + } + try { + logger.log(INFO, "Activating " + singleton); + metrics.activation(singleton::activate); + } + catch (RuntimeException f) { + if (e == null) e = f; + else e.addSuppressed(f); + } + if (singletons.isEmpty()) { + logger.log(INFO, "No registered singletons, invalidating lease"); + invalidate(); + } + if (e != null) throw e; + } + } + + private void doUnregister(SingletonWorker singleton) { + logger.log(INFO, "Unregistering " + singleton + " with ID: " + id); + RuntimeException e = null; + SingletonWorker current = singletons.peek(); + if ( ! singletons.remove(singleton)) return; + if (active && current == singleton) { + try { + logger.log(INFO, "Deactivating " + singleton); + metrics.deactivation(singleton::deactivate); + } + catch (RuntimeException f) { + e = f; + } + if ( ! singletons.isEmpty()) try { + logger.log(INFO, "Activating " + singletons.peek()); + metrics.activation(singletons.peek()::activate); + } + catch (RuntimeException f) { + if (e == null) e = f; + else e.addSuppressed(f); + } + if (singletons.isEmpty()) { + logger.log(INFO, "No registered singletons, invalidating lease"); + invalidate(); + } + } + if (e != null) throw e; + } + + /** + * Attempt to acquire the lock, if not held. + * If lock is held, or acquired, ping the ZK cluster to extend our deadline. + */ + private void renewLease() { + if (doom.get() == INVALID) { + logger.log(INFO, "Lease invalidated"); + doom.set(null); + return; // Skip to updateStatus, deactivation, and release the lock. + } + // Witness value to detect if invalidation occurs between here and successful ping. + Instant ourDoom = doom.get(); + Instant start = clock.instant(); + if (lock == null) try { + lock = curator.lock(path.append("lock"), tickTimeout); + logger.log(INFO, "Acquired lock for ID: " + id); + } + catch (RuntimeException e) { + logger.log(FINE, e, () -> "Failed acquiring lock for '" + path + "' within " + tickTimeout); + return; + } + try { + curator.set(path.append("ping"), new byte[0]); + } + catch (RuntimeException e) { + logger.log(FINE, "Failed pinging ZK cluster", e); + return; + } + if ( ! doom.compareAndSet(ourDoom, start.plus(curator.sessionTimeout().multipliedBy(9).dividedBy(10)))) { + logger.log(FINE, "Deadline changed, current lease renewal is void"); + } + } + + /** + * Attempt to activate or deactivate if status has changed. + * If activation fails, we release the lock, to a different container may acquire it. + */ + private void updateStatus() { + Instant ourDoom = doom.get(); + boolean shouldBeActive = ourDoom != null && ourDoom != INVALID && ! clock.instant().isAfter(ourDoom); + if ( ! active && shouldBeActive) { + logger.log(INFO, "Activating singleton for ID: " + id); + try { + active = true; + if ( ! singletons.isEmpty()) metrics.activation(singletons.peek()::activate); + } + catch (RuntimeException e) { + logger.log(WARNING, "Failed to activate " + singletons.peek() + ", deactivating again", e); + shouldBeActive = false; + } + } + if (active && ! shouldBeActive) { + logger.log(INFO, "Deactivating singleton for ID: " + id); + logger.log(FINE, () -> "Doom value is " + doom); + try { + if ( ! singletons.isEmpty()) metrics.deactivation(singletons.peek()::deactivate); + active = false; + } + catch (RuntimeException e) { + logger.log(WARNING, "Failed to deactivate " + singletons.peek(), e); + } + unlock(); + } + } + + CompletableFuture<?> register(SingletonWorker singleton) { + Task task = Task.register(singleton); + tasks.offer(task); + return task.future; + } + + CompletableFuture<?> unregister(SingletonWorker singleton) { + Task task = Task.unregister(singleton); + tasks.offer(task); + return task.future; + } + + void invalidate() { + doom.set(INVALID); + } + + void shutdown() { + logger.log(INFO, "Shutting down " + this); + if ( ! shutdown.compareAndSet(false, true)) { + logger.log(WARNING, "Shutdown called more than once on " + this); + } + if (Thread.currentThread() != worker) { + try { + worker.join(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + else { + unlock(); + } + if ( ! tasks.isEmpty()) { + logger.log(WARNING, "Non-empty task list after shutdown: " + tasks.size() + " remaining"); + for (Task task : tasks) task.future.cancel(true); // Shouldn't happen. + } + } + + private class MetricHelper { + + static final String PREFIX = "jdisc.singleton."; + static final String IS_ACTIVE = PREFIX + "is_active"; + static final String ACTIVATION = PREFIX + "activation.count"; + static final String ACTIVATION_MILLIS = PREFIX + "activation.millis"; + static final String ACTIVATION_FAILURES = PREFIX + "activation.failure.count"; + static final String DEACTIVATION = PREFIX + "deactivation.count"; + static final String DEACTIVATION_MILLIS = PREFIX + "deactivation.millis"; + static final String DEACTIVATION_FAILURES = PREFIX + "deactivation.failure.count"; + + final Metric.Context context; + + MetricHelper() { + this.context = metric.createContext(Map.of("singletonId", id)); + } + + void activation(Runnable activation) { + Instant start = clock.instant(); + boolean failed = false; + metric.add(ACTIVATION, 1, context); + try { + activation.run(); + } + catch (RuntimeException e) { + failed = true; + throw e; + } + finally { + metric.set(ACTIVATION_MILLIS, Duration.between(start, clock.instant()).toMillis(), context); + if (failed) metric.add(ACTIVATION_FAILURES, 1, context); + else metric.set(IS_ACTIVE, 1, context); + } + } + + void deactivation(Runnable deactivation) { + Instant start = clock.instant(); + boolean failed = false; + metric.add(DEACTIVATION, 1, context); + try { + deactivation.run(); + } + catch (RuntimeException e) { + failed = true; + throw e; + } + finally { + metric.set(DEACTIVATION_MILLIS, Duration.between(start, clock.instant()).toMillis(), context); + if (failed) metric.add(DEACTIVATION_FAILURES, 1, context); + metric.set(IS_ACTIVE, 0, context); + } + } + + } + + } + +} diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/api/VespaCurator.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/api/VespaCurator.java index 32aef2da118..f2bc38a4644 100644 --- a/zkfacade/src/main/java/com/yahoo/vespa/curator/api/VespaCurator.java +++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/api/VespaCurator.java @@ -5,16 +5,197 @@ import com.yahoo.concurrent.UncheckedTimeoutException; import com.yahoo.path.Path; import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; /** * A client for a ZooKeeper cluster running inside Vespa. Applications that want to use ZooKeeper can inject this in * their code. * * @author mpolden + * @author jonmv */ public interface VespaCurator { - /** Create and acquire a re-entrant lock in given path. This blocks until the lock is acquired or timeout elapses. */ + /** Returns the stat for the node at the given path, or empty if no node exists at that path. */ + Optional<Meta> stat(Path path); + + /** Returns the content and stat for the node at the given path, or empty if no node exists at that path. */ + Optional<Data> read(Path path); + + /** + * Writes the given data to a node at the given path, creating it and its parents as needed, and returns the + * stat of the modified node. Failure to write, due to connection loss, is retried a limited number of times. + */ + Meta write(Path path, byte[] data); + + /** + * Atomically compares the version in the stat of the node at the given path, with the expected version, and then: + * if they are equal, attempts the write operation (see {@link #write(Path, byte[])}); + * otherwise, return empty. + */ + Optional<Meta> write(Path path, byte[] data, int expectedVersion); + + /** Recursively deletes any node at the given path, and any children it may have. */ + void deleteAll(Path path); + + /** Deletes the node at the given path. Failres due to connection loss are retried a limited number of times. */ + void delete(Path path); + + /** + * Atomically compares the version in the stat of the node at the given path, with the expected version, and then: + * if they are equal, attempts the delete operation (see {@link #delete(Path)}), and returns {@code} true; + * otherwise, returns {@code false}. + */ + boolean delete(Path path, int expectedVersion); + + /** Lists the children of the node at the given path, or throws if there is no node at that path. */ + List<String> list(Path path); + + /** Creates and acquires a re-entrant lock with the given path. This blocks until the lock is acquired or timeout elapses. */ AutoCloseable lock(Path path, Duration timeout) throws UncheckedTimeoutException; + /** Data of a ZK node, including content (possibly empty, never {@code null}) and metadata. */ + record Data(Meta meta, byte[] data) { } + + /** Metadata for a ZK node. */ + record Meta(int version) { } + + /** + * Register the singleton with the framework, so it may become active. + * <p> + * <strong>Call this after construction of the singleton, typically during component construction!</strong> + * <ul> + * <li>If this activates the singleton, this happens synchronously, and any errors are propagated here.</li> + * <li>If this replaces an already active singleton, its deactivation is also called, prior to activation of this.</li> + * <li>If (de)activation is not complete within the given timeout, a timeout exception is thrown.</li> + * <li>If an error occurs (due to failed activation), unregistration is automatically attempted, with the same timeout.</li> + * </ul> + */ + void register(SingletonWorker singleton, Duration timeout); + + /** + * Unregister with the framework, so this singleton may no longer be active, and returns + * <p> + * <strong>Call this before deconstruction of the singleton, typically during component deconstruction!</strong> + * <ul> + * <li>If this singleton is active, deactivation will be called synchronously, and errors propagated here.</li> + * <li>If this also triggers activation of a new singleton, its activation is called after deactivation of this.</li> + * <li>If (de)activation is not complete within the given timeout, a timeout exception is thrown.</li> + * </ul> + */ + void unregister(SingletonWorker singleton, Duration timeout); + + /** + * Whether this container currently holds the exclusive lease for activation of singletons with this ID. + */ + boolean isActive(String singletonId); + + /** + * Callback interface for processes of which only a single instance should be active at any time, across all + * containers in the cluster, and across all component generations. + * <p> + * <br> + * Sample usage: + * <pre> + * public class SingletonHolder extends AbstractComponent { + * + * private static final Duration timeout = Duration.ofSeconds(10); + * private final VespaCurator curator; + * private final SingletonWorker singleton; + * + * public SingletonHolder(VespaCurator curator) { + * this.curator = curator; + * this.singleton = new Singleton(); + * curator.register(singleton, timeout); + * } + * + * @Override + * public void deconstruct() { + * curator.unregister(singleton, timeout); + * singleton.shutdown(); + * } + * + * } + * + * public class Singleton implements SingletonWorker { + * + * private final SharedResource resource = ...; // Shared resource that requires exclusive access. + * private final ExecutorService executor = Executors.newSingleThreadExecutor(); + * private final AtomicBoolean running = new AtomicBoolean(); + * private Future<?> future = null; + * + * @Override + * public void activate() { + * resource.open(); // Verify resource works here, and propagate any errors out. + * running.set(true); + * future = executor.submit(this::doWork); + * } + * + * @Override + * public void deactivate() { + * running.set(false); + * try { future.get(10, TimeUnit.SECONDS); } + * catch (Exception e) { ... } + * finally { resource.close(); } + * } + * + * private void doWork() { + * while (running.get()) { ... } // Regularly check whether we should keep running. + * } + * + * public void shutdown() { + * executor.shutdownNow(); // Executor should have no running tasks at this point. + * } + * + * } + * </pre> + * <p> + * <br> + * Notes to implementors: + * <ul> + * <li>{@link #activate()} is called by the system on a singleton whenever it is the newest registered + * singleton in this container, and this container has the lease for the ID with which the singleton + * was registered. See {@link #id}, {@link #register} and {@link #isActive}.</li> + * <li>{@link #deactivate()} is called by the system on a singleton which is currently active whenever + * the above no longer holds. See {@link #unregister}.</li> + * <li>Callbacks for the same ID are always invoked by the same thread, in serial; the callbacks must + * return in a timely manner, but are encouraged to throw exceptions when something's wrong</li> + * <li>Activation and deactivation may be triggered by: + * <ol> + * <li>the container acquiring or losing the activation lease; or</li> + * <li>registration of unregistration of a new or obsolete singleton.</li> + * </ol> + * Events triggered by the latter happen synchronously, and errors are propagated to the caller for cleanup. + * Events triggered by the former may happen in the background, and because the system tries to always have + * one activated singleton, exceptions during activation will cause the container to abandon its lease, so + * another container may obtain it instead; exceptions during deactivation are only logged. + * </li> + * <li>A container without any registered singletons will not attempt to hold the activation lease.</li> + * </ul> + */ + interface SingletonWorker { + + /** + * Called by the system whenever this singleton becomes the single active worker. + * If this is triggered because the container obtains the activation lease, and activation fails, + * then the container immediately releases the lease, so another container may acquire it instead. + */ + void activate(); + + /** Called by the system whenever this singleton is no longer the single active worker. */ + void deactivate(); + + /** + * The singleton ID to use when registering this with a {@link VespaCurator}. + * At most one singleton worker with the given ID will be active, in the cluster, at any time. + * {@link VespaCurator#isActive(String)} may be polled to see whether this container is currently + * allowed to have an active singleton with the given ID. + */ + default String id() { return getClass().getName(); } + + } + } diff --git a/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCuratorFramework.java b/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCuratorFramework.java index 81fb24bd7e5..c8566015ea1 100644 --- a/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCuratorFramework.java +++ b/zkfacade/src/main/java/com/yahoo/vespa/curator/mock/MockCuratorFramework.java @@ -8,6 +8,7 @@ import com.yahoo.concurrent.UncheckedTimeoutException; import com.yahoo.path.Path; import com.yahoo.vespa.curator.CompletionTimeoutException; import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.mock.MemoryFileSystem.Node; import com.yahoo.vespa.curator.recipes.CuratorLockException; import org.apache.curator.CuratorZookeeperClient; import org.apache.curator.framework.CuratorFramework; @@ -83,6 +84,8 @@ import org.apache.curator.retry.RetryForever; import org.apache.curator.utils.EnsurePath; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.KeeperException.BadVersionException; +import org.apache.zookeeper.KeeperException.NoNodeException; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.data.ACL; import org.apache.zookeeper.data.Stat; @@ -124,6 +127,8 @@ public class MockCuratorFramework implements CuratorFramework { /** Listeners to changes to a particular path */ private final ListenerMap listeners = new ListenerMap(); + public final MockListenable<ConnectionStateListener> connectionStateListeners = new MockListenable<>(); + private CuratorFrameworkState curatorState = CuratorFrameworkState.LATENT; private int monotonicallyIncreasingNumber = 0; @@ -267,7 +272,7 @@ public class MockCuratorFramework implements CuratorFramework { @Override public Listenable<ConnectionStateListener> getConnectionStateListenable() { - return new MockListenable<>(); + return connectionStateListeners; } @Override @@ -351,7 +356,7 @@ public class MockCuratorFramework implements CuratorFramework { // ----- Start of adaptor methods from Curator to the mock file system ----- /** Creates a node below the given directory root */ - private String createNode(String pathString, byte[] content, boolean createParents, CreateMode createMode, MemoryFileSystem.Node root, Listeners listeners, Long ttl) + private String createNode(String pathString, byte[] content, boolean createParents, Stat stat, CreateMode createMode, MemoryFileSystem.Node root, Listeners listeners, Long ttl) throws KeeperException.NodeExistsException, KeeperException.NoNodeException { validatePath(pathString); Path path = Path.fromString(pathString); @@ -371,18 +376,21 @@ public class MockCuratorFramework implements CuratorFramework { node.setTtl(ttl); String nodePath = "/" + path.getParentPath().toString() + "/" + name; listeners.notify(Path.fromString(nodePath), content, PathChildrenCacheEvent.Type.CHILD_ADDED); + if (stat != null) stat.setVersion(node.version()); return nodePath; } /** Deletes a node below the given directory root */ - private void deleteNode(String pathString, boolean deleteChildren, MemoryFileSystem.Node root, Listeners listeners) - throws KeeperException.NoNodeException, KeeperException.NotEmptyException { + private void deleteNode(String pathString, boolean deleteChildren, int version, MemoryFileSystem.Node root, Listeners listeners) + throws KeeperException { validatePath(pathString); Path path = Path.fromString(pathString); MemoryFileSystem.Node parent = root.getNode(Paths.get(path.getParentPath().toString()), false); if (parent == null) throw new KeeperException.NoNodeException(path.toString()); MemoryFileSystem.Node node = parent.children().get(path.getName()); if (node == null) throw new KeeperException.NoNodeException(path.getName() + " under " + parent); + if (version != -1 && version != node.version()) + throw new KeeperException.BadVersionException("expected version " + version + ", but was " + node.version()); if ( ! node.children().isEmpty() && ! deleteChildren) throw new KeeperException.NotEmptyException(path.toString()); parent.remove(path.getName()); @@ -390,16 +398,20 @@ public class MockCuratorFramework implements CuratorFramework { } /** Returns the data of a node */ - private byte[] getData(String pathString, MemoryFileSystem.Node root) throws KeeperException.NoNodeException { + private byte[] getData(String pathString, Stat stat, MemoryFileSystem.Node root) throws KeeperException.NoNodeException { validatePath(pathString); - return getNode(pathString, root).getContent(); + return getNode(pathString, stat, root).getContent(); } /** sets the data of an existing node */ - private void setData(String pathString, byte[] content, MemoryFileSystem.Node root, Listeners listeners) - throws KeeperException.NoNodeException { + private void setData(String pathString, byte[] content, int version, Stat stat, MemoryFileSystem.Node root, Listeners listeners) + throws KeeperException { validatePath(pathString); - getNode(pathString, root).setContent(content); + Node node = getNode(pathString, null, root); + if (version != -1 && version != node.version()) + throw new KeeperException.BadVersionException("expected version " + version + ", but was " + node.version()); + node.setContent(content); + if (stat != null) stat.setVersion(node.version()); listeners.notify(Path.fromString(pathString), content, PathChildrenCacheEvent.Type.CHILD_UPDATED); } @@ -414,13 +426,14 @@ public class MockCuratorFramework implements CuratorFramework { } /** Returns a node or throws the appropriate exception if it doesn't exist */ - private MemoryFileSystem.Node getNode(String pathString, MemoryFileSystem.Node root) throws KeeperException.NoNodeException { + private MemoryFileSystem.Node getNode(String pathString, Stat stat, MemoryFileSystem.Node root) throws KeeperException.NoNodeException { validatePath(pathString); Path path = Path.fromString(pathString); MemoryFileSystem.Node parent = root.getNode(Paths.get(path.getParentPath().toString()), false); if (parent == null) throw new KeeperException.NoNodeException(path.toString()); MemoryFileSystem.Node node = parent.children().get(path.getName()); if (node == null) throw new KeeperException.NoNodeException(path.toString()); + if (stat != null) stat.setVersion(node.version()); return node; } @@ -783,6 +796,7 @@ public class MockCuratorFramework implements CuratorFramework { private abstract static class MockProtectACLCreateModeStatPathAndBytesable<String> implements ProtectACLCreateModeStatPathAndBytesable<String> { + Stat stat = null; public BackgroundPathAndBytesable<String> withACL(List<ACL> list) { throw new UnsupportedOperationException("Not implemented in MockCurator"); } @@ -832,6 +846,7 @@ public class MockCuratorFramework implements CuratorFramework { @Override public ACLBackgroundPathAndBytesable<String> storingStatIn(Stat stat) { + this.stat = stat; return this; } @@ -850,12 +865,12 @@ public class MockCuratorFramework implements CuratorFramework { @Override public String forPath(String s, byte[] bytes) throws Exception { - return createNode(s, bytes, createParents, createMode, fileSystem.root(), listeners, ttl); + return createNode(s, bytes, createParents, stat, createMode, fileSystem.root(), listeners, ttl); } @Override public String forPath(String s) throws Exception { - return createNode(s, new byte[0], createParents, createMode, fileSystem.root(), listeners, ttl); + return createNode(s, new byte[0], createParents, stat, createMode, fileSystem.root(), listeners, ttl); } }; @@ -867,12 +882,12 @@ public class MockCuratorFramework implements CuratorFramework { @Override public String forPath(String s, byte[] bytes) throws Exception { - return createNode(s, bytes, createParents, createMode, fileSystem.root(), listeners, ttl); + return createNode(s, bytes, createParents, stat, createMode, fileSystem.root(), listeners, ttl); } @Override public String forPath(String s) throws Exception { - return createNode(s, new byte[0], createParents, createMode, fileSystem.root(), listeners, ttl); + return createNode(s, new byte[0], createParents, stat, createMode, fileSystem.root(), listeners, ttl); } }; @@ -890,11 +905,11 @@ public class MockCuratorFramework implements CuratorFramework { } public String forPath(String s) throws Exception { - return createNode(s, new byte[0], createParents, createMode, fileSystem.root(), listeners, ttl); + return createNode(s, new byte[0], createParents, null, createMode, fileSystem.root(), listeners, ttl); } public String forPath(String s, byte[] bytes) throws Exception { - return createNode(s, bytes, createParents, createMode, fileSystem.root(), listeners, ttl); + return createNode(s, bytes, createParents, null, createMode, fileSystem.root(), listeners, ttl); } @Override @@ -1043,9 +1058,8 @@ public class MockCuratorFramework implements CuratorFramework { @Override public Stat forPath(String path) throws Exception { try { - MemoryFileSystem.Node node = getNode(path, fileSystem.root()); Stat stat = new Stat(); - stat.setVersion(node.version()); + getNode(path, stat, fileSystem.root()); return stat; } catch (KeeperException.NoNodeException e) { @@ -1067,6 +1081,7 @@ public class MockCuratorFramework implements CuratorFramework { private class MockDeleteBuilder extends MockBackgroundPathableBuilder<Void> implements DeleteBuilder { private boolean deleteChildren = false; + private int version = -1; @Override public BackgroundVersionable deletingChildrenIfNeeded() { @@ -1081,11 +1096,12 @@ public class MockCuratorFramework implements CuratorFramework { @Override public BackgroundPathable<Void> withVersion(int i) { - throw new UnsupportedOperationException("Not implemented in MockCurator"); + version = i; + return this; } public Void forPath(String pathString) throws Exception { - deleteNode(pathString, deleteChildren, fileSystem.root(), listeners); + deleteNode(pathString, deleteChildren, version, fileSystem.root(), listeners); return null; } @@ -1107,7 +1123,7 @@ public class MockCuratorFramework implements CuratorFramework { } public byte[] forPath(String path) throws Exception { - return getData(path, fileSystem.root()); + return getData(path, null, fileSystem.root()); } @Override @@ -1142,13 +1158,34 @@ public class MockCuratorFramework implements CuratorFramework { @Override public WatchPathable<byte[]> storingStatIn(Stat stat) { - throw new UnsupportedOperationException("Not implemented in MockCurator"); + return new WatchPathable<byte[]>() { + @Override + public byte[] forPath(String path) throws Exception { + return getData(path, stat, fileSystem.root()); + } + + @Override + public Pathable<byte[]> watched() { + return null; + } + + @Override + public Pathable<byte[]> usingWatcher(Watcher watcher) { + return null; + } + + @Override + public Pathable<byte[]> usingWatcher(CuratorWatcher watcher) { + return null; + } + }; } } // extends MockBackgroundACLPathAndBytesableBuilder<Stat> private class MockSetDataBuilder implements SetDataBuilder { + int version = -1; @Override public SetDataBackgroundVersionable compressed() { throw new UnsupportedOperationException("Not implemented in MockCurator"); @@ -1156,18 +1193,22 @@ public class MockCuratorFramework implements CuratorFramework { @Override public BackgroundPathAndBytesable<Stat> withVersion(int i) { - throw new UnsupportedOperationException("Not implemented in MockCurator"); + version = i; + return this; } @Override public Stat forPath(String path, byte[] bytes) throws Exception { - setData(path, bytes, fileSystem.root(), listeners); - return null; + Stat stat = new Stat(); + setData(path, bytes, version, stat, fileSystem.root(), listeners); + return stat; } @Override - public Stat forPath(String s) throws Exception { - return null; + public Stat forPath(String path) throws Exception { + Stat stat = new Stat(); + setData(path, new byte[0], version, stat, fileSystem.root(), listeners); + return stat; } @Override @@ -1206,18 +1247,21 @@ public class MockCuratorFramework implements CuratorFramework { } /** Allows addition of directoryListeners which are never called */ - private static class MockListenable<T> implements Listenable<T> { + public static class MockListenable<T> implements Listenable<T> { + + public final List<T> listeners = new ArrayList<>(); @Override public void addListener(T t) { + listeners.add(t); } @Override - public void addListener(T t, Executor executor) { - } + public void addListener(T t, Executor executor) { throw new UnsupportedOperationException("not supported in mock curator"); } @Override public void removeListener(T t) { + listeners.remove(t); } } @@ -1288,13 +1332,13 @@ public class MockCuratorFramework implements CuratorFramework { @Override public CuratorTransactionBridge forPath(String s, byte[] bytes) throws Exception { - createNode(s, bytes, false, createMode, newRoot, delayedListener, null); + createNode(s, bytes, false, null, createMode, newRoot, delayedListener, null); return new MockCuratorTransactionBridge(); } @Override public CuratorTransactionBridge forPath(String s) throws Exception { - createNode(s, new byte[0], false, createMode, newRoot, delayedListener, null); + createNode(s, new byte[0], false, null, createMode, newRoot, delayedListener, null); return new MockCuratorTransactionBridge(); } @@ -1316,14 +1360,16 @@ public class MockCuratorFramework implements CuratorFramework { private class MockTransactionDeleteBuilder implements TransactionDeleteBuilder<CuratorTransactionBridge> { + int version = -1; @Override public Pathable<CuratorTransactionBridge> withVersion(int i) { - throw new UnsupportedOperationException("Not implemented in MockCurator"); + version = i; + return this; } @Override public CuratorTransactionBridge forPath(String path) throws Exception { - deleteNode(path, false, newRoot, delayedListener); + deleteNode(path, false, version, newRoot, delayedListener); return new MockCuratorTransactionBridge(); } @@ -1331,6 +1377,7 @@ public class MockCuratorFramework implements CuratorFramework { private class MockTransactionSetDataBuilder implements TransactionSetDataBuilder<CuratorTransactionBridge> { + int version = -1; @Override public VersionPathAndBytesable<CuratorTransactionBridge> compressed() { throw new UnsupportedOperationException("Not implemented in MockCurator"); @@ -1338,18 +1385,19 @@ public class MockCuratorFramework implements CuratorFramework { @Override public PathAndBytesable<CuratorTransactionBridge> withVersion(int i) { - throw new UnsupportedOperationException("Not implemented in MockCurator"); + version = i; + return this; } @Override public CuratorTransactionBridge forPath(String s, byte[] bytes) throws Exception { - MockCuratorFramework.this.setData(s, bytes, newRoot, delayedListener); + MockCuratorFramework.this.setData(s, bytes, version, null, newRoot, delayedListener); return new MockCuratorTransactionBridge(); } @Override public CuratorTransactionBridge forPath(String s) throws Exception { - MockCuratorFramework.this.setData(s, new byte[0], newRoot, delayedListener); + MockCuratorFramework.this.setData(s, new byte[0], version, null, newRoot, delayedListener); return new MockCuratorTransactionBridge(); } diff --git a/zkfacade/src/test/java/com/yahoo/vespa/curator/CuratorTest.java b/zkfacade/src/test/java/com/yahoo/vespa/curator/CuratorTest.java index 71eae121313..3ace79ecc67 100644 --- a/zkfacade/src/test/java/com/yahoo/vespa/curator/CuratorTest.java +++ b/zkfacade/src/test/java/com/yahoo/vespa/curator/CuratorTest.java @@ -82,7 +82,8 @@ public class CuratorTest { CuratorConfig.Server::port, curatorConfig.zookeeperLocalhostAffinity()), Optional.empty(), - juteMaxBuffer); + juteMaxBuffer, + Curator.DEFAULT_ZK_SESSION_TIMEOUT); } } diff --git a/zkfacade/src/test/java/com/yahoo/vespa/curator/CuratorWrapperTest.java b/zkfacade/src/test/java/com/yahoo/vespa/curator/CuratorWrapperTest.java new file mode 100644 index 00000000000..5eb38c559a9 --- /dev/null +++ b/zkfacade/src/test/java/com/yahoo/vespa/curator/CuratorWrapperTest.java @@ -0,0 +1,333 @@ +package com.yahoo.vespa.curator; + +import com.yahoo.jdisc.test.MockMetric; +import com.yahoo.path.Path; +import com.yahoo.test.ManualClock; +import com.yahoo.vespa.curator.api.VespaCurator; +import com.yahoo.vespa.curator.api.VespaCurator.Meta; +import com.yahoo.vespa.curator.api.VespaCurator.SingletonWorker; +import com.yahoo.vespa.curator.mock.MockCurator; +import com.yahoo.vespa.curator.mock.MockCuratorFramework; +import org.apache.curator.framework.state.ConnectionState; +import org.junit.Test; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Phaser; +import java.util.concurrent.atomic.AtomicReference; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +/** + * @author jonmv + */ +public class CuratorWrapperTest { + + static final Path lockPath = Path.fromString("/vespa/singleton/v1/singleton/lock"); + + @Test + public void testUserApi() throws Exception { + try (Curator wrapped = new MockCurator()) { + CuratorWrapper curator = new CuratorWrapper(wrapped, new MockMetric()); + + Path path = Path.fromString("path"); + assertEquals(Optional.empty(), curator.stat(path)); + + Meta meta = curator.write(path, "data".getBytes(UTF_8)); + assertEquals(Optional.of(meta), curator.stat(path)); + + assertEquals("data", new String(curator.read(path).get().data(), UTF_8)); + assertEquals(meta, curator.read(path).get().meta()); + + assertEquals(Optional.empty(), curator.write(path, new byte[0], 0)); + + meta = curator.write(path, new byte[0], meta.version()).get(); + assertEquals(3, meta.version()); + + assertEquals(List.of("path"), curator.list(Path.createRoot())); + + assertFalse(curator.delete(path, 0)); + + curator.delete(path, 3); + + assertEquals(List.of(), curator.list(Path.createRoot())); + + try (AutoCloseable lock = curator.lock(path, Duration.ofSeconds(1))) { + assertEquals(List.of("path"), wrapped.getChildren(CuratorWrapper.userRoot)); + } + } + } + + @Test + public void testSingleSingleton() { + try (Curator wrapped = new MockCurator()) { + Phaser stunning = new Phaser(1); + ManualClock clock = new ManualClock() { + @Override public Instant instant() { + stunning.arriveAndAwaitAdvance(); + // Let test thread advance time when desired. + stunning.arriveAndAwaitAdvance(); + return super.instant(); + }; + }; + MockMetric metric = new MockMetric(); + CuratorWrapper curator = new CuratorWrapper(wrapped, clock, Duration.ofMillis(100), metric); + + // First singleton to register becomes active during construction. + Singleton singleton = new Singleton(curator); + assertTrue(singleton.isActive); + assertTrue(wrapped.exists(lockPath)); + stunning.register(); + assertTrue(curator.isActive(singleton.id())); + stunning.arriveAndDeregister(); + singleton.shutdown(); + assertFalse(singleton.isActive); + // ... and deactivated as a result of unregistering again. + + // Singleton can be set up again, but this time, time runs away. + Phaser mark2 = new Phaser(2); // Janitor and helper. + new Thread(() -> { + mark2.arriveAndAwaitAdvance(); // Wait for janitor to call activate. + stunning.arriveAndAwaitAdvance(); // Let janitor measure time spent on activation, while test thread waits for it. + stunning.arriveAndAwaitAdvance(); // Let janitor measure time spent on activation, while test thread waits for it. + }).start(); + singleton = new Singleton(curator) { + @Override public void activate() { + // Set up sync in clock on next renewLease. + super.activate(); + stunning.register(); + mark2.arrive(); + } + }; + assertTrue(singleton.isActive); + + stunning.arriveAndAwaitAdvance(); // Wait for next renewLease. + stunning.arriveAndAwaitAdvance(); // Let next renewLease complete. + stunning.arriveAndAwaitAdvance(); // Wait for next updateStatus. + clock.advance(wrapped.sessionTimeout()); + singleton.phaser.register(); // Set up so we can synchronise with deactivation. + stunning.forceTermination(); // Let lease expire, and ensure further ticks complete if we lose the race to unregister. + + singleton.phaser.arriveAndAwaitAdvance(); + assertFalse(singleton.isActive); + verifyMetrics(Map.of("activation.count", 2.0, + "activation.millis", 0.0, + "deactivation.count", 2.0, + "deactivation.millis", 0.0), + metric); + + // Singleton is reactivated next tick. + singleton.phaser.awaitAdvance(singleton.phaser.arriveAndDeregister()); + assertTrue(singleton.isActive); + verifyMetrics(Map.of("activation.count", 3.0, + "activation.millis", 0.0, + "deactivation.count", 2.0, + "deactivation.millis", 0.0), + metric); + + // Manager unregisters remaining singletons on shutdown. + curator.deconstruct(); + assertFalse(singleton.isActive); + verifyMetrics(Map.of("activation.count", 3.0, + "activation.millis", 0.0, + "deactivation.count", 3.0, + "deactivation.millis", 0.0, + "is_active", 0.0), + metric); + } + } + + @Test + public void testSingletonsInSameContainer() { + try (Curator wrapped = new MockCurator()) { + MockMetric metric = new MockMetric(); + CuratorWrapper curator = new CuratorWrapper(wrapped, new ManualClock(), Duration.ofMillis(100), metric); + + // First singleton to register becomes active during construction. + Singleton singleton = new Singleton(curator); + assertTrue(singleton.isActive); + assertTrue(wrapped.exists(lockPath)); + assertTrue(curator.isActive(singleton.id())); + + Singleton newSingleton = new Singleton(curator); + assertTrue(newSingleton.isActive); + assertFalse(singleton.isActive); + + Singleton newerSingleton = new Singleton(curator); + assertTrue(newerSingleton.isActive); + assertFalse(newSingleton.isActive); + assertFalse(singleton.isActive); + + singleton.shutdown(); + assertTrue(newerSingleton.isActive); + assertFalse(newSingleton.isActive); + assertFalse(singleton.isActive); + + newerSingleton.shutdown(); + assertFalse(newerSingleton.isActive); + assertTrue(newSingleton.isActive); + assertFalse(singleton.isActive); + verifyMetrics(Map.of("activation.count", 4.0, + "activation.millis", 0.0, + "deactivation.count", 3.0, + "deactivation.millis", 0.0, + "is_active", 1.0), + metric); + + // Add a singleton which fails activation. + Phaser stunning = new Phaser(2); + AtomicReference<String> thrownMessage = new AtomicReference<>(); + new Thread(() -> { + RuntimeException e = assertThrows(RuntimeException.class, + () -> new Singleton(curator) { + @Override public void activate() { + throw new RuntimeException("expected test exception"); + } + @Override public void deactivate() { + stunning.arriveAndAwaitAdvance(); + stunning.arriveAndAwaitAdvance(); + throw new RuntimeException("expected test exception"); + } + @Override public String toString() { + return "failing singleton"; + } + }); + thrownMessage.set(e.getMessage()); + stunning.arriveAndAwaitAdvance(); + }).start(); + + stunning.arriveAndAwaitAdvance(); // Failing component is about to be deactivated. + assertFalse(newSingleton.isActive); + assertTrue(curator.isActive(newSingleton.id())); // No actual active components, but container has the lease. + verifyMetrics(Map.of("activation.count", 5.0, + "activation.millis", 0.0, + "activation.failure.count", 1.0, + "deactivation.count", 5.0, + "deactivation.millis", 0.0, + "is_active", 0.0), + metric); + stunning.arriveAndAwaitAdvance(); // Failing component is done being deactivated. + stunning.arriveAndAwaitAdvance(); // Failing component is done cleaning up after itself. + assertTrue(newSingleton.isActive); + assertEquals("failed to register failing singleton", thrownMessage.get()); + verifyMetrics(Map.of("activation.count", 6.0, + "activation.millis", 0.0, + "activation.failure.count", 1.0, + "deactivation.count", 5.0, + "deactivation.millis", 0.0, + "is_active", 1.0), + metric); + + newSingleton.shutdown(); + curator.deconstruct(); + verifyMetrics(Map.of("activation.count", 6.0, + "activation.millis", 0.0, + "activation.failure.count", 1.0, + "deactivation.count", 6.0, + "deactivation.millis", 0.0, + "is_active", 0.0), + metric); + } + } + + @Test + public void testSingletonsInDifferentContainers() { + try (MockCurator wrapped = new MockCurator()) { + MockMetric metric = new MockMetric(); + CuratorWrapper curator = new CuratorWrapper(wrapped, new ManualClock(), Duration.ofMillis(100), metric); + + // Simulate a different container holding the lock. + Singleton singleton; + try (Lock lock = wrapped.lock(lockPath, Duration.ofSeconds(1))) { + singleton = new Singleton(curator); + assertFalse(singleton.isActive); + assertFalse(curator.isActive(singleton.id())); + assertEquals(Map.of(), metric.metrics()); + singleton.phaser.register(); + } + + singleton.phaser.arriveAndAwaitAdvance(); + assertTrue(curator.isActive(singleton.id())); + assertTrue(singleton.isActive); + verifyMetrics(Map.of("activation.count", 1.0), + metric); + + // Simulate a different container wanting the lock. + Phaser stunning = new Phaser(2); + new Thread(() -> { + try (Lock lock = wrapped.lock(lockPath, Duration.ofSeconds(2))) { + stunning.arriveAndAwaitAdvance(); + stunning.arriveAndAwaitAdvance(); + } + }).start(); + + // Simulate connection loss for our singleton's ZK session. + ((MockCuratorFramework) wrapped.framework()).connectionStateListeners.listeners.forEach(listener -> listener.stateChanged(null, ConnectionState.LOST)); + singleton.phaser.arriveAndAwaitAdvance(); + stunning.arriveAndAwaitAdvance(); + assertFalse(singleton.isActive); + verifyMetrics(Map.of("activation.count", 1.0, + "activation.millis", 0.0, + "deactivation.count", 1.0, + "deactivation.millis", 0.0, + "is_active", 0.0), + metric); + + // Connection is restored, and the other container releases the lock again. + stunning.arriveAndAwaitAdvance(); + singleton.phaser.arriveAndAwaitAdvance(); + assertTrue(singleton.isActive); + verifyMetrics(Map.of("activation.count", 2.0, + "activation.millis", 0.0, + "deactivation.count", 1.0, + "deactivation.millis", 0.0), + metric); + + singleton.phaser.arriveAndDeregister(); + singleton.shutdown(); + curator.deconstruct(); + assertFalse(singleton.isActive); + verifyMetrics(Map.of("activation.count", 2.0, + "activation.millis", 0.0, + "deactivation.count", 2.0, + "deactivation.millis", 0.0, + "is_active", 0.0), + metric); + } + } + + static class Singleton implements SingletonWorker { + final VespaCurator curator; + Singleton(VespaCurator curator) { + this.curator = curator; + + curator.register(this, Duration.ofSeconds(2)); + } + boolean isActive; + Phaser phaser = new Phaser(1); + @Override public String id() { return "singleton"; } // ... lest anonymous subclasses get different IDs ... ƪ(`▿▿▿▿´ƪ) + @Override public void activate() { + if (isActive) throw new IllegalStateException("already active"); + isActive = true; + phaser.arriveAndAwaitAdvance(); + } + @Override public void deactivate() { + if ( ! isActive) throw new IllegalStateException("already inactive"); + isActive = false; + phaser.arriveAndAwaitAdvance(); + } + public void shutdown() { curator.unregister(this, Duration.ofSeconds(2)); } + } + + static void verifyMetrics(Map<String, Double> expected, MockMetric metrics) { + expected.forEach((metric, value) -> assertEquals(metric, value, metrics.metrics().get("jdisc.singleton." + metric).get(Map.of("singletonId", "singleton")))); + } + +} diff --git a/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/SyncRequestProcessor.java b/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/SyncRequestProcessor.java new file mode 100644 index 00000000000..e03e0b07944 --- /dev/null +++ b/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/SyncRequestProcessor.java @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.zookeeper.server; + +import java.io.Flushable; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import org.apache.zookeeper.common.Time; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This RequestProcessor logs requests to disk. It batches the requests to do + * the io efficiently. The request is not passed to the next RequestProcessor + * until its log has been synced to disk. + * + * SyncRequestProcessor is used in 3 different cases + * 1. Leader - Sync request to disk and forward it to AckRequestProcessor which + * send ack back to itself. + * 2. Follower - Sync request to disk and forward request to + * SendAckRequestProcessor which send the packets to leader. + * SendAckRequestProcessor is flushable which allow us to force + * push packets to leader. + * 3. Observer - Sync committed request to disk (received as INFORM packet). + * It never send ack back to the leader, so the nextProcessor will + * be null. This change the semantic of txnlog on the observer + * since it only contains committed txns. + */ +public class SyncRequestProcessor extends ZooKeeperCriticalThread implements RequestProcessor { + + private static final Logger LOG = LoggerFactory.getLogger(SyncRequestProcessor.class); + + private static final Request REQUEST_OF_DEATH = Request.requestOfDeath; + + private static class FlushRequest extends Request { + private final CountDownLatch latch = new CountDownLatch(1); + public FlushRequest() { + super(null, 0, 0, 0, null, null); + } + } + + private static final Request turnForwardingDelayOn = new Request(null, 0, 0, 0, null, null); + private static final Request turnForwardingDelayOff = new Request(null, 0, 0, 0, null, null); + + private static class DelayingProcessor implements RequestProcessor, Flushable { + private final RequestProcessor next; + private Queue<Request> delayed = null; + private DelayingProcessor(RequestProcessor next) { + this.next = next; + } + @Override + public void flush() throws IOException { + if (delayed == null && next instanceof Flushable) { + ((Flushable) next).flush(); + } + } + @Override + public void processRequest(Request request) throws RequestProcessorException { + if (delayed == null) { + next.processRequest(request); + } else { + delayed.add(request); + } + } + @Override + public void shutdown() { + next.shutdown(); + } + private void close() { + if (delayed == null) { + delayed = new ArrayDeque<>(); + } + } + private void open() throws RequestProcessorException { + if (delayed != null) { + for (Request request : delayed) { + next.processRequest(request); + } + delayed = null; + } + } + } + + /** The number of log entries to log before starting a snapshot */ + private static int snapCount = ZooKeeperServer.getSnapCount(); + + /** + * The total size of log entries before starting a snapshot + */ + private static long snapSizeInBytes = ZooKeeperServer.getSnapSizeInBytes(); + + /** + * Random numbers used to vary snapshot timing + */ + private int randRoll; + private long randSize; + + private final BlockingQueue<Request> queuedRequests = new LinkedBlockingQueue<Request>(); + + private final Semaphore snapThreadMutex = new Semaphore(1); + + private final ZooKeeperServer zks; + + private final DelayingProcessor nextProcessor; + + /** + * Transactions that have been written and are waiting to be flushed to + * disk. Basically this is the list of SyncItems whose callbacks will be + * invoked after flush returns successfully. + */ + private final Queue<Request> toFlush; + private long lastFlushTime; + + public SyncRequestProcessor(ZooKeeperServer zks, RequestProcessor nextProcessor) { + super("SyncThread:" + zks.getServerId(), zks.getZooKeeperServerListener()); + this.zks = zks; + this.nextProcessor = nextProcessor == null ? null : new DelayingProcessor(nextProcessor); + this.toFlush = new ArrayDeque<>(zks.getMaxBatchSize()); + } + + /** + * used by tests to check for changing + * snapcounts + * @param count + */ + public static void setSnapCount(int count) { + snapCount = count; + } + + /** + * used by tests to get the snapcount + * @return the snapcount + */ + public static int getSnapCount() { + return snapCount; + } + + private long getRemainingDelay() { + long flushDelay = zks.getFlushDelay(); + long duration = Time.currentElapsedTime() - lastFlushTime; + if (duration < flushDelay) { + return flushDelay - duration; + } + return 0; + } + + /** If both flushDelay and maxMaxBatchSize are set (bigger than 0), flush + * whenever either condition is hit. If only one or the other is + * set, flush only when the relevant condition is hit. + */ + private boolean shouldFlush() { + long flushDelay = zks.getFlushDelay(); + long maxBatchSize = zks.getMaxBatchSize(); + if ((flushDelay > 0) && (getRemainingDelay() == 0)) { + return true; + } + return (maxBatchSize > 0) && (toFlush.size() >= maxBatchSize); + } + + /** + * used by tests to check for changing + * snapcounts + * @param size + */ + public static void setSnapSizeInBytes(long size) { + snapSizeInBytes = size; + } + + private boolean shouldSnapshot() { + int logCount = zks.getZKDatabase().getTxnCount(); + long logSize = zks.getZKDatabase().getTxnSize(); + return (logCount > (snapCount / 2 + randRoll)) + || (snapSizeInBytes > 0 && logSize > (snapSizeInBytes / 2 + randSize)); + } + + private void resetSnapshotStats() { + randRoll = ThreadLocalRandom.current().nextInt(snapCount / 2); + randSize = Math.abs(ThreadLocalRandom.current().nextLong() % (snapSizeInBytes / 2)); + } + + @Override + public void run() { + try { + // we do this in an attempt to ensure that not all of the servers + // in the ensemble take a snapshot at the same time + resetSnapshotStats(); + lastFlushTime = Time.currentElapsedTime(); + while (true) { + ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUE_SIZE.add(queuedRequests.size()); + + long pollTime = Math.min(zks.getMaxWriteQueuePollTime(), getRemainingDelay()); + Request si = queuedRequests.poll(pollTime, TimeUnit.MILLISECONDS); + if (si == null) { + /* We timed out looking for more writes to batch, go ahead and flush immediately */ + flush(); + si = queuedRequests.take(); + } + + if (si == REQUEST_OF_DEATH) { + break; + } + + if (si == turnForwardingDelayOn) { + nextProcessor.close(); + continue; + } + if (si == turnForwardingDelayOff) { + nextProcessor.open(); + continue; + } + + if (si instanceof FlushRequest) { + flush(); + ((FlushRequest) si).latch.countDown(); + continue; + } + + long startProcessTime = Time.currentElapsedTime(); + ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUE_TIME.add(startProcessTime - si.syncQueueStartTime); + + // track the number of records written to the log + if (!si.isThrottled() && zks.getZKDatabase().append(si)) { + if (shouldSnapshot()) { + resetSnapshotStats(); + // roll the log + zks.getZKDatabase().rollLog(); + // take a snapshot + if (!snapThreadMutex.tryAcquire()) { + LOG.warn("Too busy to snap, skipping"); + } else { + new ZooKeeperThread("Snapshot Thread") { + public void run() { + try { + zks.takeSnapshot(); + } catch (Exception e) { + LOG.warn("Unexpected exception", e); + } finally { + snapThreadMutex.release(); + } + } + }.start(); + } + } + } else if (toFlush.isEmpty()) { + // optimization for read heavy workloads + // iff this is a read or a throttled request(which doesn't need to be written to the disk), + // and there are no pending flushes (writes), then just pass this to the next processor + if (nextProcessor != null) { + nextProcessor.processRequest(si); + nextProcessor.flush(); + } + continue; + } + toFlush.add(si); + if (shouldFlush()) { + flush(); + } + ServerMetrics.getMetrics().SYNC_PROCESS_TIME.add(Time.currentElapsedTime() - startProcessTime); + } + } catch (Throwable t) { + handleException(this.getName(), t); + } + LOG.info("SyncRequestProcessor exited!"); + } + + /** Flushes all pending writes, and waits for this to complete. */ + public void syncFlush() throws InterruptedException { + FlushRequest marker = new FlushRequest(); + queuedRequests.add(marker); + marker.latch.await(); + } + + public void setDelayForwarding(boolean delayForwarding) { + queuedRequests.add(delayForwarding ? turnForwardingDelayOn : turnForwardingDelayOff); + } + + private void flush() throws IOException, RequestProcessorException { + if (this.toFlush.isEmpty()) { + return; + } + + ServerMetrics.getMetrics().BATCH_SIZE.add(toFlush.size()); + + long flushStartTime = Time.currentElapsedTime(); + zks.getZKDatabase().commit(); + ServerMetrics.getMetrics().SYNC_PROCESSOR_FLUSH_TIME.add(Time.currentElapsedTime() - flushStartTime); + + if (this.nextProcessor == null) { + this.toFlush.clear(); + } else { + while (!this.toFlush.isEmpty()) { + final Request i = this.toFlush.remove(); + long latency = Time.currentElapsedTime() - i.syncQueueStartTime; + ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUE_AND_FLUSH_TIME.add(latency); + this.nextProcessor.processRequest(i); + } + nextProcessor.flush(); + } + lastFlushTime = Time.currentElapsedTime(); + } + + public void shutdown() { + LOG.info("Shutting down"); + queuedRequests.add(REQUEST_OF_DEATH); + try { + this.join(); + this.flush(); + } catch (InterruptedException e) { + LOG.warn("Interrupted while wating for {} to finish", this); + Thread.currentThread().interrupt(); + } catch (IOException e) { + LOG.warn("Got IO exception during shutdown"); + } catch (RequestProcessorException e) { + LOG.warn("Got request processor exception during shutdown"); + } + if (nextProcessor != null) { + nextProcessor.shutdown(); + } + } + + public void processRequest(final Request request) { + Objects.requireNonNull(request, "Request cannot be null"); + + request.syncQueueStartTime = Time.currentElapsedTime(); + queuedRequests.add(request); + ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUED.add(1); + } + +} diff --git a/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/Learner.java b/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/Learner.java new file mode 100644 index 00000000000..8e80fae57dc --- /dev/null +++ b/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/Learner.java @@ -0,0 +1,920 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.zookeeper.server.quorum; + +import static java.nio.charset.StandardCharsets.UTF_8; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLSocket; +import org.apache.jute.BinaryInputArchive; +import org.apache.jute.BinaryOutputArchive; +import org.apache.jute.InputArchive; +import org.apache.jute.OutputArchive; +import org.apache.jute.Record; +import org.apache.zookeeper.ZooDefs.OpCode; +import org.apache.zookeeper.common.Time; +import org.apache.zookeeper.common.X509Exception; +import org.apache.zookeeper.server.ExitCode; +import org.apache.zookeeper.server.Request; +import org.apache.zookeeper.server.ServerCnxn; +import org.apache.zookeeper.server.ServerMetrics; +import org.apache.zookeeper.server.TxnLogEntry; +import org.apache.zookeeper.server.ZooTrace; +import org.apache.zookeeper.server.quorum.QuorumPeer.QuorumServer; +import org.apache.zookeeper.server.quorum.flexible.QuorumVerifier; +import org.apache.zookeeper.server.util.ConfigUtils; +import org.apache.zookeeper.server.util.MessageTracker; +import org.apache.zookeeper.server.util.SerializeUtils; +import org.apache.zookeeper.server.util.ZxidUtils; +import org.apache.zookeeper.txn.SetDataTxn; +import org.apache.zookeeper.txn.TxnDigest; +import org.apache.zookeeper.txn.TxnHeader; +import org.apache.zookeeper.util.ServiceUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is the superclass of two of the three main actors in a ZK + * ensemble: Followers and Observers. Both Followers and Observers share + * a good deal of code which is moved into Peer to avoid duplication. + */ +public class Learner { + + static class PacketInFlight { + + TxnHeader hdr; + Record rec; + TxnDigest digest; + + } + + QuorumPeer self; + LearnerZooKeeperServer zk; + + protected BufferedOutputStream bufferedOutput; + + protected Socket sock; + protected MultipleAddresses leaderAddr; + protected AtomicBoolean sockBeingClosed = new AtomicBoolean(false); + + /** + * Socket getter + */ + public Socket getSocket() { + return sock; + } + + LearnerSender sender = null; + protected InputArchive leaderIs; + protected OutputArchive leaderOs; + /** the protocol version of the leader */ + protected int leaderProtocolVersion = 0x01; + + private static final int BUFFERED_MESSAGE_SIZE = 10; + protected final MessageTracker messageTracker = new MessageTracker(BUFFERED_MESSAGE_SIZE); + + protected static final Logger LOG = LoggerFactory.getLogger(Learner.class); + + /** + * Time to wait after connection attempt with the Leader or LearnerMaster before this + * Learner tries to connect again. + */ + private static final int leaderConnectDelayDuringRetryMs = Integer.getInteger("zookeeper.leaderConnectDelayDuringRetryMs", 100); + + private static final boolean nodelay = System.getProperty("follower.nodelay", "true").equals("true"); + + public static final String LEARNER_ASYNC_SENDING = "zookeeper.learner.asyncSending"; + private static boolean asyncSending = + Boolean.parseBoolean(ConfigUtils.getPropertyBackwardCompatibleWay(LEARNER_ASYNC_SENDING)); + public static final String LEARNER_CLOSE_SOCKET_ASYNC = "zookeeper.learner.closeSocketAsync"; + public static final boolean closeSocketAsync = Boolean + .parseBoolean(ConfigUtils.getPropertyBackwardCompatibleWay(LEARNER_CLOSE_SOCKET_ASYNC)); + + static { + LOG.info("leaderConnectDelayDuringRetryMs: {}", leaderConnectDelayDuringRetryMs); + LOG.info("TCP NoDelay set to: {}", nodelay); + LOG.info("{} = {}", LEARNER_ASYNC_SENDING, asyncSending); + LOG.info("{} = {}", LEARNER_CLOSE_SOCKET_ASYNC, closeSocketAsync); + } + + final ConcurrentHashMap<Long, ServerCnxn> pendingRevalidations = new ConcurrentHashMap<Long, ServerCnxn>(); + + public int getPendingRevalidationsCount() { + return pendingRevalidations.size(); + } + + // for testing + protected static void setAsyncSending(boolean newMode) { + asyncSending = newMode; + LOG.info("{} = {}", LEARNER_ASYNC_SENDING, asyncSending); + + } + protected static boolean getAsyncSending() { + return asyncSending; + } + /** + * validate a session for a client + * + * @param clientId + * the client to be revalidated + * @param timeout + * the timeout for which the session is valid + * @throws IOException + */ + void validateSession(ServerCnxn cnxn, long clientId, int timeout) throws IOException { + LOG.info("Revalidating client: 0x{}", Long.toHexString(clientId)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeLong(clientId); + dos.writeInt(timeout); + dos.close(); + QuorumPacket qp = new QuorumPacket(Leader.REVALIDATE, -1, baos.toByteArray(), null); + pendingRevalidations.put(clientId, cnxn); + if (LOG.isTraceEnabled()) { + ZooTrace.logTraceMessage( + LOG, + ZooTrace.SESSION_TRACE_MASK, + "To validate session 0x" + Long.toHexString(clientId)); + } + writePacket(qp, true); + } + + /** + * write a packet to the leader. + * + * This method is called by multiple threads. We need to make sure that only one thread is writing to leaderOs at a time. + * When packets are sent synchronously, writing is done within a synchronization block. + * When packets are sent asynchronously, sender.queuePacket() is called, which writes to a BlockingQueue, which is thread-safe. + * Reading from this BlockingQueue and writing to leaderOs is the learner sender thread only. + * So we have only one thread writing to leaderOs at a time in either case. + * + * @param pp + * the proposal packet to be sent to the leader + * @throws IOException + */ + void writePacket(QuorumPacket pp, boolean flush) throws IOException { + if (asyncSending) { + sender.queuePacket(pp); + } else { + writePacketNow(pp, flush); + } + } + + void writePacketNow(QuorumPacket pp, boolean flush) throws IOException { + synchronized (leaderOs) { + if (pp != null) { + messageTracker.trackSent(pp.getType()); + leaderOs.writeRecord(pp, "packet"); + } + if (flush) { + bufferedOutput.flush(); + } + } + } + + /** + * Start thread that will forward any packet in the queue to the leader + */ + protected void startSendingThread() { + sender = new LearnerSender(this); + sender.start(); + } + + /** + * read a packet from the leader + * + * @param pp + * the packet to be instantiated + * @throws IOException + */ + void readPacket(QuorumPacket pp) throws IOException { + synchronized (leaderIs) { + leaderIs.readRecord(pp, "packet"); + messageTracker.trackReceived(pp.getType()); + } + if (LOG.isTraceEnabled()) { + final long traceMask = + (pp.getType() == Leader.PING) ? ZooTrace.SERVER_PING_TRACE_MASK + : ZooTrace.SERVER_PACKET_TRACE_MASK; + + ZooTrace.logQuorumPacket(LOG, traceMask, 'i', pp); + } + } + + /** + * send a request packet to the leader + * + * @param request + * the request from the client + * @throws IOException + */ + void request(Request request) throws IOException { + if (request.isThrottled()) { + LOG.error("Throttled request sent to leader: {}. Exiting", request); + ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue()); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream oa = new DataOutputStream(baos); + oa.writeLong(request.sessionId); + oa.writeInt(request.cxid); + oa.writeInt(request.type); + if (request.request != null) { + request.request.rewind(); + int len = request.request.remaining(); + byte[] b = new byte[len]; + request.request.get(b); + request.request.rewind(); + oa.write(b); + } + oa.close(); + QuorumPacket qp = new QuorumPacket(Leader.REQUEST, -1, baos.toByteArray(), request.authInfo); + writePacket(qp, true); + } + + /** + * Returns the address of the node we think is the leader. + */ + protected QuorumServer findLeader() { + QuorumServer leaderServer = null; + // Find the leader by id + Vote current = self.getCurrentVote(); + for (QuorumServer s : self.getView().values()) { + if (s.id == current.getId()) { + // Ensure we have the leader's correct IP address before + // attempting to connect. + s.recreateSocketAddresses(); + leaderServer = s; + break; + } + } + if (leaderServer == null) { + LOG.warn("Couldn't find the leader with id = {}", current.getId()); + } + return leaderServer; + } + + /** + * Overridable helper method to return the System.nanoTime(). + * This method behaves identical to System.nanoTime(). + */ + protected long nanoTime() { + return System.nanoTime(); + } + + /** + * Overridable helper method to simply call sock.connect(). This can be + * overriden in tests to fake connection success/failure for connectToLeader. + */ + protected void sockConnect(Socket sock, InetSocketAddress addr, int timeout) throws IOException { + sock.connect(addr, timeout); + } + + /** + * Establish a connection with the LearnerMaster found by findLearnerMaster. + * Followers only connect to Leaders, Observers can connect to any active LearnerMaster. + * Retries until either initLimit time has elapsed or 5 tries have happened. + * @param multiAddr - the address of the Peer to connect to. + * @throws IOException - if the socket connection fails on the 5th attempt + * if there is an authentication failure while connecting to leader + */ + protected void connectToLeader(MultipleAddresses multiAddr, String hostname) throws IOException { + + this.leaderAddr = multiAddr; + Set<InetSocketAddress> addresses; + if (self.isMultiAddressReachabilityCheckEnabled()) { + // even if none of the addresses are reachable, we want to try to establish connection + // see ZOOKEEPER-3758 + addresses = multiAddr.getAllReachableAddressesOrAll(); + } else { + addresses = multiAddr.getAllAddresses(); + } + ExecutorService executor = Executors.newFixedThreadPool(addresses.size()); + CountDownLatch latch = new CountDownLatch(addresses.size()); + AtomicReference<Socket> socket = new AtomicReference<>(null); + addresses.stream().map(address -> new LeaderConnector(address, socket, latch)).forEach(executor::submit); + + try { + latch.await(); + } catch (InterruptedException e) { + LOG.warn("Interrupted while trying to connect to Leader", e); + } finally { + executor.shutdown(); + try { + if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + LOG.error("not all the LeaderConnector terminated properly"); + } + } catch (InterruptedException ie) { + LOG.error("Interrupted while terminating LeaderConnector executor.", ie); + } + } + + if (socket.get() == null) { + throw new IOException("Failed connect to " + multiAddr); + } else { + sock = socket.get(); + sockBeingClosed.set(false); + } + + self.authLearner.authenticate(sock, hostname); + + leaderIs = BinaryInputArchive.getArchive(new BufferedInputStream(sock.getInputStream())); + bufferedOutput = new BufferedOutputStream(sock.getOutputStream()); + leaderOs = BinaryOutputArchive.getArchive(bufferedOutput); + if (asyncSending) { + startSendingThread(); + } + } + + class LeaderConnector implements Runnable { + + private AtomicReference<Socket> socket; + private InetSocketAddress address; + private CountDownLatch latch; + + LeaderConnector(InetSocketAddress address, AtomicReference<Socket> socket, CountDownLatch latch) { + this.address = address; + this.socket = socket; + this.latch = latch; + } + + @Override + public void run() { + try { + Thread.currentThread().setName("LeaderConnector-" + address); + Socket sock = connectToLeader(); + + if (sock != null && sock.isConnected()) { + if (socket.compareAndSet(null, sock)) { + LOG.info("Successfully connected to leader, using address: {}", address); + } else { + LOG.info("Connection to the leader is already established, close the redundant connection"); + sock.close(); + } + } + + } catch (Exception e) { + LOG.error("Failed connect to {}", address, e); + } finally { + latch.countDown(); + } + } + + private Socket connectToLeader() throws IOException, X509Exception, InterruptedException { + Socket sock = createSocket(); + + // leader connection timeout defaults to tickTime * initLimit + int connectTimeout = self.tickTime * self.initLimit; + + // but if connectToLearnerMasterLimit is specified, use that value to calculate + // timeout instead of using the initLimit value + if (self.connectToLearnerMasterLimit > 0) { + connectTimeout = self.tickTime * self.connectToLearnerMasterLimit; + } + + int remainingTimeout; + long startNanoTime = nanoTime(); + + for (int tries = 0; tries < 5 && socket.get() == null; tries++) { + try { + // recalculate the init limit time because retries sleep for 1000 milliseconds + remainingTimeout = connectTimeout - (int) ((nanoTime() - startNanoTime) / 1_000_000); + if (remainingTimeout <= 0) { + LOG.error("connectToLeader exceeded on retries."); + throw new IOException("connectToLeader exceeded on retries."); + } + + sockConnect(sock, address, Math.min(connectTimeout, remainingTimeout)); + if (self.isSslQuorum()) { + ((SSLSocket) sock).startHandshake(); + } + sock.setTcpNoDelay(nodelay); + break; + } catch (IOException e) { + remainingTimeout = connectTimeout - (int) ((nanoTime() - startNanoTime) / 1_000_000); + + if (remainingTimeout <= leaderConnectDelayDuringRetryMs) { + LOG.error( + "Unexpected exception, connectToLeader exceeded. tries={}, remaining init limit={}, connecting to {}", + tries, + remainingTimeout, + address, + e); + throw e; + } else if (tries >= 4) { + LOG.error( + "Unexpected exception, retries exceeded. tries={}, remaining init limit={}, connecting to {}", + tries, + remainingTimeout, + address, + e); + throw e; + } else { + LOG.warn( + "Unexpected exception, tries={}, remaining init limit={}, connecting to {}", + tries, + remainingTimeout, + address, + e); + sock = createSocket(); + } + } + Thread.sleep(leaderConnectDelayDuringRetryMs); + } + + return sock; + } + } + + /** + * Creating a simple or and SSL socket. + * This can be overridden in tests to fake already connected sockets for connectToLeader. + */ + protected Socket createSocket() throws X509Exception, IOException { + Socket sock; + if (self.isSslQuorum()) { + sock = self.getX509Util().createSSLSocket(); + } else { + sock = new Socket(); + } + sock.setSoTimeout(self.tickTime * self.initLimit); + return sock; + } + + /** + * Once connected to the leader or learner master, perform the handshake + * protocol to establish a following / observing connection. + * @param pktType + * @return the zxid the Leader sends for synchronization purposes. + * @throws IOException + */ + protected long registerWithLeader(int pktType) throws IOException { + /* + * Send follower info, including last zxid and sid + */ + long lastLoggedZxid = self.getLastLoggedZxid(); + QuorumPacket qp = new QuorumPacket(); + qp.setType(pktType); + qp.setZxid(ZxidUtils.makeZxid(self.getAcceptedEpoch(), 0)); + + /* + * Add sid to payload + */ + LearnerInfo li = new LearnerInfo(self.getId(), 0x10000, self.getQuorumVerifier().getVersion()); + ByteArrayOutputStream bsid = new ByteArrayOutputStream(); + BinaryOutputArchive boa = BinaryOutputArchive.getArchive(bsid); + boa.writeRecord(li, "LearnerInfo"); + qp.setData(bsid.toByteArray()); + + writePacket(qp, true); + readPacket(qp); + final long newEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid()); + if (qp.getType() == Leader.LEADERINFO) { + // we are connected to a 1.0 server so accept the new epoch and read the next packet + leaderProtocolVersion = ByteBuffer.wrap(qp.getData()).getInt(); + byte[] epochBytes = new byte[4]; + final ByteBuffer wrappedEpochBytes = ByteBuffer.wrap(epochBytes); + if (newEpoch > self.getAcceptedEpoch()) { + wrappedEpochBytes.putInt((int) self.getCurrentEpoch()); + self.setAcceptedEpoch(newEpoch); + } else if (newEpoch == self.getAcceptedEpoch()) { + // since we have already acked an epoch equal to the leaders, we cannot ack + // again, but we still need to send our lastZxid to the leader so that we can + // sync with it if it does assume leadership of the epoch. + // the -1 indicates that this reply should not count as an ack for the new epoch + wrappedEpochBytes.putInt(-1); + } else { + throw new IOException("Leaders epoch, " + + newEpoch + + " is less than accepted epoch, " + + self.getAcceptedEpoch()); + } + QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null); + writePacket(ackNewEpoch, true); + return ZxidUtils.makeZxid(newEpoch, 0); + } else { + if (newEpoch > self.getAcceptedEpoch()) { + self.setAcceptedEpoch(newEpoch); + } + if (qp.getType() != Leader.NEWLEADER) { + LOG.error("First packet should have been NEWLEADER"); + throw new IOException("First packet should have been NEWLEADER"); + } + return qp.getZxid(); + } + } + + /** + * Finally, synchronize our history with the Leader (if Follower) + * or the LearnerMaster (if Observer). + * @param newLeaderZxid + * @throws IOException + * @throws InterruptedException + */ + protected void syncWithLeader(long newLeaderZxid) throws Exception { + QuorumPacket ack = new QuorumPacket(Leader.ACK, 0, null, null); + QuorumPacket qp = new QuorumPacket(); + long newEpoch = ZxidUtils.getEpochFromZxid(newLeaderZxid); + + QuorumVerifier newLeaderQV = null; + + // In the DIFF case we don't need to do a snapshot because the transactions will sync on top of any existing snapshot + // For SNAP and TRUNC the snapshot is needed to save that history + boolean snapshotNeeded = true; + boolean syncSnapshot = false; + readPacket(qp); + Deque<Long> packetsCommitted = new ArrayDeque<>(); + Deque<PacketInFlight> packetsNotLogged = new ArrayDeque<>(); + Deque<PacketInFlight> packetsNotCommitted = new ArrayDeque<>(); + synchronized (zk) { + if (qp.getType() == Leader.DIFF) { + LOG.info("Getting a diff from the leader 0x{}", Long.toHexString(qp.getZxid())); + self.setSyncMode(QuorumPeer.SyncMode.DIFF); + if (zk.shouldForceWriteInitialSnapshotAfterLeaderElection()) { + LOG.info("Forcing a snapshot write as part of upgrading from an older Zookeeper. This should only happen while upgrading."); + snapshotNeeded = true; + syncSnapshot = true; + } else { + snapshotNeeded = false; + } + } else if (qp.getType() == Leader.SNAP) { + self.setSyncMode(QuorumPeer.SyncMode.SNAP); + LOG.info("Getting a snapshot from leader 0x{}", Long.toHexString(qp.getZxid())); + // The leader is going to dump the database + // db is clear as part of deserializeSnapshot() + zk.getZKDatabase().deserializeSnapshot(leaderIs); + // ZOOKEEPER-2819: overwrite config node content extracted + // from leader snapshot with local config, to avoid potential + // inconsistency of config node content during rolling restart. + if (!self.isReconfigEnabled()) { + LOG.debug("Reset config node content from local config after deserialization of snapshot."); + zk.getZKDatabase().initConfigInZKDatabase(self.getQuorumVerifier()); + } + String signature = leaderIs.readString("signature"); + if (!signature.equals("BenWasHere")) { + LOG.error("Missing signature. Got {}", signature); + throw new IOException("Missing signature"); + } + zk.getZKDatabase().setlastProcessedZxid(qp.getZxid()); + + // immediately persist the latest snapshot when there is txn log gap + syncSnapshot = true; + } else if (qp.getType() == Leader.TRUNC) { + //we need to truncate the log to the lastzxid of the leader + self.setSyncMode(QuorumPeer.SyncMode.TRUNC); + LOG.warn("Truncating log to get in sync with the leader 0x{}", Long.toHexString(qp.getZxid())); + boolean truncated = zk.getZKDatabase().truncateLog(qp.getZxid()); + if (!truncated) { + // not able to truncate the log + LOG.error("Not able to truncate the log 0x{}", Long.toHexString(qp.getZxid())); + ServiceUtils.requestSystemExit(ExitCode.QUORUM_PACKET_ERROR.getValue()); + } + zk.getZKDatabase().setlastProcessedZxid(qp.getZxid()); + + } else { + LOG.error("Got unexpected packet from leader: {}, exiting ... ", LearnerHandler.packetToString(qp)); + ServiceUtils.requestSystemExit(ExitCode.QUORUM_PACKET_ERROR.getValue()); + } + zk.getZKDatabase().initConfigInZKDatabase(self.getQuorumVerifier()); + zk.createSessionTracker(); + + long lastQueued = 0; + + // in Zab V1.0 (ZK 3.4+) we might take a snapshot when we get the NEWLEADER message, but in pre V1.0 + // we take the snapshot on the UPDATE message, since Zab V1.0 also gets the UPDATE (after the NEWLEADER) + // we need to make sure that we don't take the snapshot twice. + boolean isPreZAB1_0 = true; + //If we are not going to take the snapshot be sure the transactions are not applied in memory + // but written out to the transaction log + boolean writeToTxnLog = !snapshotNeeded; + TxnLogEntry logEntry; + // we are now going to start getting transactions to apply followed by an UPTODATE + outerLoop: + while (self.isRunning()) { + readPacket(qp); + switch (qp.getType()) { + case Leader.PROPOSAL: + PacketInFlight pif = new PacketInFlight(); + logEntry = SerializeUtils.deserializeTxn(qp.getData()); + pif.hdr = logEntry.getHeader(); + pif.rec = logEntry.getTxn(); + pif.digest = logEntry.getDigest(); + if (pif.hdr.getZxid() != lastQueued + 1) { + LOG.warn( + "Got zxid 0x{} expected 0x{}", + Long.toHexString(pif.hdr.getZxid()), + Long.toHexString(lastQueued + 1)); + } + lastQueued = pif.hdr.getZxid(); + + if (pif.hdr.getType() == OpCode.reconfig) { + SetDataTxn setDataTxn = (SetDataTxn) pif.rec; + QuorumVerifier qv = self.configFromString(new String(setDataTxn.getData(), UTF_8)); + self.setLastSeenQuorumVerifier(qv, true); + } + + packetsNotLogged.add(pif); + packetsNotCommitted.add(pif); + break; + case Leader.COMMIT: + case Leader.COMMITANDACTIVATE: + pif = packetsNotCommitted.peekFirst(); + if (pif.hdr.getZxid() != qp.getZxid()) { + LOG.warn( + "Committing 0x{}, but next proposal is 0x{}", + Long.toHexString(qp.getZxid()), + Long.toHexString(pif.hdr.getZxid())); + } else { + if (qp.getType() == Leader.COMMITANDACTIVATE) { + QuorumVerifier qv = self.configFromString(new String(((SetDataTxn) pif.rec).getData(), UTF_8)); + boolean majorChange = self.processReconfig( + qv, + ByteBuffer.wrap(qp.getData()).getLong(), qp.getZxid(), + true); + if (majorChange) { + throw new Exception("changes proposed in reconfig"); + } + } + if (!writeToTxnLog) { + zk.processTxn(pif.hdr, pif.rec); + packetsNotLogged.remove(); + packetsNotCommitted.remove(); + } else { + packetsNotCommitted.remove(); + packetsCommitted.add(qp.getZxid()); + } + } + break; + case Leader.INFORM: + case Leader.INFORMANDACTIVATE: + PacketInFlight packet = new PacketInFlight(); + + if (qp.getType() == Leader.INFORMANDACTIVATE) { + ByteBuffer buffer = ByteBuffer.wrap(qp.getData()); + long suggestedLeaderId = buffer.getLong(); + byte[] remainingdata = new byte[buffer.remaining()]; + buffer.get(remainingdata); + logEntry = SerializeUtils.deserializeTxn(remainingdata); + packet.hdr = logEntry.getHeader(); + packet.rec = logEntry.getTxn(); + packet.digest = logEntry.getDigest(); + QuorumVerifier qv = self.configFromString(new String(((SetDataTxn) packet.rec).getData(), UTF_8)); + boolean majorChange = self.processReconfig(qv, suggestedLeaderId, qp.getZxid(), true); + if (majorChange) { + throw new Exception("changes proposed in reconfig"); + } + } else { + logEntry = SerializeUtils.deserializeTxn(qp.getData()); + packet.rec = logEntry.getTxn(); + packet.hdr = logEntry.getHeader(); + packet.digest = logEntry.getDigest(); + // Log warning message if txn comes out-of-order + if (packet.hdr.getZxid() != lastQueued + 1) { + LOG.warn( + "Got zxid 0x{} expected 0x{}", + Long.toHexString(packet.hdr.getZxid()), + Long.toHexString(lastQueued + 1)); + } + lastQueued = packet.hdr.getZxid(); + } + if (!writeToTxnLog) { + // Apply to db directly if we haven't taken the snapshot + zk.processTxn(packet.hdr, packet.rec); + } else { + packetsNotLogged.add(packet); + packetsCommitted.add(qp.getZxid()); + } + + break; + case Leader.UPTODATE: + LOG.info("Learner received UPTODATE message"); + if (newLeaderQV != null) { + boolean majorChange = self.processReconfig(newLeaderQV, null, null, true); + if (majorChange) { + throw new Exception("changes proposed in reconfig"); + } + } + if (isPreZAB1_0) { + zk.takeSnapshot(syncSnapshot); + self.setCurrentEpoch(newEpoch); + } + self.setZooKeeperServer(zk); + self.adminServer.setZooKeeperServer(zk); + break outerLoop; + case Leader.NEWLEADER: // Getting NEWLEADER here instead of in discovery + // means this is Zab 1.0 + LOG.info("Learner received NEWLEADER message"); + if (qp.getData() != null && qp.getData().length > 1) { + try { + QuorumVerifier qv = self.configFromString(new String(qp.getData(), UTF_8)); + self.setLastSeenQuorumVerifier(qv, true); + newLeaderQV = qv; + } catch (Exception e) { + e.printStackTrace(); + } + } + + if (snapshotNeeded) { + zk.takeSnapshot(syncSnapshot); + } + + self.setCurrentEpoch(newEpoch); + writeToTxnLog = true; + //Anything after this needs to go to the transaction log, not applied directly in memory + isPreZAB1_0 = false; + + // ZOOKEEPER-3911: make sure sync the uncommitted logs before commit them (ACK NEWLEADER). + sock.setSoTimeout(self.tickTime * self.syncLimit); + self.setSyncMode(QuorumPeer.SyncMode.NONE); + zk.startupWithoutServing(); + if (zk instanceof FollowerZooKeeperServer) { + FollowerZooKeeperServer fzk = (FollowerZooKeeperServer) zk; + fzk.syncProcessor.setDelayForwarding(true); + for (PacketInFlight p : packetsNotLogged) { + fzk.logRequest(p.hdr, p.rec, p.digest); + } + packetsNotLogged.clear(); + fzk.syncProcessor.syncFlush(); + } + + writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true); + + if (zk instanceof FollowerZooKeeperServer) { + FollowerZooKeeperServer fzk = (FollowerZooKeeperServer) zk; + fzk.syncProcessor.setDelayForwarding(false); + fzk.syncProcessor.syncFlush(); + } + break; + } + } + } + ack.setZxid(ZxidUtils.makeZxid(newEpoch, 0)); + writePacket(ack, true); + zk.startServing(); + /* + * Update the election vote here to ensure that all members of the + * ensemble report the same vote to new servers that start up and + * send leader election notifications to the ensemble. + * + * @see https://issues.apache.org/jira/browse/ZOOKEEPER-1732 + */ + self.updateElectionVote(newEpoch); + + // We need to log the stuff that came in between the snapshot and the uptodate + if (zk instanceof FollowerZooKeeperServer) { + FollowerZooKeeperServer fzk = (FollowerZooKeeperServer) zk; + for (PacketInFlight p : packetsNotLogged) { + fzk.logRequest(p.hdr, p.rec, p.digest); + } + for (Long zxid : packetsCommitted) { + fzk.commit(zxid); + } + } else if (zk instanceof ObserverZooKeeperServer) { + // Similar to follower, we need to log requests between the snapshot + // and UPTODATE + ObserverZooKeeperServer ozk = (ObserverZooKeeperServer) zk; + for (PacketInFlight p : packetsNotLogged) { + Long zxid = packetsCommitted.peekFirst(); + if (p.hdr.getZxid() != zxid) { + // log warning message if there is no matching commit + // old leader send outstanding proposal to observer + LOG.warn( + "Committing 0x{}, but next proposal is 0x{}", + Long.toHexString(zxid), + Long.toHexString(p.hdr.getZxid())); + continue; + } + packetsCommitted.remove(); + Request request = new Request(null, p.hdr.getClientId(), p.hdr.getCxid(), p.hdr.getType(), null, null); + request.setTxn(p.rec); + request.setHdr(p.hdr); + request.setTxnDigest(p.digest); + ozk.commitRequest(request); + } + } else { + // New server type need to handle in-flight packets + throw new UnsupportedOperationException("Unknown server type"); + } + } + + protected void revalidate(QuorumPacket qp) throws IOException { + ByteArrayInputStream bis = new ByteArrayInputStream(qp.getData()); + DataInputStream dis = new DataInputStream(bis); + long sessionId = dis.readLong(); + boolean valid = dis.readBoolean(); + ServerCnxn cnxn = pendingRevalidations.remove(sessionId); + if (cnxn == null) { + LOG.warn("Missing session 0x{} for validation", Long.toHexString(sessionId)); + } else { + zk.finishSessionInit(cnxn, valid); + } + if (LOG.isTraceEnabled()) { + ZooTrace.logTraceMessage( + LOG, + ZooTrace.SESSION_TRACE_MASK, + "Session 0x" + Long.toHexString(sessionId) + " is valid: " + valid); + } + } + + protected void ping(QuorumPacket qp) throws IOException { + // Send back the ping with our session data + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + Map<Long, Integer> touchTable = zk.getTouchSnapshot(); + for (Entry<Long, Integer> entry : touchTable.entrySet()) { + dos.writeLong(entry.getKey()); + dos.writeInt(entry.getValue()); + } + + QuorumPacket pingReply = new QuorumPacket(qp.getType(), qp.getZxid(), bos.toByteArray(), qp.getAuthinfo()); + writePacket(pingReply, true); + } + + /** + * Shutdown the Peer + */ + public void shutdown() { + self.setZooKeeperServer(null); + self.closeAllConnections(); + self.adminServer.setZooKeeperServer(null); + + if (sender != null) { + sender.shutdown(); + } + + closeSocket(); + // shutdown previous zookeeper + if (zk != null) { + // If we haven't finished SNAP sync, force fully shutdown + // to avoid potential inconsistency + zk.shutdown(self.getSyncMode().equals(QuorumPeer.SyncMode.SNAP)); + } + } + + boolean isRunning() { + return self.isRunning() && zk.isRunning(); + } + + void closeSocket() { + if (sock != null) { + if (sockBeingClosed.compareAndSet(false, true)) { + if (closeSocketAsync) { + final Thread closingThread = new Thread(() -> closeSockSync(), "CloseSocketThread(sid:" + zk.getServerId()); + closingThread.setDaemon(true); + closingThread.start(); + } else { + closeSockSync(); + } + } + } + } + + void closeSockSync() { + try { + long startTime = Time.currentElapsedTime(); + if (sock != null) { + sock.close(); + sock = null; + } + ServerMetrics.getMetrics().SOCKET_CLOSING_TIME.add(Time.currentElapsedTime() - startTime); + } catch (IOException e) { + LOG.warn("Ignoring error closing connection to leader", e); + } + } + +} diff --git a/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/SendAckRequestProcessor.java b/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/SendAckRequestProcessor.java index c1399e53083..3b7a9dfc331 100644 --- a/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/SendAckRequestProcessor.java +++ b/zookeeper-server/zookeeper-server/src/main/java/org/apache/zookeeper/server/quorum/SendAckRequestProcessor.java @@ -67,7 +67,7 @@ public class SendAckRequestProcessor implements RequestProcessor, Flushable { LOG.warn("Closing connection to leader, exception during packet send", e); try { Socket socket = learner.sock; - if ( socket != null && ! learner.sock.isClosed()) { + if (socket != null && !socket.isClosed()) { learner.sock.close(); } } catch (IOException e1) { diff --git a/zookeeper-server/zookeeper-server/src/test/java/com/yahoo/vespa/zookeper/VespaZooKeeperTest.java b/zookeeper-server/zookeeper-server/src/test/java/com/yahoo/vespa/zookeper/VespaZooKeeperTest.java index db643d76e0d..3a125d72a89 100644 --- a/zookeeper-server/zookeeper-server/src/test/java/com/yahoo/vespa/zookeper/VespaZooKeeperTest.java +++ b/zookeeper-server/zookeeper-server/src/test/java/com/yahoo/vespa/zookeper/VespaZooKeeperTest.java @@ -38,6 +38,9 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; +/** + * @author jonmv + */ public class VespaZooKeeperTest { static final Path tempDirRoot = getTmpDir(); @@ -45,18 +48,17 @@ public class VespaZooKeeperTest { /** * Performs dynamic reconfiguration of ZooKeeper servers. - * + * <p> * First, a cluster of 3 servers is set up, and some data is written to it. * Then, 3 new servers are added, and the first 3 marked for retirement; * this should force the quorum to move the 3 new servers, but not disconnect the old ones. * Next, the old servers are removed. * Then, the cluster is reduced to size 1. * Finally, the cluster grows to size 3 again. - * + * <p> * Throughout all of this, quorum should remain, and the data should remain the same. */ @Test(timeout = 120_000) - @Ignore // Unstable, some ZK server keeps resetting connections sometimes. public void testReconfiguration() throws ExecutionException, InterruptedException, IOException, KeeperException, TimeoutException { List<ZooKeeper> keepers = new ArrayList<>(); for (int i = 0; i < 8; i++) keepers.add(new ZooKeeper()); @@ -126,7 +128,7 @@ public class VespaZooKeeperTest { static String writeData(ZookeeperServerConfig config) throws IOException, InterruptedException, KeeperException { try (ZooKeeperAdmin admin = createAdmin(config)) { List<ACL> acl = ZooDefs.Ids.OPEN_ACL_UNSAFE; - String node = admin.create("/test-node", "hi".getBytes(UTF_8), acl, CreateMode.EPHEMERAL_SEQUENTIAL); + String node = admin.create("/test-node", "hi".getBytes(UTF_8), acl, CreateMode.PERSISTENT_SEQUENTIAL); String read = new String(admin.getData(node, false, new Stat()), UTF_8); assertEquals("hi", read); return node; |